/* ============================================================
   components.css — potongan yang dipakai lebih dari satu layar.
   Butuh: tokens.css, base.css

   Semua kelas di sini berprefix fh-. Prefix bukan kosmetik:
   di prototipe 1.200 baris sudah terjadi tiga bentrok nama
   (.pill, .foot, .vip) dan style produksi 5.049 baris.
   ============================================================ */


/* ============================================================
   CELESTIAL — tiga tingkat perlakuan
   solid    : teks, dot, border tipis (pakai var(--t-celestial) langsung)
   gradient : kartu di grid           -> .fh-cel-grad
   kilau    : maksimal 1-2 per layar  -> .fh-cel-shine

   Batas kilau bukan soal selera: animasi background-position
   memaksa repaint terus-menerus dan berat di HP kelas menengah.
   ============================================================ */

.fh-cel-grad{
  border:2px solid transparent;
  background:
    linear-gradient(var(--pixel-surface),var(--pixel-surface)) padding-box,
    var(--cel-rainbow) border-box;
}

.fh-cel-shine{
  border:2px solid transparent;
  background:
    linear-gradient(var(--pixel-surface),var(--pixel-surface)) padding-box,
    var(--cel-loop) border-box;
  background-size:auto,300% 100%;
  animation:fh-celsweep 3.2s linear infinite;
}
@keyframes fh-celsweep{
  from{background-position:0 0,0% 50%}
  to  {background-position:0 0,300% 50%}
}

@media (prefers-reduced-motion:reduce){
  .fh-cel-shine{animation:none}
}


/* ============================================================
   HOLO — permukaan berkilau untuk tier tertinggi

   MENGAPA BUKAN .fh-cel-shine YANG DIPERBANYAK: kilau itu
   menganimasikan `background-position`, dan itu memaksa browser
   MENGGAMBAR ULANG elemennya tiap frame. Karena itu HANDOFF
   membatasinya 1-2 elemen per layar — batas yang masuk akal
   selama yang memasangnya adalah perancang.

   Di grid Tas yang menentukan jumlahnya PEMAIN, bukan perancang.
   Satu pemain bisa punya tiga ikan shiny, satu lagi tiga puluh.
   Jadi tekniknya harus yang biayanya tidak tumbuh secepat itu:

     yang dianimasikan di sini HANYA transform dan opacity.

   Keduanya dikerjakan compositor — browser menggeser lapisan yang
   sudah jadi, tanpa menggambar ulang isinya. Bedanya dengan
   background-position kira-kira satu tingkat besaran, dan itulah
   yang membuat efek ini boleh muncul di lebih dari dua kotak.

   Tetap ada batasnya: tiap kotak yang bergerak adalah satu lapisan
   compositor, dan lapisan makan memori. tas.js membatasi berapa
   kotak yang boleh HIDUP (FH_HOLO_MAX); sisanya tetap dapat pita
   cahaya tapi diam — dan pita yang diam berbiaya NOL saat runtime,
   cuma satu lapisan tambahan saat menggambar pertama kali.

   Induknya wajib position:relative dan overflow:hidden.
   ============================================================ */

/* Pitanya LEBAR, bukan garis. Versi sempit (34%) terbaca sebagai
   satu goresan cahaya yang lewat; yang lebar terbaca sebagai
   seluruh permukaannya memang berlapis foil — dan itu yang dicari.

   Kemiringannya ikut diturunkan 18 -> 14 derajat: makin lebar
   pitanya, makin miring dia harus dipotong tepi kotak, dan sudut
   yang terlalu tajam membuat sisa ruang di dua sudut terasa
   kosong. */
.fh-holo::after{
  content:"";
  position:absolute;z-index:2;
  top:-45%;bottom:-45%;left:10%;
  width:76%;
  pointer-events:none;
  background:linear-gradient(90deg,
    var(--holo-0),
    var(--holo-a) 20%,
    var(--holo-b) 50%,
    var(--holo-c) 80%,
    var(--holo-0));

  /* DIPARKIR DI LUAR UBIN, bukan di tengahnya.

     ⚠ Dulu transform-nya cuma rotate(14deg) — pita berhenti tepat
     melintang di tengah ubin. Itu masuk akal selama sapuannya
     berjalan terus-menerus: yang tidak dapat jatah gerak tetap
     terlihat berkilau, tanpa biaya runtime.

     Sejak sapuannya diberi JEDA, alasan itu terbalik. Ubin yang
     hidup sekarang menghabiskan 65% waktunya TANPA pita sama sekali
     (lihat fh-holo-sweep: bergerak sampai 57,5% siklus lalu diam di
     luar ubin). Jadi ubin yang diam dengan pita melintang di tengah
     justru jadi yang paling mencolok di antara tetangganya yang
     bersih — terbaca seperti animasi yang MACET, bukan seperti
     kilau diam.

     Gejalanya baru muncul waktu spesies celestial naik dari 4 jadi
     14: jatah HOLO_MAX (8 di Bestiary dan Tas, 6 di Market) mulai
     habis, dan enam ubin terakhir menampilkan pita beku itu.

     translateX(200%) = posisi ISTIRAHAT yang sama dengan ujung
     animasinya. Ubin tanpa jatah karena itu tampak persis seperti
     ubin berjatah yang sedang istirahat, dan tidak ada lagi yang
     terlihat macet. Kilau celestial-nya sendiri tidak hilang — itu
     datang dari gradient prisma di kotaknya, bukan dari pita ini.

     ⚠ Ini AMAN karena tiap tempat yang memasang .fh-holo di luar
     grid selalu ikut memasang .is-live (lihat fishMedia di ui.js
     dan sheet detail di tas.js). Pita diam cuma pernah muncul
     sebagai keadaan kehabisan jatah. */
  transform:rotate(14deg) translateX(200%);
}

/* Hanya yang ditandai HIDUP yang menyapu. Sisanya memakai posisi
   diam di atas — tetap terlihat berkilau, tanpa biaya runtime.

   .is-live ditulis terpisah dari .fh-holo supaya PITA dan KILIP
   bisa berdiri sendiri-sendiri: satu tier boleh punya kilip tanpa
   pita, dan penandanya tetap satu kelas yang sama. */
/* ⚠ SEMUA UBIN SEFASE, DAN ITU DISENGAJA.

   Sempat diberi fase sendiri-sendiri (--sweep, 8 slot) untuk
   mengobati kesan "jalan sekali lalu berhenti". Dicabut atas
   permintaan: yang diinginkan justru sapuan yang SERENTAK di
   seluruh grid, dan fase yang berbeda-beda membuatnya terlihat
   berkedip acak.

   Obat yang benar untuk kesan berhenti ternyata JEDA yang lebih
   pendek, bukan fase. Lihat --holo-dur di tokens.css: dari 13 dtk
   (jeda 10 dtk, terbaca berhenti) turun ke 8 dtk (jeda 5 dtk).

   ⚠ TIDAK ADA will-change DI SINI, dan itu juga disengaja.
   will-change:transform mempromosikan tiap pita jadi lapisan
   compositor PERMANEN. Selama jatah geraknya 4-8 ubin itu murah;
   sejak jatahnya dinaikkan supaya seluruh grid menyala (15 ubin di
   Bestiary, 19 di Tas) lapisan permanen sebanyak itu jadi mahal
   justru di perangkat yang paling butuh hemat. Tanpa will-change,
   lapisannya cuma hidup selama sapuan berjalan -- 38% waktu. */
.fh-holo.is-live::after{
  animation:fh-holo-sweep var(--holo-dur) linear infinite;
}

/* Jangkauannya dihitung terhadap LEBAR PITA (76% kotak), bukan
   lebar kotak: -150% membawanya lepas penuh di kiri, +200% lepas
   penuh di kanan.

   DUA ANGKA YANG BERBEDA, DAN INI YANG PALING MUDAH TERTUKAR:

     KECEPATAN SAPUAN   = berapa lama pita menyeberang (7,5 dtk,
                          yaitu bagian 0%-50% keyframe di bawah)
     JEDA ANTAR-SAPUAN  = berapa lama tidak ada pita sama sekali

   Versi lama tidak punya jeda: begitu pita keluar di kanan dia
   langsung masuk lagi dari kiri, dan yang tersisa cuma 2,5 dtk
   sunyi — sisa waktu pita berada di luar ubin. Terbaca seperti
   pita yang mondar-mandir tanpa henti.

   Menaikkan durasi saja TIDAK menyelesaikannya: itu memperlambat
   sapuannya, bukan memperpanjang jedanya, dan jedanya cuma ikut
   naik sepertiganya. Yang benar MENAHAN pita di luar layar sesudah
   dia lewat — itulah gunanya singgahan kedua yang nilainya sama
   dengan singgahan akhir: pita bergerak sampai 35,4% siklus lalu
   DIAM di luar ubin sampai 100%.

      8 dtk = 4,6 dtk menyeberang + 3,4 dtk diam
     sapuan yang TERLIHAT 3,0 dtk, jeda 5,0 dtk

   ⚠ JEDANYA SUDAH DUA KALI DISETEL, dan keduanya karena keliru:
   2,5 dtk terbaca "mondar-mandir tanpa henti", 10 dtk terbaca
   "jalan sekali lalu berhenti". 5 dtk di antara keduanya.

   Begitu keduanya terpisah, masing-masing bisa disetel sendiri:
   4,6 dtk mengatur SECEPAT APA pita menyeberang, 13 dtk mengatur
   SEBERAPA SERING dia datang. Sempat tertukar sekali — jedanya
   diperpanjang dengan memperlambat sapuannya, dan yang terjadi
   pitanya jadi merayap. Kalau salah satu terasa salah, periksa
   dulu yang mana dari keduanya.

   Lompatan balik dari 100% ke 0% tidak terlihat karena di kedua
   ujung itu pita memang sedang di luar ubin.

   ⚠ ANGKA INI MENGIKAT @keyframes fh-ubin1/2 di bawah: siklus
   transformasi ubin WAJIB kelipatan durasi ini. Kalau diubah,
   siklus itu dan persentasenya ikut dihitung ulang. */
@keyframes fh-holo-sweep{
  0%        {transform:rotate(14deg) translateX(-150%)}
  57.5%,100%{transform:rotate(14deg) translateX(200%)}
}

/* Titik kilip. Dipakai Shiny DAN Celestial — keduanya tier yang
   di bot memang bernama "shiny", dan berkilip itu arti harfiah
   namanya. Yang membedakan keduanya bukan kilipnya, tapi apa yang
   ADA DI BAWAHNYA: shiny berkilip di atas foil krom, celestial
   berkilip di atas prisma yang juga disapu pita cahaya.

   YANG DIKEJAR: BINTANG DI LANGIT. Satu titik menyala pelan di
   suatu tempat, padam, lalu titik LAIN menyala di tempat lain.
   Tidak pernah semuanya sekaligus, dan tidak pernah di petak yang
   itu-itu saja.

   ENAM BINTANG, dan angka itu turun dari satu aturan keras:

     SATU LAPIS = SATU TITIK. opacity berlaku untuk seluruh lapis,
     jadi dua titik dalam satu lapis WAJIB menyala dan padam
     bersama-sama — yang terlihat sepasang lampu berkedip, bukan
     bintang. Ini pernah dicoba (dua titik per lapis) dan itu persis
     keluhannya.

   Karena itu jumlah bintang = jumlah lapis, dan lapis datang
   berkelompok tiga: tiap elemen menyumbang dirinya sendiri plus
   ::before dan ::after. Dua elemen (lihat SPARK di ui.js) = enam
   lapis = enam bintang yang bisa berdiri sendiri-sendiri.

   ⚠ MAU LEBIH BANYAK? Tambah ELEMEN di SPARK, lalu tambahkan
   aturannya di sini. Menambah titik ke dalam satu lapis akan
   terlihat seperti kemunduran ke versi yang sudah ditolak.

   Empat hal lain yang membuatnya terbaca sebagai langit:

   1. ENAM SIKLUS YANG TIDAK SELARAS: 15 / 17 / 19 / 21 / 23 / 25
      detik. Kelipatan persekutuannya berjam-jam, jadi pola
      gabungannya tidak pernah kembali sama.

   2. TIGA IRAMA NYALA yang puncaknya duduk di sepertiga berbeda
      dalam tiap perempat siklus (fh-kilip-a/b/c). Bahkan kalau dua
      lapis kebetulan sefase, mereka tetap menyala bergantian.

   3. NYALANYA PELAN: tiap kilip membentang 11% siklus, jadi 1,7
      detik di lapis tercepat sampai 2,8 detik di yang paling
      lambat. Padam penuh di antaranya, karena bintang yang tidak
      pernah benar-benar hilang terbaca sebagai titik debu di layar.

      ⚠ Yang menentukan seberapa pelan BUKAN lebar kilipnya saja,
      tapi lebar DIKALI panjang siklusnya — dan lebar itu sekaligus
      menentukan seberapa sering titiknya menyala. Dua hal itu satu
      tuas yang sama di sini: melebarkan kilip = lebih pelan DAN
      lebih ramai. Kalau cuma ingin salah satunya, yang digeser
      panjang siklusnya.

   4. TITIKNYA BERPINDAH, DAN MENYEBAR SAMPAI KE TEPI. Tiap lapis
      adalah satu kotak kecil berisi titiknya, dan kotak itu
      MELOMPAT ke empat tempat berbeda tiap siklus lewat
      background-position, steps(1,end) — tidak ada gerak yang
      terlihat, dia cuma sudah pindah waktu menyala lagi, dan
      lompatannya jatuh tepat saat lapisnya padam. Enam lapis x
      empat singgahan = 24 tempat, disebar merata ke seluruh petak.

      ⚠ UKURAN KOTAK ITU YANG MENENTUKAN SEBERAPA LUAS SEBARANNYA,
      dan ini mudah salah. background-position persen menempatkan
      kotak di dalam SISA ruang, yaitu (100% − ukuran kotak). Waktu
      kotaknya 60%, sisanya cuma 40%, dan titik di tengah kotak
      tidak pernah keluar dari pita 30%–70% — persis keluhan "tidak
      menyebar". Di 22%, sisanya 78% dan titiknya menjangkau
      11%–89%: hampir seluruh kotak ikan. Mengecilkan kotak =
      melebarkan sebaran, bukan sebaliknya.

   --kilip = BENIH per kotak, angka bulat 0-11 yang dipasang JS dari
   id ekornya, dipakai menggeser FASE keenam lapis lewat
   animation-delay negatif. Karena letak titiknya ikut berubah
   menurut waktu, beda fase otomatis berarti beda letak: dua ikan
   shiny bersebelahan tidak pernah menyala bersamaan maupun di titik
   yang sama. Kosong = 0, dan layar yang belum mengirimnya tetap
   benar — cuma seragam.

   ⚠ BIAYA, dan ini menyimpang dari aturan di kepala blok HOLO di
   atas: background-position memang menyebabkan PENGGAMBARAN ULANG,
   bukan cuma pergeseran lapisan. Yang membuatnya tetap murah adalah
   steps(1,end): posisinya berubah empat kali per BELASAN detik,
   bukan enam puluh kali per detik, dan bidang yang digambar ulang
   cuma sebesar satu kotak ikan. Kalau suatu saat lompatannya
   diperhalus jadi gerak menerus, biayanya naik ribuan kali lipat —
   jangan.

   Jumlah lapisan compositor per kotak sekarang ENAM. Jatah kotak
   yang boleh hidup di tiap layar (FH_HOLO_MAX di Tas, LIVE_MAX di
   Museum) bernilai enam kali lipat dari sebelumnya; jangan
   dinaikkan tanpa mengukur. */
.fh-spark{
  /* Resep satu titik: inti putih pekat lalu halo tipis. Itu yang
     membuatnya terbaca TEBAL — memperbesar radius saja cuma
     menghasilkan bulatan kasar, bukan kilip. Custom property
     menurun ke ::before dan ::after tanpa perlu diulang. */
  --titik:var(--spark) 35%,var(--spark-mid) 60%,var(--holo-0);

  /* JARI-JARI TITIK, tiga ukuran. Variabel, bukan angka langsung,
     karena WADAHNYA ADA DUA UKURAN yang jauh berbeda dan angka yang
     benar di satu tempat salah di tempat lain — lihat blok
     "UKURAN TITIK IKUT WADAHNYA" di bawah. */
  --r-s:2px;
  --r-m:2.5px;
  --r-l:3px;

  position:absolute;inset:0;z-index:3;
  pointer-events:none;

  /* 22%, dan angka ini yang menentukan sebaran — lihat catatan 4
     di atas. no-repeat wajib: tanpa itu ubinnya berulang memenuhi
     kotak dan yang terlihat bukan satu bintang tapi kisi-kisi. */
  background-size:22% 22%;
  background-repeat:no-repeat;
  opacity:.3;
}
.fh-spark::before,
.fh-spark::after{
  content:'';position:absolute;inset:0;
  pointer-events:none;opacity:.3;
  background-size:22% 22%;
  background-repeat:no-repeat;
}

/* Enam ukuran titik yang tidak sama: bintang di langit tidak sama
   terangnya, dan enam titik sebesar-besaran terbaca sebagai enam
   lampu. background-position di sini adalah singgahan PERTAMA tiap
   lapis — dia juga yang terlihat saat kotaknya tidak bergerak
   (bukan .is-live). */
.fh-spark          {background-image:radial-gradient(circle var(--r-l) at 50% 50%,var(--titik));background-position:6% 10%}
.fh-spark::before  {background-image:radial-gradient(circle var(--r-m) at 50% 50%,var(--titik));background-position:72% 8%}
.fh-spark::after   {background-image:radial-gradient(circle var(--r-s) at 50% 50%,var(--titik));background-position:38% 92%}
.fh-spark--2         {background-image:radial-gradient(circle var(--r-m) at 50% 50%,var(--titik));background-position:50% 4%}
.fh-spark--2::before {background-image:radial-gradient(circle var(--r-s) at 50% 50%,var(--titik));background-position:90% 16%}
.fh-spark--2::after  {background-image:radial-gradient(circle var(--r-l) at 50% 50%,var(--titik));background-position:20% 4%}

/* ============================================================
   UKURAN TITIK IKUT WADAHNYA

   Kilip yang sama dipakai di DUA wadah yang jauh berbeda ukurannya,
   dan ini diukur, bukan dikira:

     kotak rak / grid        54 x 54 px
     panggung sheet detail  332 x 187 px      -> 6x lebih lebar

   Yang IKUT membesar cuma kotak jalannya, karena dia persen
   (22%): 11,9px di kotak rak, 73px di panggung. Jari-jari titiknya
   TIDAK — dia panjang mutlak, dan itu memang harus begitu supaya
   titik di kotak rak tidak menyusut jadi debu.

   Akibatnya satu titik 6px terbaca 11% lebar wadah di kotak rak,
   tapi cuma 1,8% di panggung — di sana dia nyaris hilang di atas
   video ikan yang bergerak. Angka yang benar untuk satu wadah
   memang salah untuk yang lain.

   NAIKNYA TIDAK SEBANDING (bukan 6x), dan itu disengaja. Titik
   sebesar 11% dari panggung akan jadi bulatan 36px — itu bola,
   bukan bintang. Yang dikejar tetap terbaca sebagai KILAU: sekitar
   4% lebar panggung sudah cukup terlihat tanpa berhenti terasa
   kecil.

   Berlaku untuk KEEMPAT layar yang memakai panggung ini (Tas,
   Bestiary, Market, Museum), karena selektornya menyasar
   panggungnya, bukan layarnya.
   ============================================================ */
.fh-fishstage__art .fh-spark{
  --r-s:4.5px;
  --r-m:5.5px;
  --r-l:7px;
}


/* DUA ANIMASI PER LAPIS: nyalanya dan pindahnya. Keduanya WAJIB
   berdurasi dan berdelay sama persis — itu yang menjamin lompatan
   posisinya jatuh saat lapisnya padam.

   animation-timing-function ditulis sebagai DAFTAR yang sejajar
   dengan daftar animasinya: ease-in-out untuk nyalanya, steps untuk
   pindahnya. Satu nilai tunggal akan berlaku untuk keduanya, dan
   kotaknya ikut meluncur pelan alih-alih melompat.

   animation-delay ditulis SESUDAH shorthand-nya: `animation`
   mengembalikan delay ke 0, jadi urutan terbalik membuat benih per
   kotak diam-diam tidak berefek. */
.is-live .fh-spark{
  animation:fh-kilip-a 15s infinite,fh-geser-a1 15s infinite;
  animation-timing-function:ease-in-out,steps(1,end);
  animation-delay:calc(var(--kilip,0) * -1.07s),calc(var(--kilip,0) * -1.07s);
  will-change:opacity;
}
.is-live .fh-spark::before{
  animation:fh-kilip-b 17s infinite,fh-geser-b1 17s infinite;
  animation-timing-function:ease-in-out,steps(1,end);
  animation-delay:calc(var(--kilip,0) * -1.23s - 4.1s),
                  calc(var(--kilip,0) * -1.23s - 4.1s);
  will-change:opacity;
}
.is-live .fh-spark::after{
  animation:fh-kilip-c 19s infinite,fh-geser-c1 19s infinite;
  animation-timing-function:ease-in-out,steps(1,end);
  animation-delay:calc(var(--kilip,0) * -1.41s - 8.3s),
                  calc(var(--kilip,0) * -1.41s - 8.3s);
  will-change:opacity;
}
.is-live .fh-spark--2{
  animation:fh-kilip-c 21s infinite,fh-geser-a2 21s infinite;
  animation-timing-function:ease-in-out,steps(1,end);
  animation-delay:calc(var(--kilip,0) * -1.57s - 2.6s),
                  calc(var(--kilip,0) * -1.57s - 2.6s);
  will-change:opacity;
}
.is-live .fh-spark--2::before{
  animation:fh-kilip-a 23s infinite,fh-geser-b2 23s infinite;
  animation-timing-function:ease-in-out,steps(1,end);
  animation-delay:calc(var(--kilip,0) * -1.73s - 11.4s),
                  calc(var(--kilip,0) * -1.73s - 11.4s);
  will-change:opacity;
}
.is-live .fh-spark--2::after{
  animation:fh-kilip-b 25s infinite,fh-geser-c2 25s infinite;
  animation-timing-function:ease-in-out,steps(1,end);
  animation-delay:calc(var(--kilip,0) * -1.91s - 6.2s),
                  calc(var(--kilip,0) * -1.91s - 6.2s);
  will-change:opacity;
}

/* TIGA IRAMA, dipakai bergantian oleh enam lapis. Semuanya empat
   kilip per siklus — satu di tiap perempat — tapi puncaknya duduk
   di sepertiga yang berbeda dalam perempat itu:

     a  puncak di tengah perempat   (12,5 / 37,5 / 62,5 / 87,5)
     b  puncak di akhir perempat    (17   / 42   / 67   / 92  )
     c  puncak di awal perempat     ( 8   / 33   / 58   / 83  )

   Tiap kilip membentang 11% siklus, jadi 1,7 detik di lapis 15
   detik sampai 2,8 detik di lapis 25 detik.

   ⚠ Tiap perempat WAJIB berakhir gelap (nilai 0 di 25/50/75/100%).
   Itu jendela tempat kotaknya melompat; kalau salah satu kilip
   dilebarkan sampai menyentuh batas perempat, titiknya akan
   terlihat teleport. */
@keyframes fh-kilip-a{
  0%,7%     {opacity:0}
  12.5%     {opacity:1}
  18%,32%   {opacity:0}
  37.5%     {opacity:.85}
  43%,57%   {opacity:0}
  62.5%     {opacity:1}
  68%,82%   {opacity:0}
  87.5%     {opacity:.9}
  93%,100%  {opacity:0}
}
@keyframes fh-kilip-b{
  0%,11.5%  {opacity:0}
  17%       {opacity:.9}
  22.5%,36.5%{opacity:0}
  42%       {opacity:1}
  47.5%,61.5%{opacity:0}
  67%       {opacity:.85}
  72.5%,86.5%{opacity:0}
  92%       {opacity:1}
  97.5%,100%{opacity:0}
}
@keyframes fh-kilip-c{
  0%,2.5%   {opacity:0}
  8%        {opacity:.95}
  13.5%,27.5%{opacity:0}
  33%       {opacity:1}
  38.5%,52.5%{opacity:0}
  58%       {opacity:.8}
  63.5%,77.5%{opacity:0}
  83%       {opacity:1}
  88.5%,100%{opacity:0}
}

/* 24 singgahan. Disebar seperti kisi berjitter, bukan diacak
   sembarangan: yang dijaga bukan cuma titiknya berpindah, tapi
   bahwa seluruh petak kebagian — 2% sampai 96% mendatar, 4% sampai
   94% menurun, tanpa dua singgahan yang saling berdempetan. */
@keyframes fh-geser-a1{
  0%{background-position:6% 10%}   25%{background-position:58% 30%}
  50%{background-position:30% 74%} 75%{background-position:86% 54%}
}
@keyframes fh-geser-b1{
  0%{background-position:72% 8%}   25%{background-position:16% 44%}
  50%{background-position:94% 80%} 75%{background-position:44% 60%}
}
@keyframes fh-geser-c1{
  0%{background-position:38% 92%}  25%{background-position:2% 66%}
  50%{background-position:66% 20%} 75%{background-position:24% 26%}
}
@keyframes fh-geser-a2{
  0%{background-position:50% 4%}   25%{background-position:12% 84%}
  50%{background-position:80% 38%} 75%{background-position:36% 50%}
}
@keyframes fh-geser-b2{
  0%{background-position:90% 16%}  25%{background-position:28% 58%}
  50%{background-position:60% 88%} 75%{background-position:6% 34%}
}
@keyframes fh-geser-c2{
  0%{background-position:20% 4%}   25%{background-position:76% 66%}
  50%{background-position:46% 38%} 75%{background-position:96% 94%}
}

/* Aturan yang sama seperti kilau celestial: gerak dimatikan penuh
   kalau sistem memintanya. Pita dan titiknya TETAP ada, cuma diam —
   yang dimatikan geraknya, bukan penandanya. */
@media (prefers-reduced-motion:reduce){
  /* Tanpa animasi, pita dikembalikan ke tengah ubin — kebalikan
     dari aturan di atas, dan itu disengaja. Pita melintang terbaca
     "macet" hanya kalau ada tetangga yang bergerak; di sini TIDAK
     ADA yang bergerak, jadi yang tersisa cuma kilau diam. Tanpa
     baris ini pitanya terparkir di luar ubin dan pemakai
     reduced-motion kehilangan kilaunya sama sekali. */
  .fh-holo.is-live::after{
    animation:none;
    transform:rotate(14deg);
  }
  .is-live .fh-spark,
  .is-live .fh-spark::before,
  .is-live .fh-spark::after{animation:none;opacity:.45}
}


/* ============================================================
   PANEL — pengganti kotak-bergaris.
   Arah visualnya: kurangi border, pakai bidang yang sedikit
   lebih terang dari base.
   ============================================================ */

.fh-panel{
  background:var(--pixel-surface);
  border:1px solid var(--pixel-border);
  border-radius:var(--r-lg);
  overflow:hidden;
}


/* ============================================================
   TILE — kotak aset berkilau.
   Dipakai slot equipment di Home dan thumbnail di grid overlay.
   Warna isi datang dari --t yang dipasang inline oleh JS
   (hasil FH.gearColor / warna tier), bukan dari kelas.
   ============================================================ */

.fh-tile{
  /* position:relative supaya lapisan kedua ikan (.fh-ikon2) punya
     jangkar. Aman dipasang ke SEMUA ubin: tidak ada satu pun anak
     .fh-tile yang absolut selain lapisan itu -- centang dan gembok
     di kartu equipment bersaudara dengan ubinnya, bukan di
     dalamnya. */
  position:relative;
  border-radius:7px;
  border:1px solid var(--edge-dark);
  background:
    linear-gradient(180deg,var(--gloss-hi),rgba(255,255,255,0) 45%,var(--gloss-lo)),
    var(--t,var(--pixel-raised));
  box-shadow:inset 0 0 0 1px var(--edge-in);
  display:grid;place-items:center;
  font-family:var(--font-pixel);font-size:8px;line-height:1.7;
  color:var(--pixel-muted);
  overflow:hidden;
}

/* Aset gear adalah ilustrasi halus. .fh-tile tidak memasang
   image-rendering apa pun, jadi tidak ada yang diwarisi keliru —
   tapi kelas .fh-hd-art tetap ditulis eksplisit di elemen img. */
.fh-tile__img{
  width:100%;height:100%;object-fit:contain;
  /* HALO GELAP TIPIS di sekeliling aset.

     Kotak slot terisi warna rarity PENUH, jadi aset yang warnanya
     senada dengan kotaknya akan hilang — dan itu bukan kasus
     karangan: Umpan Mythical ungu duduk di kotak --g-3 yang juga
     ungu, hue yang sama, bukan cuma terang yang mirip.

     Solusinya bayangan gelap tipis di belakang aset, BUKAN mengubah
     warna kotak (ASSET_SPEC bagian 3) — warna kotak itu yang
     menyampaikan rarity, dan mengubahnya untuk satu aset berarti
     merusak arti yang dibawa semua kotak lain.

     --art-edge sudah dipakai untuk hal yang sama persis di tiga
     tempat: ubin Tas (2px), ubin Museum (3px), dan panggung hadiah
     Gacha (6px). Ditulis di sini, bukan di tiap layar, karena kotak
     gear yang sama muncul di Home, overlay ganti perlengkapan, dan
     Market — dan aturan yang disalin ke tiga tempat akan menyimpang
     di satu tempat. */
  filter:drop-shadow(0 0 2px var(--art-edge));
}

/* MARGIN AMAN UNTUK ASET PERLENGKAPAN.

   convert_gear.py memangkas ruang kosong lalu menyisakan margin 4%,
   jadi sumbu panjang tiap aset mengisi ~94% kanvasnya. Di kotak 66px
   itu berarti ilustrasinya berhenti 2px dari tepi kotak — menempel,
   dan yang bentuknya bulat (Umpan Ajaib, Coklat) menempel di keempat
   sisi sekaligus sementara yang panjang tipis (Ikan) menyisakan
   ruang besar di atas dan bawah. Satu grid berisi keduanya terbaca
   tidak rata.

   8% di sini, bukan margin lebih besar di converter: margin yang
   dipanggang ke berkas membuang piksel yang sudah dibayar (384px
   jadi efektif 356px), dan angkanya tidak bisa lagi diubah tanpa
   mengkonversi ulang semua aset.

   ⚠ Aturan ini DICABUT untuk slot Home — lihat
   `.fh-slot .fh-tile__img--gear` di home.css. Di sana tidak ada grid
   yang perlu dirapikan, jadi yang tertinggal cuma dua lapis margin
   yang bertumpuk.

   Dipisahkan dari .fh-tile__img dan TIDAK dipasang ke ikan. Kotak
   ikan di Tas, Bestiary, Museum, dan Market memakai kelas dasar yang
   sama, dan di sana aset penuh memang yang diinginkan — ikan berdiri
   sendiri di kotaknya, tidak berbaris dengan enam bentuk lain yang
   harus terbaca satu keluarga.

   Sisanya bukan urusan CSS: Ikan tetap tampak lebih kecil dari Ajaib
   karena digambar panjang dan tipis, dan hanya gambarnya yang bisa
   mengubah itu (ASSET_SPEC bagian 3, "Bentuk"). */
.fh-tile__img--gear{padding:8%}


/* ============================================================
   WUJUD KEDUA DI UBIN — transformasi yang DIIKAT KE SAPUAN HOLO

   ⚠ JADWALNYA SENGAJA BEDA DARI KARTU PAPAN PERINGKAT, dan dulu
   memang sempat sama. Keduanya dipisah karena syaratnya berbeda:
   ubin punya pita holo yang menyapu di atasnya, kartu rank tidak.
   Jadwal rank tinggal di rank.css bersama alasannya sendiri.

   SETIAP SAPUAN MEMICU SATU PERUBAHAN. Sapuan pertama mengubah
   wujud pertama jadi kedua, sapuan berikutnya mengembalikannya,
   begitu seterusnya — tidak ada sapuan yang lewat tanpa membawa
   akibat.

   ⚠ ITU YANG MENGUNCI SIKLUSNYA JADI TEPAT DUA KALI SIKLUS HOLO
   — dua kali SIKLUS (13 dtk), bukan dua kali lama menyeberang
   (4,6 dtk). Sejak holo punya jeda, keduanya angka yang berbeda.
   Satu putaran transformasi butuh maju DAN balik; kalau tiap sapuan
   memicu satu perubahan, satu putaran memakan dua sapuan. Jadi
   16 dtk di sini bukan angka yang dipilih, melainkan 2 x 8 dtk
   yang dihitung -- dan sejak durasinya calc(var(--holo-dur) * 2),
   dia ikut sendiri kalau --holo-dur diubah. Kalau durasi holo diubah, angka ini ikut, dan
   persentase di bawah dihitung ulang — siklus yang bukan
   kelipatan sapuan akan HANYUT: mula-mula pas, lalu makin lama
   makin meleset sampai perubahannya terjadi saat pita entah di
   mana.

   Versi sebelumnya memicu perubahan tiap sapuan KE-2, jadi separuh
   sapuan lewat tanpa akibat — dan sapuan yang lewat begitu saja
   membuat kaitannya ke pita jadi tidak terbaca sama sekali.

   Geometrinya dihitung, bukan dicocok-cocokkan: pita menaungi
   tengah ubin dari detik 1,349 sampai 2,663 tiap sapuan, jadi maju
   jatuh di 8,43%-16,64% siklus dan balik di 58,43%-66,64%.
   ============================================================ */

@keyframes fh-ubin1{
  0%,13%      {opacity:1}
  16.6%,58.4% {opacity:0}
  62%,100%    {opacity:1}
}
@keyframes fh-ubin2{
  0%,8.4%     {opacity:0}
  15%,60%     {opacity:1}
  66.6%,100%  {opacity:0}
}

/* UBIN IKAN. Ikan yang punya wujud kedua (<sp>-2.webp) menumpuk
   dua gambar di ubin yang sama, persis seperti kartu papan
   peringkat.

   TIDAK DIGERBANGI .is-live, dan itu disengaja. Cuma sebagian ubin
   yang pitanya benar-benar menyapu (jatah HOLO_MAX per layar), dan
   ikan yang tiernya tidak punya pita sama sekali tidak akan pernah
   punya sapuan untuk diikuti. Menggerbanginya berarti transformasi
   HILANG DIAM-DIAM untuk mereka. Yang diikat cuma WAKTUNYA: di ubin
   yang pitanya hidup keduanya berbarengan, di ubin lain
   perubahannya tetap terjadi pada jadwal yang sama — tidak ada yang
   salah di sana, cuma tidak ada pita yang menemani.

   Kesejajarannya datang dari ASETNYA, bukan dari sini:
   convert_fish.py memangkas kedua wujud dengan KOTAK YANG SAMA
   (lihat kelompokkan() di sana). Tanpa itu keduanya dipangkas ke
   isinya masing-masing — dan karena wujud kedua meluber lebih
   lebar, badan ikannya akan tergambar dengan ukuran yang berbeda
   dan terlihat mengkerut saat transisi.

   Dipakai empat layar sekaligus: Tas, Bestiary, Market, dan rak
   Museum. Satu ikan tidak boleh bertransformasi di satu layar dan
   diam di layar lain — itu terbaca seperti kerusakan, bukan
   seperti pilihan. */
/* Durasinya DIHITUNG dari siklus holo, bukan ditulis ulang: satu
   putaran transformasi = dua sapuan (maju di sapuan pertama, balik
   di sapuan kedua). Ditulis 26s mati, angka itu akan menyimpang
   diam-diam begitu --holo-dur diubah. */
.fh-ikon1.fh-ikon1{
  animation:fh-ubin1 calc(var(--holo-dur) * 2) ease-in-out infinite;
}

/* KELASNYA DITULIS DUA KALI, dan itu bukan salah ketik.

   Beberapa layar mengatur ikonnya lewat selektor anak langsung:
     .fh-bagtile   > img{position:relative;z-index:1}   (tas.css)
     .fh-mus__tile > img{position:relative;z-index:1}   (museum.css)

   Keduanya berkekhususan (0,1,1) dan berkas layar dimuat SESUDAH
   components.css — jadi `.fh-ikon2` polos (0,1,0) KALAH, dan
   position:absolute-nya batal. Akibatnya lapisan kedua kembali jadi
   item grid biasa, turun ke baris bawah ubin, dan ubinnya terbagi
   dua. Gejalanya: gambar kedua "hilang" dan gambar pertama
   mengkerut. Sudah terjadi dua kali, di Museum dan di Tas.

   `.fh-ikon2.fh-ikon2` berkekhususan (0,2,0) dan mengalahkan
   keduanya, tanpa peduli siapa induknya. Ditulis SEKALI di sini,
   bukan ditambal per layar: layar berikutnya yang menulis
   `X > img{position:...}` akan tetap benar tanpa tahu apa-apa
   tentang aturan ini. Itu sebabnya cara ini dipilih daripada
   menambah timpaan di tas.css dan museum.css.

   z-index tetap WAJIB, dan alasannya sama dengan centang di kartu
   equipment: .fh-tile__img memakai filter:drop-shadow, jadi dia
   membentuk stacking context sendiri dan lapisan absolut tanpa
   z-index bisa tenggelam di baliknya. */
.fh-ikon2.fh-ikon2{
  position:absolute;inset:0;z-index:2;
  width:100%;height:100%;object-fit:contain;
  opacity:0;
  animation:fh-ubin2 calc(var(--holo-dur) * 2) ease-in-out infinite;
}

@media (prefers-reduced-motion:reduce){
  /* Diam di wujud PERTAMA — wujud yang dipakai ikan lain, jadi
     grid tetap terbaca satu keluarga. */
  .fh-ikon1.fh-ikon1{animation:none;opacity:1}
  .fh-ikon2.fh-ikon2{animation:none;opacity:0}
}


/* Titik warna tier di baris menu saring. Dipakai menu tier
   Bestiary dan Market. Titik, bukan seluruh baris diwarnai:
   sembilan baris berwarna penuh di menu setinggi 320px terbaca
   seperti palet, bukan seperti pilihan. */
.fh-dot-tier{
  flex:none;width:9px;height:9px;border-radius:2px;
}


/* ============================================================
   CHIP — pil kecil di header (level, koin)
   ============================================================ */

.fh-chip{
  display:flex;align-items:center;gap:7px;
  padding:5px 11px;border-radius:20px;
  background:var(--pixel-raised);
  border:1px solid var(--pixel-border);
}
/* margin-left:auto mendorong angka ke kanan, supaya dua chip
   bertumpuk yang dipaksa sama lebar punya angka sejajar vertikal. */
.fh-chip b{
  font-family:var(--font-pixel);font-size:10px;letter-spacing:.3px;
  margin-left:auto;
}
.fh-chip__label{
  font-family:var(--font-pixel);font-size:7px;
  color:var(--pixel-muted);flex-shrink:0;
}

.fh-coin{
  width:16px;height:16px;border-radius:50%;flex-shrink:0;
  background:radial-gradient(circle at 34% 30%,
    var(--coin-hi),var(--pixel-primary) 58%,var(--coin-lo));
  box-shadow:inset 0 0 0 1.5px var(--emboss-in);
}


/* ============================================================
   BADGE TIER — nama tier ikan / karakter.
   Warna diambil dari currentColor supaya cukup satu properti
   yang diset JS.
   ============================================================ */

.fh-tier{
  display:inline-block;
  font-family:var(--font-pixel);font-size:7px;letter-spacing:.3px;
  padding:4px 7px;border-radius:var(--r-xs);
  background:var(--pixel-sunken);
  border:1px solid currentColor;
}

/* VARIAN TERISI: BADGE TIER = KOTAKNYA DALAM UKURAN KECIL.

   Dipakai sheet detail ikan (Tas, Bestiary, Market) dan baris ikan
   di papan mingguan. Warnanya datang dari --t yang dipasang JS
   dengan FH.tierFill(), jadi celestial memang pelangi — permukaan
   yang sama persis dengan kotak ikan di grid.

   Sempat dikerjakan sebagai cincin 1px bergradient di sekeliling
   plat gelap. Secara teknis benar — gradientnya persis sama dengan
   kotak di grid — tapi gagal total dilihat: satu piksel tepi pada
   badge 90x16 tidak pernah terbaca sebagai "warna slot". Sekarang
   badge-nya DIISI permukaan itu, sama seperti kotaknya.

   HURUFNYA GELAP, dan itu hasil pengukuran, bukan selera. Diuji
   terhadap 12 bidang tier: ink gelap meloloskan 11 pada 4.5:1, ink
   terang hanya 1. Wajar — dari sembilan tier, tujuh warnanya
   terang. Garis tepi teks bukan pilihan di sini: hurufnya 7px font
   pixel, dan outline 1px pada goresan 1px justru menelan bentuknya.

   background-size 240% ADALAH bagian dari legibilitasnya, bukan
   hiasan. Satu-satunya bidang yang gagal diukur adalah tepi gelap
   gradient secret, yang cuma 3.25:1. Dengan gradient diperbesar,
   badge hanya menampilkan bagian dalam yang terang dan tepi itu
   tidak pernah sampai ke bawah huruf. Untuk enam tier berwarna
   rata nilainya tidak berpengaruh sama sekali.

   ⚠ background-size HARUS ditulis SESUDAH background: shorthand-nya
   mengembalikan background-size ke auto, jadi urutan terbalik
   membuat baris ini diam-diam tidak berefek.

   ⚠ Dua selektor, satu aturan. .fh-fishhd__tier tidak diberi kelas
   --isi supaya markup empat sheet yang sudah ada tidak perlu
   disentuh; yang penting deklarasinya TIDAK disalin. */
.fh-tier--isi,
.fh-fishhd .fh-fishhd__tier{
  font-size:7px;
  color:var(--ink-on-tier);
  background:var(--t,var(--pixel-raised));
  background-size:240% 240%;
  background-position:center;
  border-color:var(--art-edge);
}


/* ============================================================
   TAG KLAN
   Tanpa kurung siku: border sudah menjalankan fungsi pemisah.
   Kalau pemain tidak punya klan, HAPUS elemennya — jangan
   tampilkan kosong.
   ============================================================ */

.fh-clan{
  font-family:var(--font-pixel);font-size:8px;
  color:var(--pixel-muted);
  border:1px solid var(--pixel-border);
  border-radius:var(--r-xs);
  padding:3px 4px;flex-shrink:0;
}

/* Varian logam, dipakai di chat dan kartu profil. */
.fh-clan--metal{
  font-size:7px;line-height:1;padding:3px 5px;border:0;
  color:var(--clan-ink);
  background:linear-gradient(160deg,var(--clan-hi),var(--clan-mid) 55%,var(--clan-lo));
  box-shadow:inset 0 0 0 1px var(--gloss-edge);
}


/* ============================================================
   MAHKOTA VIP
   Digambar sebagai path pixel, shape-rendering:crispEdges supaya
   tepinya tidak dihaluskan browser.
   ============================================================ */

.fh-crown{fill:var(--pixel-primary);shape-rendering:crispEdges;flex-shrink:0}

/* ---------- FOTO DAN NAMA BISA DITEKAN ----------

   Menandai foto atau nama pemain sebagai jalan masuk ke kartu
   profilnya. Dipakai chat global, papan mingguan, dan papan
   sepanjang masa; markupnya dibangun FH.tapUser() dan penangannya
   FH.onTapUser(), keduanya di ui.js.

   TANPA GARIS BAWAH, TANPA WARNA TAUTAN. Layar-layar ini sudah
   berisi nama emas untuk VIP, tag klan, gelar, keping peringkat,
   dan nama ikan sewarna tier; satu penanda lagi di tiap nama akan
   membuat setiap baris terlihat seperti kumpulan tautan. Yang
   dipakai isyarat yang cuma muncul saat disentuh — dan di layar
   sentuh, foto orang memang tempat yang pertama dicoba orang.

   Sasaran tapnya tidak perlu diperbesar: foto paling kecil 30px
   (chat) sudah di ambang yang bisa ditekan, dan namanya sebaris
   penuh. */
.fh-tapuser{cursor:pointer;-webkit-tap-highlight-color:transparent}

/* Foto mengkerut, nama meredup. Bedanya karena bentuknya beda:
   transform pada teks sebaris menggeser baris di sebelahnya,
   sementara opacity pada foto bulat nyaris tidak terlihat.

   Dipilih lewat kelas fotonya masing-masing, bukan lewat tag: di
   papan skor yang membawa .fh-tapuser adalah <span class="fh-rkav">
   pembungkusnya, di chat <span class="fh-mav"> fotonya sendiri. */
.fh-mav.fh-tapuser:active,
.fh-rkav.fh-tapuser:active{transform:scale(.92)}
.fh-mav.fh-tapuser,
.fh-rkav.fh-tapuser{transition:transform .12s}
b.fh-tapuser:active,
.fh-rknama.fh-tapuser:active{opacity:.6}

/* Fokus papan ketik TETAP TERLIHAT. Tidak satu pun dari mereka
   <button>, jadi cincin fokus bawaan tidak datang sendiri. */
.fh-tapuser:focus-visible{
  outline:2px solid var(--pixel-primary);
  outline-offset:2px;border-radius:var(--r-xs);
}

@media (prefers-reduced-motion:reduce){
  .fh-mav.fh-tapuser,
  .fh-rkav.fh-tapuser{transition:none}
}


/* ============================================================
   BAR PROGRES
   Warna isian diambil dari currentColor: satu properti untuk
   diganti, dan tidak perlu variabel baru per pemakaian.
   ============================================================ */

.fh-bar{
  height:6px;border-radius:2px;overflow:hidden;
  background:var(--pixel-raised);
  color:var(--pixel-primary);
}
.fh-bar--thin{height:4px}
.fh-bar > i{display:block;height:100%;background:currentColor}


/* ============================================================
   TOMBOL UTAMA
   Bayangan padat 3px, bukan blur — sistem visualnya hard edge.
   Saat ditekan tombol turun sejauh bayangannya, jadi terasa
   benar-benar tertekan.
   ============================================================ */

.fh-btn{
  display:block;width:100%;height:52px;
  border:0;border-radius:var(--r-md);
  background:var(--pixel-primary);color:var(--ink-on-primary);
  font-family:var(--font-pixel);font-size:11px;
  box-shadow:0 3px 0 var(--pixel-primary-dim);
}
.fh-btn:active{transform:translateY(3px);box-shadow:none}
.fh-btn:disabled{
  background:var(--pixel-raised);color:var(--pixel-muted);
  box-shadow:none;cursor:not-allowed;transform:none;
}

/* Tombol aksi utama sebuah LAYAR, bukan sebuah overlay: ikut
   mengalir bersama isi, tidak menempel di kaki. Dipakai Home
   ("LANJUT MANCING") dan Tas ("JUAL CEPAT") — kaki layar di
   kedua tempat itu sudah milik bottom nav.

   width:auto, bukan calc(100% - 24px): .fh-btn memasang
   width:100% dan itu akan meluber keluar bersama margin. Perhitungan
   manualnya juga meleset di bawah 360px, tempat --sp-md turun jadi
   10px sementara angka 24 tidak ikut berubah. */
.fh-cta{margin:var(--sp-lg) var(--sp-md) 0;width:auto}

.fh-btn--ghost{
  height:42px;
  background:var(--pixel-raised);color:var(--pixel-text);
  border:1px solid var(--pixel-border);
  font-size:9px;letter-spacing:.5px;box-shadow:none;
}
.fh-btn--ghost:active{transform:scale(.98)}

/* Tombol tutup kotak di pojok overlay. */
.fh-btn-x{
  width:32px;height:32px;flex-shrink:0;
  border:1px solid var(--pixel-border);border-radius:var(--r-sm);
  background:var(--pixel-surface);color:var(--pixel-text);
  font-size:16px;line-height:1;
}


/* ============================================================
   PAGINASI — prev/next + "Hal 2 dari 3".

   Dipakai daftar listing Market dan direktori Clan. Naik ke sini
   begitu pemakainya jadi dua, sesuai aturan IMPLEMENTASI bagian 2:
   dipakai dua layar atau lebih -> components.css, satu layar ->
   berkas layarnya.

   BUKAN deretan nomor halaman, dan itu keputusan lebar sekaligus
   keputusan isi: pada 390px deretan nomor untuk daftar yang bisa
   berisi ratusan baris akan terpotong sendiri, dan melompat ke
   halaman 7 bukan gerakan yang berarti di daftar yang urutannya
   bisa berubah tiap menit.

   Barisnya disembunyikan UTUH saat cuma ada satu halaman — dua
   panah mati mengapit "1 dari 1" adalah tiga elemen yang tidak
   menjawab apa pun. Yang menyembunyikan JS-nya, bukan CSS: dia
   yang tahu berapa halaman yang ada.
   ============================================================ */

.fh-pager{
  display:flex;align-items:center;justify-content:center;
  gap:var(--sp-md);
  padding:10px var(--sp-md) var(--sp-md);
  border-top:1px solid var(--hairline);
}
.fh-pager span{
  font-size:12px;color:var(--pixel-muted);
  font-variant-numeric:tabular-nums;
  /* Lebar dikunci: "Hal 9 dari 12" lebih panjang dari "Hal 1 dari
     2", dan tanpa lantai lebar kedua panah bergeser ke dalam tiap
     kali pemain sampai di halaman dua digit. */
  min-width:96px;text-align:center;
}
.fh-pager__b{
  width:32px;height:32px;border-radius:50%;
  display:grid;place-items:center;
  background:var(--pixel-raised);
  border:1px solid var(--pixel-border);
  color:var(--pixel-text);
}
.fh-pager__b svg{width:5px;height:9px;fill:currentColor;shape-rendering:crispEdges}
.fh-pager__b:active{transform:scale(.92)}
/* Ujung daftar: tombolnya TETAP ADA, hanya mati. Menghilangkannya
   membuat baris ini bergeser kiri-kanan tiap kali pemain sampai di
   halaman pertama atau terakhir. */
.fh-pager__b:disabled{opacity:.3;color:var(--pixel-muted)}


/* ============================================================
   BADGE ANGKA — jumlah iklan tersisa, pesan belum dibaca.
   Ditaruh menonjol keluar dari induknya. Kalau induknya perlu
   overflow:hidden, beri padding + margin negatif seukuran
   tonjolan, jangan buang overflow-nya.
   ============================================================ */

.fh-badge{
  position:absolute;top:-4px;right:-4px;
  min-width:15px;height:15px;padding:0 4px;
  border-radius:8px;
  background:var(--t-myth);color:var(--ink-on-danger);
  font-size:10px;font-weight:600;font-style:normal;
  display:grid;place-items:center;
  border:1px solid var(--pixel-bg);
}


/* ============================================================
   TOAST — kabar berhasil, di tengah layar

   BUKAN lapisan tumpukan overlay: dia tidak menahan apa pun, tidak
   perlu backdrop, dan TIDAK boleh menyalakan tombol back Telegram.
   Menekan back untuk menutup ucapan selamat adalah tombol yang
   dipakai untuk hal yang salah.

   Hanya untuk kabar BAIK. Kegagalan tetap memakai kotak yang
   menetap di dalam layar: pesan yang menghilang sendiri setelah
   2,6 detik boleh dilewatkan, dan alasan sebuah tindakan gagal
   tidak boleh boleh dilewatkan.
   ============================================================ */

.fh-toast{
  position:fixed;left:50%;top:50%;
  z-index:var(--z-toast);
  width:calc(100% - 72px);max-width:260px;
  padding:var(--sp-md) var(--sp-lg);
  text-align:center;
  background:var(--pixel-surface);
  border:1px solid var(--up);
  border-radius:var(--r-xl);
  box-shadow:var(--shadow-sheet);
  transform:translate(-50%,-50%) scale(.94);
  opacity:0;pointer-events:none;
  transition:transform .18s ease-out,opacity .15s;
}
.fh-toast.is-on{
  transform:translate(-50%,-50%) scale(1);
  opacity:1;pointer-events:auto;
}
.fh-toast b{
  display:block;margin-bottom:7px;
  font-family:var(--font-pixel);font-size:10px;
  color:var(--up);
}
.fh-toast span{
  display:block;
  font-size:12px;line-height:1.45;color:var(--pixel-text);
}

@media (prefers-reduced-motion:reduce){
  .fh-toast{transition:opacity .15s}
}


/* ============================================================
   PESAN TERKUNCI
   ============================================================ */

.fh-lock{
  padding:var(--sp-md);border-radius:var(--r-md);
  background:var(--alert-bg);
  border:1px solid var(--alert-border);
  font-size:12px;color:var(--alert-text);
}
.fh-lock b{display:block;color:var(--pixel-text);font-size:12px;margin-bottom:3px}


/* ============================================================
   BOTTOM NAV — ada di setiap layar utama, jadi tinggal di sini
   dan bukan di home.css.

   Tab bar atas DIHAPUS TOTAL. Masalah terbesar UI lama adalah
   dua sistem navigasi sekaligus: tab bar 6 item di atas plus
   bottom nav 5 ikon di bawah. Pemain tidak tahu mana yang utama,
   dan tab atas paling jauh dari jempol serta bertabrakan dengan
   header Telegram.

   Lima tab: Home, Tas, Bestiary, Market, Profil.
   Leaderboard turun ke Profil, Chat naik jadi ikon di header —
   keduanya bukan tujuan harian jadi tidak layak dapat slot nav.

   sticky bottom:0 DI DALAM alur, bukan fixed. Dengan begitu nav
   menempel saat konten panjang dan berhenti di akhir konten saat
   konten pendek.
   ============================================================ */

.fh-nav{
  position:sticky;bottom:0;
  z-index:var(--z-nav);
  flex:none;height:var(--nav-h);
  background:var(--pixel-surface);
  border-top:1px solid var(--pixel-border);
  display:grid;grid-template-columns:repeat(5,1fr);
  padding-bottom:env(safe-area-inset-bottom);
  overflow:visible;            /* ikon tab aktif menyembul ke atas bar */
}

.fh-nav a{
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:1px;position:relative;
  color:var(--pixel-muted);font-size:8px;
}

/* Ikon nav BERWARNA PENUH, satu file per tab per ukuran.
   Non-aktif diredam lewat filter, bukan lewat file kedua. */
.fh-nav__ico{
  width:var(--nav-ico);height:var(--nav-ico);
  transition:filter .15s,opacity .15s;
}
.fh-nav a:not(.is-on) .fh-nav__ico{filter:grayscale(.85) brightness(.75);opacity:.65}

/* Aset besar DIHASILKAN TERPISAH di 58px, tidak di-scale lewat
   CSS: skala 1.45x membuat lebar pixel tidak rata dan tepinya
   kotor. Dua img per tab, CSS memilih mana yang tampil — tidak
   ada JS dan tidak ada kedip saat berpindah tab. */
.fh-nav__ico--lg{display:none;width:var(--nav-ico-on);height:var(--nav-ico-on)}
.fh-nav a.is-on .fh-nav__ico--sm{display:none}
.fh-nav a.is-on .fh-nav__ico--lg{
  display:block;
  margin-top:-30px;margin-bottom:-4px;
  filter:drop-shadow(0 3px 0 var(--drop-nav));
}

/* Cadangan selama aset 58px belum lengkap.

   Ditulis dengan EMPAT kelas + tipe (0,4,1) supaya menang atas
   aturan di atasnya yang (0,3,1) — `.fh-nav a.is-on .fh-nav__ico--sm`
   sudah menyembunyikan ikon kecil, dan modifier satu kelas akan
   kalah tanpa gejala apa pun. Ini persis jebakan spesifisitas yang
   sudah sekali menjatuhkan toggle side rail; lihat IMPLEMENTASI
   bagian 6.

   JS memasang .fh-nav--noart hanya kalau berkas 58px-nya benar
   gagal dimuat, jadi aturan ini mati sendiri begitu asetnya ada. */
.fh-nav a.is-on.fh-nav--noart .fh-nav__ico--lg{display:none}
.fh-nav a.is-on.fh-nav--noart .fh-nav__ico--sm{
  display:block;
  width:var(--nav-ico-on);height:var(--nav-ico-on);
  margin-top:-30px;margin-bottom:-4px;
  filter:drop-shadow(0 3px 0 var(--drop-nav));
}

/* DI ATAS overlay yang menyisip. Dipasang ui.js lewat syncNav(),
   dan hanya selama lapisan paling atas memang jenis itu.

   Angkanya harus melewati seluruh jangkauan tumpukan overlay
   (Z_BASE 60 + 10 per lapisan di ui.js), bukan cuma --z-overlay:
   yang dibandingkan bukan overlay pertama, tapi overlay ke berapa
   pun yang kebetulan sedang paling atas. Tetap DI BAWAH --z-toast,
   karena toast melaporkan hasil tindakan dan harus menang atas
   apa pun. */
.fh-nav--atas{z-index:180}

.fh-nav a.is-on{z-index:2;color:var(--pixel-primary);font-size:10px;font-weight:600;letter-spacing:.2px}

/* Garis penanda di tepi atas bar. */
.fh-nav a.is-on::before{
  content:'';position:absolute;top:0;
  width:26px;height:2px;background:var(--pixel-primary);
}

/* Halo redup di belakang ikon yang menyembul. Dipakai radial
   gradient, bukan blur/glow CSS: sistem visualnya hard edge dan
   blur berat di HP kelas menengah. */
.fh-nav a.is-on::after{
  content:'';position:absolute;top:-18px;left:50%;
  transform:translateX(-50%);
  width:56px;height:56px;border-radius:50%;
  background:radial-gradient(circle,rgba(var(--primary-rgb),.18),transparent 68%);
  pointer-events:none;z-index:-1;
}

@media (prefers-reduced-motion:reduce){
  .fh-nav__ico{transition:none}
}


/* ============================================================
   PENANDA VIP BERBENTUK TEKS
   Melengkapi mahkota, bukan menggantikannya: mahkota terbaca
   sekilas, teks menjawab "ini apa" bagi pemain yang belum hafal
   ikonnya.

   Dipakai juga di layar Profil nanti, jadi tinggal di sini.

   Sengaja isi emas penuh, bukan garis tepi seperti .fh-tier:
   di 7px teks berwarna di atas latar gelap mulai luntur, tinta
   gelap di atas emas tetap tegas.
   ============================================================ */

.fh-vip-tag{
  display:block;
  font-family:var(--font-pixel);
  font-size:7px;line-height:1;letter-spacing:.5px;
  padding:3px 4px;border-radius:var(--r-xs);
  background:var(--pixel-primary);
  color:var(--ink-on-primary);
  white-space:nowrap;
}


/* ============================================================
   AVATAR
   Ukuran lewat --fh-av: Home memakai 40px, Profil 72px.

   Pakai variabel, bukan `.fh-pav .fh-av{width:72px}`. Aturan
   berantai begitu memang menang, tapi tiap layar baru menambah
   satu aturan yang menimpa aturan lain — dan begitu ada dua yang
   berat sama, yang menang cuma ditentukan urutan berkas.

   Pemain tanpa photo_url mendapat lingkaran berisi huruf pertama
   namanya; warnanya tidak perlu aset apa pun.
   ============================================================ */

.fh-av{
  --fh-av:40px;
  width:var(--fh-av);height:var(--fh-av);
  border-radius:50%;
  background:var(--pixel-raised);
  border:1px solid var(--pixel-border);
  display:grid;place-items:center;
  font-size:calc(var(--fh-av) * .33);
  font-weight:600;color:var(--pixel-muted);
  background-size:cover;background-position:center;
  transition:transform .12s;
}

@media (prefers-reduced-motion:reduce){
  .fh-av{transition:none}
}


/* ============================================================
   PAPAN SKOR — segmen kategori + baris peringkat

   Naik ke sini dari bestiary.css begitu Papan Mingguan jadi
   pemakai kedua. Aturan bagian 2 IMPLEMENTASI dipakai apa adanya:
   dipakai dua layar atau lebih -> components.css.

   Yang dibagi cuma KERANGKANYA: segmen kategori, pita sticky di
   puncak daftar, padding daftar, keping peringkat berikut
   medalinya, kaki peringkat-sendiri, dan pesan kosong.

   BENTUK BARISNYA TIDAK dibagi, dan itu keputusan yang berubah
   sekali. Waktu Papan Mingguan masih memakai baris satu-baris yang
   sama, seluruh keluarga .fh-lb* naik ke sini. Begitu Papan
   Mingguan berganti jadi kartu — foto profil, poin di bawah nama,
   ilustrasi karakter yang menyembul — kedua layar tidak lagi
   berbagi apa pun selain kerangka di atas, jadi bentuk barisnya
   TURUN LAGI ke berkas layarnya masing-masing:
     bestiary.css  .fh-lbrow / .fh-lbnama / .fh-lbnilai (baris padat)
     rank.css      .fh-rkrow dan seisinya (kartu)

   Yang TETAP di sini: .fh-lbrank. Dua layar sama-sama menggambar
   peringkat dengan warna medali, dan dua salinan pemetaan
   emas/perak/perunggu berarti juara satu bisa tampil dengan dua
   warna berbeda di dua papan begitu salah satunya disetel.

   ⚠ .fh-lbtop berlatar --pixel-surface karena lahir di dalam
   bottom sheet. Di dalam overlay penuh layar latarnya --pixel-bg,
   dan rank.css yang menimpanya. Kalau nanti ada pemakai ketiga,
   jadikan variabel alih-alih menambah timpaan kedua.
   ============================================================ */


/* Segmen kategori MENEMPEL di puncak daftar. Daftarnya sampai 50
   baris, dan tanpa sticky pemain yang sudah di peringkat 30 harus
   menggulung balik ke atas hanya untuk berpindah kategori.

   Pembungkusnya yang berlatar, bukan segmennya: segmen punya
   margin, dan baris yang lewat di celah margin saat digulung
   terlihat seperti bocor. */
.fh-lbtop{
  position:sticky;top:0;z-index:2;
  padding:var(--sp-md) var(--sp-md) var(--sp-sm);
  background:var(--pixel-surface);
}

/* ---------- SEGMEN KATEGORI ----------
   Semua pilihan selalu terlihat. Bentuk sakelar (satu tombol yang
   berganti label) gugur karena alasan lama: sakelar hanya
   menampilkan keadaan sekarang, dan pemain harus menekannya untuk
   tahu ada pilihan lain sama sekali.

   Sempat naik ke components.css waktu Market ikut memakainya, lalu
   turun lagi ke sini begitu tab Market berganti jadi tab menyatu
   ala Tas. Aturannya di IMPLEMENTASI bagian 2: dipakai dua layar
   atau lebih -> components.css, satu layar -> berkas layarnya.
   Turun-naik seperti ini memang biayanya, dan itu lebih murah
   daripada components.css yang perlahan jadi tempat penampungan.

   Bedanya dengan tab menyatu (.fh-bagcat): segmen dipakai saat yang
   dipilih adalah CARA MELIHAT daftar yang sama — dua kategori papan
   skor untuk ikan yang sama, atau tiga saringan atas satu deret
   pesan — bukan isi yang berbeda-beda di satu wadah.

   JUMLAH SEGMENNYA BEBAS. Namanya sempat "SEGMEN DUA KATEGORI"
   waktu cuma papan skor yang memakainya; chat memakai tiga, dan
   flex:1 pada tombolnya sudah membagi rata berapa pun jumlahnya.
   Yang membatasi cuma lebar layar: di 390px, empat label masih
   muat kalau pendek, lima tidak. */
.fh-seg{
  display:flex;gap:4px;
  padding:3px;
  background:var(--pixel-bg);
  border:1px solid var(--pixel-border);
  border-radius:var(--r-md);
}
.fh-seg__b{
  flex:1;min-width:0;height:30px;
  display:flex;align-items:center;justify-content:center;
  padding:0 6px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  font-size:12px;font-weight:600;
  color:var(--pixel-muted);
  background:none;
  border-radius:var(--r-sm);
}
/* Yang aktif naik satu tingkat permukaan, bukan diberi warna aksi.
   Emas dipesan untuk AKSI (aturan di HANDOFF); pilihan yang sedang
   menyala itu keadaan, bukan ajakan menekan. */
.fh-seg__b.is-on{
  color:var(--pixel-text);
  background:var(--pixel-raised);
  box-shadow:inset 0 0 0 1px var(--pixel-border);
}

.fh-lblist{padding:0 var(--sp-md) var(--sp-md)}

/* KOLOM PERINGKAT berlebar TETAP, dan itu yang menjaga kolom nama
   lurus dari "1" sampai "50". Font pixel lebarnya persis 1em per
   karakter, jadi dua digit di 10px = 20px; 24px memberi ruang
   untuk medali tanpa membuat kolomnya menganga. */
.fh-lbrank{
  flex:none;
  width:24px;height:24px;
  display:grid;place-items:center;
  font-family:var(--font-pixel);font-size:10px;
  color:var(--pixel-muted);
}

/* TIGA TERATAS DAPAT MEDALI, dan tiga warna berbeda — bukan emas
   untuk ketiganya. Emas/perak/perunggu adalah arti yang dibawa
   pemain dari luar app dan tidak perlu dijelaskan; tiga bidang
   emas yang sama justru menghapus urutan di antara mereka, padahal
   di papan skor jarak antara juara satu dan tiga adalah seluruh
   ceritanya.

   Bidangnya diisi, bukan cuma hurufnya diwarnai: pada 10px font
   pixel, huruf berwarna di atas panel gelap terlalu tipis untuk
   terbaca sebagai penanda — dan perunggu, yang paling gelap dari
   ketiganya, akan jadi yang paling sulit dilihat justru karena
   dia peringkat tiga. Huruf gelap di atas bidang terang: aturan
   yang sama dengan badge tier. */
.fh-lbrank.is-medal{
  border-radius:50%;
  color:var(--ink-on-tier);
  background:var(--m,var(--medal-1));
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.22);
}
.fh-lbrank.m1{--m:var(--medal-1)}
.fh-lbrank.m2{--m:var(--medal-2)}
.fh-lbrank.m3{--m:var(--medal-3)}

/* KAKI PERINGKAT SENDIRI. Tidak menggulung bersama daftar: dia
   harus tetap terlihat justru saat pemain menggulung mencari
   dirinya di antara 50 nama. */
.fh-lbme{padding-top:10px;padding-bottom:calc(10px + env(safe-area-inset-bottom))}

/* Satu baris keterangan di atas barisnya. Kiri menyebut apa ini,
   kanan menyebut seberapa kurang — dan yang kanan itu satu-satunya
   angka di layar ini yang bisa ditindaklanjuti. */
.fh-lbme__hd{
  display:flex;align-items:baseline;gap:var(--sp-sm);
  margin-bottom:6px;
  font-size:11px;color:var(--pixel-muted);
}
.fh-lbme__gap{
  margin-left:auto;
  color:var(--pixel-primary);opacity:.85;
  text-align:right;
}

.fh-lbkabar{
  padding:var(--sp-lg) var(--sp-md);
  text-align:center;
  font-size:12px;color:var(--pixel-muted);
}
