/*
 * FRUITCITY member app.
 *
 * SELURUH WARNA APP INI DIAMBIL DARI LOGO FRUITCITY, dan itu satu-satunya
 * sumbernya. Logo itu enam warna - merah, pink, kuning, hijau, oranye, ungu -
 * dan nilainya dibaca langsung dari berkas logonya, bukan dikira-kira.
 *
 * Sistemnya dua lapis, dan keduanya beda pekerjaan:
 *
 *  - HIJAU (#1c7a2d, dipekatkan dari hijau logo #45b653) memikul semua yang
 *    bisa ditekan: tombol, tautan, tab aktif, isian batang kemajuan. Satu warna
 *    berarti "ini bisa ditekan". Hijau yang dipilih karena hijaulah warna
 *    terbanyak di logo itu sendiri - seluruh tagline dan huruf T.
 *  - ENAM WARNA LOGO memikul kepribadian: pelat ikon tiap bagian, lencana tier,
 *    bola hiasan, tanda app.
 *
 * Dua mode, dipilih member lewat tab Akun dan ditulis ke <html data-theme> oleh
 * theme.js sebelum halaman digambar. Yang membedakan keduanya cuma NETRALNYA -
 * kanvas, permukaan, tinta - sementara warna merek tetap warna merek:
 *
 *  - TERANG itu putih bersih. Latarnya diam supaya warna logo di atasnya
 *    terlihat; kanvas berwarna justru MENGURANGI warna app, karena tiap pelat
 *    berwarna kehilangan kontras terhadap latar yang sewarna dengannya.
 *  - GELAP tetap gelap, dan hijaunya dinaikkan ke #56c96a - hijau tombol yang
 *    benar di atas putih cuma 1.5:1 di atas hitam.
 *
 * Semua yang berbeda antara keduanya adalah CUSTOM PROPERTY, bukan aturan
 * kembar. Kalau sebuah komponen butuh warna berbeda per mode, tokennya yang
 * ditambah - bukan satu blok `[data-theme='dark']` lagi di bawah sana.
 *
 * Hal yang sama berlaku untuk UKURAN HURUF, BOBOT, JARAK dan RADIUS: semuanya
 * token berskala di `:root`. Angka telanjang di dalam aturan komponen -
 * `font-size: 14.5px`, `gap: 9px` - adalah bagaimana app ini dulu sampai punya
 * 22 ukuran huruf dan 12 nilai jarak yang tidak seorang pun memutuskannya.
 *
 * Poppins is the website's typeface, self-hosted here so the CSP needs no CDN
 * and the app keeps its typography offline.
 */

/*
 * ANGKA TELANJANG DI SINI, BUKAN `var(--fw-*)`. JANGAN DIRAPIKAN.
 *
 * `font-weight` di dalam `@font-face` bukan properti, ia DESKRIPTOR - dan
 * deskriptor tidak mengenal properti kustom. `font-weight: var(--fw-regular)`
 * di sini tidak error, tidak memerahkan apa pun di devtools: ia cuma tidak sah,
 * lalu diam-diam jatuh ke `normal`.
 *
 * Akibatnya keempat potongan ini mengaku sebagai bobot 400 yang sama. Peramban
 * memilih satu untuk semua bobot, dan untuk 600/700 ia MENEBALKAN SENDIRI
 * potongan itu secara sintetis. Hasilnya seluruh app terbaca tebal - termasuk
 * keterangan 400 yang seharusnya paling ringan - dan `document.fonts.check(
 * '400 12px Poppins')` menjawab `false` untuk keempatnya.
 *
 * Gejalanya persis seperti "font-weight-nya kebanyakan tebal", jadi mudah
 * salah didiagnosis sebagai keputusan desain. Cara memastikannya satu baris:
 * `[...document.fonts].map(f => f.weight)` - kalau semuanya `normal`, ini
 * penyebabnya.
 */
@font-face {
  font-family: 'Poppins';
  src: url('fonts/poppins-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Poppins';
  src: url('fonts/poppins-500.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Poppins';
  src: url('fonts/poppins-600.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Poppins';
  src: url('fonts/poppins-700.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  /*
   * ---- Warna aksi: HIJAU LOGO ----
   *
   * Dulu keluarga ini biru #007cff, warna utama fruitcity.id. Biru itu dilepas
   * karena ia tidak ada di logo FRUITCITY sama sekali: enam huruf logonya merah,
   * pink, kuning, hijau, oranye dan ungu, dan satu-satunya biru di keluarga
   * merek ini hidup di lockup FRUITCITY PARK.
   *
   * Yang dipilih hijau, karena hijaulah warna yang paling banyak dipakai logo
   * itu sendiri - seluruh tagline "CREATIVITY FOR FUN" dan huruf T.
   *
   * Tangganya BUKAN satu warna yang dimuda-tuakan sembarangan. Tiap anak tangga
   * punya pekerjaan, dan pekerjaan itulah yang menentukan seberapa gelap:
   *
   *   --brand-600  TINTA aksi: teks tautan, ikon, tab aktif. 5.4:1 di atas
   *                putih. Ia BERBALIK di mode gelap.
   *   --brand-700  tinta aksi yang lebih gelap. Ikut berbalik.
   *   --brand-500  hijau isian yang lebih hidup, HANYA untuk yang tidak
   *                ditulisi: ujung gradien, batang kemajuan, cincin fokus.
   *                2.6:1 di atas putih - sebagai teks ia tidak pernah boleh.
   *
   * Hijau logo apa adanya tidak punya anak tangga di sini - ia sudah hidup
   * sebagai `--c-green` di palet logo di bawah, dan dua nama untuk satu warna
   * adalah dua nama yang cepat atau lambat akan berbeda nilainya.
   *
   * DUA KELUARGA, dan bedanya menentukan: yang dipakai sebagai TINTA harus
   * berbalik saat modenya berbalik, yang dipakai sebagai ISIAN tidak boleh.
   *
   * Tombol hijau adalah isian dengan teks putih di atasnya. Kalau isian itu
   * ikut berbalik jadi hijau muda di mode gelap, teks putihnya jatuh ke 2.1:1 -
   * tombol paling penting di seluruh app berubah jadi tulisan yang tidak bisa
   * dibaca, tepat di layar yang dipakai orang di ruangan gelap. Jadi isian
   * merek punya tokennya sendiri, dan token itu SENGAJA tidak muncul di blok
   * `[data-theme='dark']` di bawah.
   */
  --brand-700: #14672a;
  --brand-600: #1c7a2d;
  --brand-500: #2e9e48;
  --brand-050: #e9f6ec;
  /* Isian merek. Sama di kedua mode, selalu berteks putih.
     `--brand-solid` 5.4:1 dengan putih di atasnya, dan 3.4:1 terhadap kanvas
     gelap - cukup untuk terbaca sebagai benda, cukup gelap untuk ditulisi. */
  --brand-solid: #1c7a2d;
  --brand-deep: #14672a;
  /*
   * Permukaan tombol TERPILIH di kontrol segmen. Di mode terang ia `--surface`
   * yang putih di atas alur `--surface-2` yang abu - terpilih berarti naik.
   *
   * Di mode gelap urutan itu TERBALIK sendiri: `--surface` lebih gelap daripada
   * `--surface-2`, jadi tombol yang sedang dipilih justru jadi lubang paling
   * gelap di kontrolnya. Naik dan turun tidak boleh bertukar arti hanya karena
   * modenya berganti, jadi mode gelap memberinya permukaannya sendiri.
   */
  --seg-active: var(--surface);
  /*
   * Cincin fokus, dan ia token TERSENDIRI - bukan anak tangga hijau yang
   * kebetulan dipakai. Cincin fokus punya syarat yang tidak dimiliki warna lain
   * di app ini: ia harus terbaca 3:1 terhadap APA PUN yang kebetulan ada di
   * belakangnya - putih, kanvas, kartu, dan tombol hijau itu sendiri.
   *
   * Hijau muda gagal syarat itu (#86d193 cuma 1.9:1 di atas putih), dan hijau
   * tombol gagal di atas tombolnya sendiri. Yang dipakai anak tangga tengah.
   */
  --focus: #2e9e48;

  /* ---- Brand: logo palette, read from the logo's own PLTE chunk ---- */
  --c-green: #45b653;
  --c-yellow: #ffdd2f;
  --c-orange: #f79633;
  --c-red: #ed3237;
  --c-pink: #da3263;
  --c-purple: #834b9b;

  /*
   * ---- SKALA HURUF ----
   *
   * Sembilan anak tangga, dan itu jumlah yang sengaja dibatasi. Sebelum ini app
   * memakai 22 ukuran berbeda - 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15,
   * 15.5, 16 - yang selisihnya setengah piksel dan tidak terbaca sebagai
   * perbedaan apa pun, hanya sebagai ketidakrapian. Dua teks yang berbeda
   * PERAN harus berbeda ukuran; dua teks dengan peran sama harus sama persis.
   *
   * Dua nilai di bawah ini tidak boleh diubah tanpa alasan yang kuat:
   *   --fs-badge 11px  lantai app ini, dan HANYA untuk lencana huruf besar
   *                    tebal. Teks yang dibaca tidak pernah turun ke sini.
   *   --fs-lg    16px  lantai iOS Safari: isian di bawah 16px membuat halaman
   *                    ikut membesar sendiri saat disentuh.
   */
  --fs-badge: 11px;
  --fs-caption: 12px;
  --fs-sm: 13px;
  --fs-base: 14px;
  --fs-md: 15px;
  --fs-lg: 16px;
  --fs-xl: 20px;
  --fs-2xl: 24px;
  --fs-3xl: 30px;
  /* Kode voucher yang dibacakan melintasi meja kasir. Satu-satunya ukuran di
     luar tangga, karena pekerjaannya juga di luar semua yang lain. */
  --fs-code: 44px;

  /*
   * ---- BOBOT HURUF ----
   *
   * Empat, dan tidak lebih. Yang dibuang `650`: ia dipakai empat kali dan tidak
   * pernah terbaca berbeda dari 600 di layar mana pun - Poppins tidak punya
   * potongan 650, jadi mesin menebalkannya sendiri dan hasilnya sedikit
   * berbeda di tiap ponsel.
   *
   * TIAP ANAK TANGGA PUNYA PEKERJAANNYA, dan daftar ini yang menentukan, bukan
   * selera per komponen:
   *
   *   400  teks yang DIBACA - kalimat, keterangan, deskripsi, isi artikel
   *   500  label, nilai sekunder, tab tidak aktif, tombol tenang
   *   600  judul kartu & baris, tombol, tautan, lencana
   *   700  judul layar, judul bagian, dan ANGKA yang dicari orang
   *
   * Tangga 500 dulu praktis kosong - lima pemakai dari tujuh puluh empat - jadi
   * semua yang bukan kalimat melompat dari 400 langsung ke 600, dan seluruh app
   * terbaca tebal. Yang hilang bukan cuma kelembutannya: kalau semua tebal,
   * tidak ada yang tebal, dan angka poin kehilangan satu-satunya cara ia
   * menonjol dari label di sebelahnya.
   *
   * 700 itu JATAH, bukan penekanan yang boleh dipakai siapa saja. Sebelum
   * menambahkannya ke komponen baru, tanya: apakah ini judul layar, judul
   * bagian, atau angka? Kalau bukan ketiganya, jawabannya 600.
   */
  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;

  /*
   * ---- JARAK ----
   *
   * Kelipatan 4. Sebelum ini jaraknya 2, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 16
   * - dua belas nilai untuk pekerjaan yang sebenarnya cuma lima.
   */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-8: 32px;

  /*
   * ---- UKURAN PELAT IKON ----
   *
   * Pelat ikon dulu punya sepuluh ukuran (26, 34, 36, 38, 40, 42, 46, 52, 54,
   * 62) dan selisih 2px di antaranya tidak pernah disengaja siapa pun. Empat
   * sudah cukup: penanda judul, isi kartu, baris daftar, dan medali.
   */
  --plate-xs: 28px;
  --plate-sm: 36px;
  --plate-md: 40px;
  --plate-lg: 48px;

  /*
   * ---- PANAH KEMBALI, DAN RUANG YANG IA MINTA ----
   *
   * Cakram mengambang di pojok kiri atas tiap layar dorong. Ia `fixed`, jadi ia
   * tidak mendorong apa pun - dan justru itu masalahnya: judul layar duduk
   * sebaris dengannya dan harus menghindar SENDIRI.
   *
   * Tiga aturan harus setuju soal seberapa jauh menghindarnya - tombolnya,
   * judulnya, dan keterangan di bawah judul - jadi angkanya DITURUNKAN, bukan
   * diketik tiga kali. Tombol yang suatu hari dibesarkan 4px menggeser ketiganya
   * sekaligus; sebelumnya ia akan menabrak judul di sepuluh layar dan tidak
   * seorang pun tahu di mana angka 52 itu ditulis.
   *
   * `- var(--sp-5)` di ujung karena `--title-inset` dipakai sebagai `padding`
   * DI DALAM `.hero`, yang sudah punya jarak tepinya sendiri sebesar itu.
   */
  --back-x: 14px;
  --back-size: 42px;
  --title-inset: calc(var(--back-x) + var(--back-size) + var(--sp-4) - var(--sp-5));

  /*
   * ---- MODE TERANG: putih bersih ----
   *
   * Kanvasnya dulu pastel ungu dengan sapuan pink ke biru langit, dengan alasan
   * bahwa merek taman bermain anak tidak pantas berlatar abu-abu kantor. Alasan
   * itu benar; obatnya yang salah. Latar berwarna tidak menambah warna pada app
   * - ia MENGURANGI, karena setiap warna di atasnya kehilangan kontras terhadap
   * latarnya sendiri. Pelat ikon pink di atas kanvas pink adalah pelat yang
   * hilang.
   *
   * Jadi sekarang latarnya diam dan warnanya datang dari ISI: pelat ikon per
   * bagian, tinta tier di kartu member, lencana, dan aksi yang hijau. Enam
   * warna logo justru terlihat lebih cerah di atas latar yang tidak ikut
   * bersuara - dan diam itu pula yang membuat app ini terbaca sebagai barang
   * mahal, bukan sebagai brosur.
   *
   * Tintanya abu-abu kebiruan netral, bukan ungu: tinta berwarna hanya masuk
   * akal kalau latarnya juga berwarna.
   */
  --ink: #14181f;
  --ink-2: #454c58;
  /* 4.8:1 di atas `--surface`, 4.5:1 di atas `--canvas` - lolos di keduanya.
     Nilai lama (#6f6784) hanya diuji di atas putih. */
  --muted: #6b7280;
  --line: #e8eaee;
  --surface: #ffffff;
  --surface-2: #f2f4f7;
  --canvas: #f7f8fa;
  /*
   * Tidak ada sapuan warna lagi, di kedua mode. Ia masih token - bukan aturan
   * yang dihapus - karena `.sheet`, `.boot` dan `.detail-view` melukis kanvas
   * yang sama dan semuanya membacanya; mengosongkan satu nilai di sini lebih
   * aman daripada mencabut empat baris `background-image` yang tersebar.
   */
  --canvas-wash: none;

  /*
   * Header layar dalam. Ia PUTIH, setinggi judulnya, dengan lembar isi
   * berkanvas yang naik menutupi tepi bawahnya - bukan pita berwarna lagi.
   *
   * Pita berwarna adalah cara termudah membuat app terlihat ramai: ia muncul di
   * SETIAP layar, selalu setinggi dua baris teks, dan tidak pernah mengatakan
   * apa pun yang tidak sudah tertulis di judulnya. Yang membuatnya tetap hidup
   * sekarang satu bola warna logo yang sangat samar di sudutnya.
   */
  --hero-grad: linear-gradient(180deg, var(--surface) 0%, var(--surface) 100%);
  --hero-ink: var(--ink);
  /*
   * Layar MASUK dapat perlakuan sebaliknya, dan itu satu-satunya pengecualian:
   * di sana tidak ada isi yang perlu dibaca di belakang warna, dan kesan
   * pertama sebuah app terjadi tepat sekali. Hijau pekat merek, tinta putih.
   */
  --auth-grad: linear-gradient(155deg, #3aa851 0%, var(--brand-600) 52%, var(--brand-700) 100%);
  --auth-ink: #ffffff;

  /*
   * Tombol bulat yang melayang di atas poster atau header. Dulu ia putih mati
   * dengan ikon `--ink` - di mode gelap itu berarti panah nyaris putih di atas
   * lingkaran putih, jadi tombol kembalinya ADA tapi tidak terlihat sama
   * sekali. Sekarang cakramnya ikut tema, panahnya selalu `--ink`.
   */
  --overlay-bg: rgba(255, 255, 255, 0.92);
  --overlay-line: rgba(20, 24, 31, 0.1);

  /* Merah logo dipekatkan sampai lolos sebagai teks di atas putih (5.2:1).
     Merah logo apa adanya #ed3237 hanya 4.0:1 - cukup untuk pelat, kurang
     untuk kalimat kesalahan yang justru wajib terbaca. */
  --danger: #cf2b2f;

  /*
   * Radius. Empat, dan semuanya kelipatan yang jelas berbeda - kotak 10px dan
   * kotak 12px bersebelahan terbaca sebagai salah satunya lupa disetel.
   */
  --r-sm: 10px;
  --r-md: 14px;
  --r-lg: 20px;
  --r-xl: 28px;
  --r-full: 999px;

  /* Two shadows only: one for things resting on the page, one for the wallet
     card that floats over the hero. More than that reads as noise.
     Warnanya abu-abu kebiruan netral sekarang, mengikuti kanvasnya: bayangan
     ungu di atas kanvas putih terbaca sebagai noda, bukan sebagai ketinggian. */
  --lift: 0 1px 2px rgba(16, 24, 40, 0.04), 0 6px 18px rgba(16, 24, 40, 0.07);
  --lift-hi: 0 2px 6px rgba(16, 24, 40, 0.07), 0 16px 36px rgba(16, 24, 40, 0.13);
  /* Kartu member, dan HANYA kartu member. `--lift-hi` tidak dinaikkan karena ia
     dipakai berdua dengan bilah kasir; menaikkannya di sana berarti menaikkan
     benda yang sudah menempel penuh ke tepi layar. */
  --lift-card: 0 1px 1px rgba(16, 24, 40, 0.04), 0 4px 10px rgba(16, 24, 40, 0.08),
    0 22px 46px rgba(16, 24, 40, 0.17);

  /*
   * TIMBUL - dua garis, bukan satu bayangan.
   *
   * Sebuah benda terbaca menonjol karena cahaya jatuh dari ATAS: bibir atasnya
   * menangkap cahaya, bibir bawahnya jatuh ke bayangan. Bayangan luar saja
   * hanya mengangkat benda datar; sepasang garis inilah yang membuat
   * permukaannya sendiri terasa melengkung.
   *
   * Cahayanya PUTIH di kedua mode dan bukan `--surface`: sumber cahaya tidak
   * ikut berbalik saat lampu ruangan dimatikan, cuma jadi lebih lemah. Karena
   * itu yang berubah di mode gelap hanya alfanya - 0,60 turun ke 0,16 - dan
   * bayangannya justru naik, karena benda gelap yang timbul dikenali dari
   * bayangnya, bukan dari kilaunya.
   */
  --emboss-top: rgba(255, 255, 255, 0.6);
  --emboss-bottom: rgba(16, 24, 40, 0.11);
  --emboss-lift: 0 1px 1px rgba(16, 24, 40, 0.05), 0 3px 8px rgba(16, 24, 40, 0.14);

  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --tabbar-h: 62px;
  /*
   * Bilah tier di bawah kartu. Dulu ia GELAP di kedua mode; di beranda mode
   * terang itu membuatnya jadi satu-satunya benda gelap di layar pastel -
   * bukan sorotan, melainkan lubang. Sekarang ia ikut temanya.
   *
   * Yang membuatnya tetap terbaca sebagai tombol bukan lagi kegelapannya,
   * melainkan tiga hal yang tidak dimiliki kanvas maupun kartu di atasnya:
   * warnanya (kartu putih, bilah bertinta biru-lilac), garis tepinya, dan
   * bayangannya. Tintanya sekarang tinta tema biasa - `--ink`/`--muted` sudah
   * berbalik sendiri per mode, jadi tidak ada set tinta kedua untuk dirawat.
   */
  /*
   * Alas bilah tier. Hanya ALASNYA - tintanya warna tier, dan tinta itu
   * dicampurkan DI `.wallet-goal`, bukan di sini.
   *
   * Perbedaan itu bukan gaya penulisan, ia keharusan: nilai sebuah properti
   * kustom disubstitusi pada elemen tempat ia DIDEKLARASIKAN, bukan pada elemen
   * yang memakainya. `var(--tier-solid)` yang ditulis di dalam `:root` dihitung
   * di `:root` - tempat `--tier-solid` belum ada - jadi yang menang cadangannya,
   * dan hasil hijau itulah yang lalu diwariskan ke seluruh pohon. Kartu Silver
   * pun mendapat bilah hijau, diam-diam, tanpa satu pun pesan kesalahan.
   */
  --strip-base: var(--surface);
  --strip-ink: var(--ink);
  /* `--ink-2`, bukan `--muted`. Bilah ini bukan kartu putih: di atas permukaan
     bertinta, abu-abu redup jatuh ke 3.9:1 - lolos di layar besar, hilang
     di bawah matahari. */
  --strip-ink-2: var(--ink-2);
  --strip-line: rgba(20, 24, 31, 0.09);
  /* Alur batang kemajuan DI DALAM bilah, dan ia putih bersih di mode terang
     dengan sengaja: isian batang memakai warna tier, dan warna tier apa pun
     butuh alas netral - batang biru di atas alur biru muda hilang. */
  --strip-track: #ffffff;
  /*
   * The only vertical rhythm on a scrolling screen that matters: the air
   * BETWEEN sections has to be clearly larger than the air WITHIN one, or the
   * eye cannot tell where a section ends. Within: `--sp-3` (12px, jarak antar
   * kartu di dalam satu daftar). Between: this - lebih dari dua kali lipatnya,
   * dan itu selisih yang cukup untuk dilihat tanpa dihitung.
   */
  --gap-section: var(--sp-8);
}

/*
 * ---- MODE GELAP ----
 *
 * Dipilih oleh atribut, bukan oleh `@media (prefers-color-scheme)`. Itu bedanya
 * "app ini gelap kalau ponselnya gelap" dengan "app ini gelap kalau SAYA
 * memintanya": theme.js menulis `data-theme` ke <html> sebelum halaman
 * digambar - dari pilihan tersimpan kalau ada, dari setelan ponsel kalau tidak.
 *
 * Konsekuensinya satu, dan sengaja: tanpa theme.js app selalu terang. Itu
 * bukan kerugian - tanpa app.js ia tidak menampilkan apa pun juga.
 */
[data-theme='dark'] {
  --ink: #eef1f5;
  --ink-2: #c2c8d2;
  --muted: #8f97a3;
  --line: #262d38;
  --surface: #161b22;
  --surface-2: #1d232c;
  --canvas: #0d1117;
  --canvas-wash: none;
  /*
   * Hijau tombol #1c7a2d hanya 1.5:1 di atas kanvas hitam - di mode gelap ia
   * praktis lubang hitam berbentuk tombol. Jadi tangganya DIBALIK di sini:
   * yang jadi warna aksi adalah anak tangga yang terang.
   *
   * Yang TIDAK dibalik `--brand-500`: ia dipakai sebagai ujung gradien dan
   * isian batang, dan warna isian adalah warna merek - ia sama di kedua mode.
   */
  --brand-600: #56c96a;
  --brand-700: #3fb356;
  --brand-050: #16301d;
  --seg-active: #2b3441;
  /* Di atas kanvas hitam, hijau tengah jatuh ke 2.1:1 - cincin fokus yang tidak
     terlihat sama saja dengan tidak ada. Dinaikkan sampai ia terbaca terhadap
     kanvas MAUPUN terhadap tombol hijau yang ia lingkari. */
  --focus: #7fd991;
  --danger: #ff8079;
  /*
   * Header di mode gelap juga bukan pita berwarna - ia permukaan kartu, satu
   * tingkat lebih terang daripada kanvas. Itu yang membuat judul layar terbaca
   * sebagai bidang tersendiri tanpa perlu warna sama sekali.
   */
  --hero-grad: linear-gradient(180deg, var(--surface) 0%, var(--surface) 100%);
  --hero-ink: var(--ink);
  --auth-grad: linear-gradient(155deg, #1f8a37 0%, #14672a 52%, #0d4a1d 100%);
  --auth-ink: #ffffff;
  --overlay-bg: rgba(22, 27, 34, 0.88);
  --overlay-line: rgba(255, 255, 255, 0.16);
  /* Di sini bilah tier memang gelap - bukan karena ia bilah tier, karena
     seluruh appnya gelap. Ia sedikit lebih terang daripada permukaan kartu:
     di atas kanvas #0d1117, abu yang sama akan menempel jadi satu dengan
     latarnya. */
  --strip-base: #232b37;
  --strip-line: rgba(255, 255, 255, 0.07);
  --strip-track: rgba(255, 255, 255, 0.16);
  --lift: 0 1px 2px rgba(0, 0, 0, 0.4), 0 6px 20px rgba(0, 0, 0, 0.3);
  --lift-hi: 0 2px 6px rgba(0, 0, 0, 0.5), 0 18px 40px rgba(0, 0, 0, 0.45);
  --lift-card: 0 1px 2px rgba(0, 0, 0, 0.5), 0 5px 14px rgba(0, 0, 0, 0.42),
    0 24px 50px rgba(0, 0, 0, 0.5);
  /* Kilaunya melemah, bayangannya menguat - lihat alasannya di `:root`. */
  --emboss-top: rgba(255, 255, 255, 0.16);
  --emboss-bottom: rgba(0, 0, 0, 0.38);
  --emboss-lift: 0 1px 2px rgba(0, 0, 0, 0.45), 0 4px 12px rgba(0, 0, 0, 0.42);
}

* {
  box-sizing: border-box;
}
html,
body {
  margin: 0;
  padding: 0;
}

body {
  background: var(--canvas);
  /* Urutan ini penting: `background` adalah shorthand dan menghapus
     background-image, jadi sapuannya harus datang SESUDAH warna datarnya. Di
     mode gelap `--canvas-wash` bernilai `none` dan yang tersisa warna datar. */
  background-image: var(--canvas-wash);
  color: var(--ink);
  font-family: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  font-size: var(--fs-md);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: contain;
}

.hidden {
  display: none !important;
}

button {
  font-family: inherit;
}

/* ---------------- Decorative blobs ---------------- */
/*
 * Soft colour shapes behind the hero. They are what stops a flat brand-colour
 * band from looking like a bank header - and they use the logo's own colours, so
 * the personality comes from the brand rather than from decoration for its own
 * sake.
 *
 * Sekarang header layar dalam PUTIH, bukan pita berwarna lagi, jadi bola-bola
 * inilah satu-satunya warna di sana - dan justru karena itu ia harus lebih
 * tipis, bukan lebih tebal. Bola kuning 50% di atas pita pastel cuma menambah
 * satu lapis pastel lagi; bola yang sama di atas putih adalah noda kuning
 * selebar layar. Yang dicari kesan bahwa cahayanya berwarna, bukan bahwa
 * kertasnya berwarna.
 *
 * Di layar MASUK ia tetap setebal aslinya: di sana latarnya hijau pekat, dan
 * bola setipis ini hilang sama sekali.
 */
.blob {
  position: absolute;
  border-radius: 50%;
  /* Blurred far past its own radius on purpose: at a small blur these read as
     hard circles sitting ON the header. Softened this much they become light
     inside it, which is the difference between decoration and atmosphere. */
  filter: blur(45px);
  opacity: 0.16;
}
/*
 * Di layar masuk latarnya hijau PEKAT, dan di atas warna pekat bola berwarna
 * tidak menambah warna - ia mengotori. Kuning di atas hijau tua jadi zaitun,
 * merah muda jadi cokelat lumpur, dan sudut kiri atas layar pertama yang
 * dilihat orang jadi noda.
 *
 * `screen` membuat warnanya MENERANGI alih-alih menutupi: kuning di atas hijau
 * jadi hijau kekuningan yang bersih, merah muda jadi merah muda yang menyala.
 * Blending-nya terkurung di `.auth-sky` sendiri karena `overflow: hidden` di
 * sana sudah membentuk konteks penumpukan - ia tidak akan menembus ke lembar
 * isian di bawahnya.
 */
.auth-sky .blob {
  mix-blend-mode: screen;
  opacity: 0.5;
}
.blob-a {
  width: 210px;
  height: 210px;
  background: var(--c-yellow);
  top: -80px;
  right: -60px;
}
.blob-b {
  width: 180px;
  height: 180px;
  background: var(--c-pink);
  top: 10px;
  left: -80px;
}
.blob-c {
  width: 170px;
  height: 170px;
  background: var(--c-green);
  bottom: -80px;
  right: 20px;
}

/* ---------------- Boot ---------------- */

.boot {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--canvas);
  background-image: var(--canvas-wash);
  z-index: 60;
}
.boot-mark svg,
.brand-mark svg {
  width: 56px;
  height: 56px;
}
.boot-mark {
  animation: bob 1.3s var(--ease) infinite;
}
@keyframes bob {
  0%,
  100% {
    transform: translateY(0) scale(1);
  }
  50% {
    transform: translateY(-8px) scale(1.04);
  }
}
@media (prefers-reduced-motion: reduce) {
  .boot-mark {
    animation: none;
  }
}

/* ---------------- Offline ---------------- */

.offline-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  padding: var(--sp-2) var(--sp-4) calc(var(--sp-2) + env(safe-area-inset-top));
  background: var(--c-orange);
  color: #3a2400;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  text-align: center;
}

/* ---------------- Masuk ---------------- */

.view-auth {
  position: relative;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  overflow: hidden;
}
.auth-sky {
  position: absolute;
  inset: 0 0 auto;
  height: 62vh;
  background: var(--auth-grad);
  overflow: hidden;
}
.auth-card {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 460px;
  margin: 0 auto;
}
/* `--auth-ink`, bukan `--hero-ink`. Keduanya dulu satu nilai karena header dan
   layar masuk sama-sama pita pastel; sekarang header putih bertinta gelap dan
   layar masuk hijau pekat bertinta putih - dua pekerjaan, dua token. */
.brand {
  text-align: center;
  padding: 12vh var(--sp-6) var(--sp-8);
  color: var(--auth-ink);
}
.brand-mark {
  width: 84px;
  height: 84px;
  margin: 0 auto var(--sp-4);
  border-radius: var(--r-xl);
  background: #fff;
  display: grid;
  place-items: center;
  box-shadow: var(--lift-hi);
}
.brand-name {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  letter-spacing: -0.015em;
}
.brand-sub {
  margin: 0;
  font-size: var(--fs-md);
  /* 0.82, bukan 0.9: di atas hijau pekat teks putih 90% masih terbaca sebagai
     baris kedua yang setara judulnya. Yang dicari keterangan, bukan subjudul. */
  opacity: 0.82;
}

.auth-sheet {
  padding: var(--sp-6) var(--sp-5) calc(var(--sp-8) + env(safe-area-inset-bottom));
  min-height: 46vh;
}

/* ---------------- Sheet ---------------- */
/*
 * The content sheet rides UP over the coloured header. That single overlap is
 * what makes a page read as an app screen instead of a web page: the header
 * becomes a backdrop, and the sheet becomes the subject.
 */
.sheet {
  position: relative;
  z-index: 2;
  margin-top: calc(-1 * var(--sp-5));
  padding: var(--sp-5) var(--sp-4) var(--sp-6);
  background: var(--canvas);
  background-image: var(--canvas-wash);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
}

/* ---------------- Fields & buttons ---------------- */

.field-label {
  display: block;
  font-size: var(--fs-base);
  font-weight: var(--fw-medium);
  margin-bottom: var(--sp-2);
}
.field {
  width: 100%;
  /* 16px is the floor below which iOS Safari zooms the page on focus. */
  font-size: var(--fs-lg);
  font-family: inherit;
  font-weight: var(--fw-medium);
  color: var(--ink);
  background: var(--surface);
  border: 1.5px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--sp-4) var(--sp-4);
  min-height: 54px;
  transition: border-color 0.14s var(--ease), box-shadow 0.14s var(--ease);
}
.field::placeholder {
  color: var(--muted);
  font-weight: var(--fw-regular);
}
.field:focus {
  outline: none;
  border-color: var(--brand-600);
  box-shadow: 0 0 0 4px var(--brand-050);
}
.field-code {
  text-align: center;
  font-size: var(--fs-3xl);
  font-weight: var(--fw-bold);
  letter-spacing: 0.34em;
  text-indent: 0.34em;
  font-variant-numeric: tabular-nums;
}
.field-hint {
  margin: var(--sp-2) 0 var(--sp-5);
  font-size: var(--fs-sm);
  color: var(--muted);
}

.btn {
  width: 100%;
  min-height: 54px;
  border-radius: var(--r-md);
  border: 1.5px solid transparent;
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  transition: transform 0.1s var(--ease), opacity 0.14s var(--ease),
    background 0.14s var(--ease);
}
.btn + .btn {
  margin-top: var(--sp-3);
}
.btn:active:not(:disabled) {
  transform: scale(0.985);
}
.btn:disabled {
  opacity: 0.45;
  cursor: default;
}
.btn:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
/* `--brand-solid`, bukan `--brand-600`. Yang kedua adalah TINTA dan ia berbalik
   jadi hijau muda di mode gelap - dengan label putih di atasnya, tombol
   terpenting di app jatuh ke 2.1:1. Isian tidak ikut berbalik. */
.btn-primary {
  background: var(--brand-solid);
  color: #fff;
  box-shadow: 0 4px 14px rgba(20, 103, 42, 0.24);
}
.btn-quiet {
  background: var(--surface);
  border-color: var(--line);
  color: var(--ink-2);
}
.btn-link {
  background: none;
  border: none;
  color: var(--muted);
  font-weight: var(--fw-medium);
  min-height: 46px;
  font-size: var(--fs-base);
}
.btn-danger {
  background: transparent;
  border-color: var(--line);
  color: var(--danger);
  margin-top: var(--sp-2);
}

.alert {
  margin: var(--sp-4) 0 0;
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  font-size: var(--fs-base);
  font-weight: var(--fw-medium);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--danger);
}

/* ---------------- App shell ---------------- */

.view-app {
  min-height: 100dvh;
  padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  max-width: 560px;
  margin: 0 auto;
}

.hero {
  position: relative;
  /* NOT overflow:hidden - the wallet card has to hang below the edge, and
     clipping it here is what flattens the whole screen back into a web page.
     The blobs are clipped by .hero-deco instead, which is what it is for. */
  padding: calc(var(--sp-6) + env(safe-area-inset-top)) var(--sp-5) var(--sp-5);
  background: var(--hero-grad);
  color: var(--hero-ink);
}
.hero-deco {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
/* No wallet on these screens, so the sheet does the overlapping instead. */
.hero-short {
  padding-bottom: var(--sp-8);
}
.hero-title {
  position: relative;
  margin: 0;
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  letter-spacing: -0.02em;
  line-height: 1.2;
}
/*
 * Keterangan di bawah judul layar. `--ink-2` sebagai warna, bukan tinta judul
 * yang ditipiskan `opacity`.
 *
 * Opacity terlihat sama saja selama latarnya satu warna, dan berhenti sama
 * begitu ada bola hiasan lewat di belakangnya: teks 88% di atas bola kuning
 * berubah jadi kuning kecokelatan, sementara `--ink-2` tetap `--ink-2`.
 */
.hero-caption {
  position: relative;
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-base);
  color: var(--ink-2);
}
/* ---------------- Banner beranda ---------------- */
/*
 * Carousel full-bleed tepat di bawah baris sapaan, dan gambar terbesar di
 * seluruh app - jadi aturannya yang paling ketat:
 *
 *  - RASIO DIKUNCI DI SINI, bukan oleh gambarnya. Slide yang tingginya
 *    mengikuti gambar akan melompat saat gambar berikutnya termuat dan
 *    menggeser kartu member di bawahnya, persis pada detik jempol menekannya.
 *    16:9 karena itu bentuk yang orang sudah punya file-nya.
 *  - `object-fit: cover`, jadi gambar tegak TERPOTONG, bukan menciut. Itu
 *    sebabnya pratinjau di dashboard memakai potongan yang sama persis.
 *  - Tidak ada teks yang digambar di atas slide. Banner di pasar ini datang
 *    dengan tulisannya sudah ada di dalam artwork; caption tambahan pasti
 *    menabraknya.
 */
.banner {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--surface-2);
}
.banner-track {
  display: flex;
  height: 100%;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.banner-track::-webkit-scrollbar {
  display: none;
}
.banner-slide {
  flex: 0 0 100%;
  /* `always`, bukan `normal`: satu geseran cepat tidak boleh melewati dua
     slide sekaligus - orang kehilangan tempatnya dan tidak tahu ada yang
     terlewat. */
  scroll-snap-align: start;
  scroll-snap-stop: always;
  height: 100%;
  padding: 0;
  border: none;
  background: none;
  display: block;
  font-family: inherit;
  color: inherit;
  cursor: pointer;
}
/* Slide tanpa tujuan tetap digambar, tapi tidak berpura-pura bisa ditekan. */
.banner-slide.is-static {
  cursor: default;
}
.banner-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.banner-slide:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: -3px;
}
/* Gradien setipis mungkin, hanya setinggi jalur titik - dan HANYA kalau ada
   titiknya. Tanpa ini titik putih hilang di atas poster berlatar putih; dengan
   satu slide (tanpa titik) ia cuma menggelapkan gambar tanpa alasan. */
.banner.has-dots::after {
  content: '';
  position: absolute;
  inset: auto 0 0;
  height: 44px;
  background: linear-gradient(to top, rgba(8, 14, 22, 0.45), rgba(8, 14, 22, 0));
  pointer-events: none;
}
.banner-dots {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 9px;
  z-index: 1;
  display: flex;
  justify-content: center;
  gap: var(--sp-2);
}
/* Titik aktif MEMANJANG, tidak sekadar memutih: di layar terang, perbedaan
   putih 55% dan putih 100% hilang sama sekali, sedangkan perbedaan panjang
   tetap terbaca. */
.banner-dot {
  width: 6px;
  height: 6px;
  padding: 0;
  border: none;
  border-radius: var(--r-full);
  background: rgba(255, 255, 255, 0.55);
  cursor: pointer;
  transition: width 0.22s var(--ease), background 0.22s var(--ease);
}
.banner-dot.is-active {
  width: 18px;
  background: #fff;
}
@media (prefers-reduced-motion: reduce) {
  .banner-dot {
    transition: none;
  }
}

/* ---------------- Member card ---------------- */
/*
 * Kartu member: siapa (nomor + tier) di atas, apa yang dimiliki (tiga angka) di
 * tengah, satu tombol di bawah. Itu saja.
 *
 * Yang KELUAR dari kartu ini adalah strip "menuju tier berikutnya". Itu bukan
 * sifat kartunya melainkan pekerjaan yang sedang berjalan, dan sebagai bilah
 * tersendiri di bawah kartu ia bisa gelap, selebar layar, dan jelas bisa
 * ditekan. Di dalam kartu ia selamanya kotak di dalam kotak.
 *
 * Warnanya datang dari tier. Satu `data-tier` di kartu menetapkan
 * `--tier-solid` untuk semua isinya, jadi kartu Gold benar-benar gold - cahaya,
 * chip, medali, dan angka poin bergerak bersama, bukan empat komponen yang
 * masing-masing memilih gold sendiri.
 */

.wallet {
  position: relative;
  /* Cahaya tier adalah lingkaran buram yang meluber melewati sudut; tanpa ini
     ia melukis kotak lembut di luar kartu. */
  overflow: hidden;
  /* Permukaannya bukan putih rata lagi. Kartu member itu benda, dan benda punya
     bahan: warna tier membasahi SELURUH kartu, dari 6% di pundak kiri sampai
     14% di sudut kanan bawah.

     Dulu sapuan ini cuma 9% dan cuma di ujung bawah, dengan 44% teratas kartu
     dibiarkan `--surface` murni. Batas 9% itu dipasang karena angka-angka besar
     dikhawatirkan kehilangan kontras - tetapi angka-angka itu sejak lama sudah
     pindah ke `.wallet-facts`, yang punya latar `--surface` sendiri dan tidak
     bisa disentuh sapuan ini. Yang tersisa adalah kartu yang 44% teratasnya
     putih identik di keempat tier: Silver dan Platinum berselisih 3 dari 255
     pada satu sudut saja, dan itu bukan selisih yang bisa dilihat mata.

     Angka-angka ini pernah dua kali lebih pekat (10/14/22) dan diturunkan atas
     permintaan pemilik. Yang membuat penurunan itu aman adalah LUASNYA, bukan
     kepekatannya: sapuan 6% yang menutup seluruh kartu tetap terbaca sebagai
     warna, sementara 9% yang hanya menyentuh satu sudut tidak. Kalau suatu saat
     ia ditipiskan lagi, yang harus dijaga bukan angkanya melainkan syarat itu -
     begitu sapuannya kembali menyisakan bidang `--surface` polos di bagian yang
     paling dilihat, keempat tier kembali kembar.

     Teks yang duduk langsung di atas sapuan ini cuma satu, `.wallet-id-label`,
     dan ia tetap `--ink-2` (lihat aturannya di bawah): pada 14% ia menahan
     7,1:1 di tier terburuk (Platinum terang) dan 7,3:1 di mode gelap terburuk
     (Gold). `--muted` yang lama masih tidak cukup - 3,97:1, tetap di bawah AA.

     Yang tetap tidak ikut berwarna: panel angka, bilah tujuan, dan tombol -
     ketiganya membawa latarnya sendiri. Itu disengaja: kartu berwarna dengan
     panel netral di tengahnya terbaca sebagai bahan yang ditakik, bukan sebagai
     poster. */
  background: linear-gradient(
    158deg,
    color-mix(in srgb, var(--tier-solid, var(--brand-600)) 6%, var(--surface)) 0%,
    color-mix(in srgb, var(--tier-solid, var(--brand-600)) 9%, var(--surface)) 45%,
    color-mix(in srgb, var(--tier-solid, var(--brand-600)) 14%, var(--surface)) 100%
  );
  color: var(--ink);
  /* Tepi setipis rambut dalam warna tier. Di mode terang inilah yang memberi
     kartu putih di atas kanvas pastel sebuah BATAS - bayangan saja menyerahkan
     seluruh pekerjaan itu pada mata. */
  border: 1px solid color-mix(in srgb, var(--tier-solid, var(--brand-600)) 16%, transparent);
  border-radius: var(--r-xl);
  /* Ruang di atas dan di bawah angka besar itulah yang membedakan kartu dari
     kotak isi. Kartu yang pas-pasan tingginya selalu terbaca sebagai panel -
     dan dengan tiga angka duduk dalam SATU baris, ruang itu tidak datang lagi
     dari susunannya, jadi ia harus datang dari sini.
     Atas 28, bawah 24: yang di dasar kartu adalah tombol biru penuh, dan blok
     warna sepekat itu membawa bobotnya sendiri - diberi ruang sama besar
     dengan bagian atas yang isinya cuma nomor tipis, kartunya terlihat berat
     sebelah ke bawah. */
  padding: var(--sp-8) var(--sp-6) var(--sp-6);
  /* Kartu ini benda yang MELAYANG di atas beranda, bukan panel yang menempel.
     Bayangannya karena itu satu tingkat lebih dalam daripada `--lift-hi` yang
     dipakai sisa app - cukup untuk memisahkannya dari kanvas pastel di
     belakangnya, belum cukup untuk terbaca sebagai benda yang mengambang jauh.

     Garis cahaya di bibir atas ikut dipasang meski di mode terang ia praktis
     tak terlihat di atas permukaan yang sudah putih: yang membutuhkannya mode
     gelap, di mana tepi atas kartu tanpa garis ini larut ke dalam kanvas. */
  box-shadow:
    inset 0 1px 0 var(--emboss-top),
    var(--lift-card);
}
.wallet-glow {
  position: absolute;
  top: -84px;
  right: -54px;
  width: 216px;
  height: 216px;
  border-radius: 50%;
  background: var(--tier-solid, var(--brand-600));
  opacity: 0.17;
  filter: blur(42px);
  pointer-events: none;
}

/* Semuanya di atas cahaya tier DAN di atas maskot, yang keduanya berlapis nol.
   Isi kartu yang lupa disebut di sini akan tenggelam di balik maskot. */
.wallet-brand {
  position: relative;
  z-index: 1;
}

/*
 * Baris paling atas kartu: merek di kiri, tier di kanan - seperti pada kartu
 * member sungguhan, di mana logo penerbit dan kelas kartunya memang duduk
 * sebaris.
 */
.wallet-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-6);
}
/*
 * Logonyalah yang MEMUAI dan yang MENGALAH.
 *
 * Medali dan lencana tier lebarnya ditentukan isinya - "PLATINUM" 23px lebih
 * lebar daripada "GOLD" - dan keduanya tidak boleh menciut: lencana yang
 * terpotong menyebutkan tier yang salah. Jadi merekalah yang tetap, dan grup
 * logo ini yang menyesuaikan diri.
 */
.wallet-logos {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: flex-end;
  gap: var(--sp-3);
}
/*
 * Rata BAWAH di dalam grup: keduanya lockup dengan tinggi berbeda - yang kiri
 * satu baris, yang kanan dua - dan hanya garis bawahnyalah yang sama-sama ada
 * pada keduanya. Rata tengah membuat kata "FRUITCITY" di kedua logo duduk di
 * dua ketinggian berbeda, dan itu terlihat seperti salah pasang.
 *
 * Tingginya yang dikunci, bukan lebarnya. `min-width: 0` + `object-fit`
 * membuat keduanya boleh menciut PROPORSIONAL saat kartunya sempit (layar
 * 320px dengan lencana "PLATINUM" adalah kasus tersempitnya) - tanpa itu,
 * gambar dengan lebar intrinsik menolak mengecil dan mendorong lencana keluar
 * kartu.
 */
.wallet-logo {
  display: block;
  height: 19px;
  width: auto;
  min-width: 0;
  object-fit: contain;
  object-position: left bottom;
}
/* Lockup dua baris, jadi ia butuh lebih tinggi supaya kata "FRUITCITY" di
   dalamnya tidak jadi setengah ukuran tetangganya. */
.wallet-logo-park {
  height: 24px;
}
/* Garis rambut antara dua logo. Keduanya merek yang BERBEDA, dan tanpa pemisah
   keduanya terbaca sebagai satu lockup panjang yang aneh. */
.wallet-logo-sep {
  flex: none;
  width: 1px;
  height: 22px;
  background: var(--line);
}

/*
 * Lencana tier: medali dan namanya dalam satu pelat berwarna tier.
 *
 * Satu benda, bukan dua yang berdampingan. Medali sendirian tidak menyebutkan
 * tier apa pun, dan nama sendirian kehilangan satu-satunya hal di kartu ini
 * yang terasa seperti sesuatu yang DIRAIH.
 */
.wallet-tier {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-3) var(--sp-1) var(--sp-2);
  border-radius: var(--r-full);
  background: var(--tier-bg, var(--surface-2));
  color: var(--tier, var(--ink-2));
}
.wallet-tier-medal {
  flex: none;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
}
.wallet-tier-medal svg {
  width: 20px;
  height: 20px;
}
.wallet-tier-name {
  font-size: var(--fs-badge);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}

/*
 * Maskot di sudut kanan, dan sengaja TERTUTUP SEPARUH oleh panel angka di
 * bawahnya. Yang terlihat kepala, balon dan konfetinya; badannya hilang di
 * balik panel. Itu yang membuatnya terbaca sebagai hiasan yang mengintip dari
 * dalam kartu, bukan stiker yang ditempel di atasnya - dan sekaligus alasan ia
 * boleh sebesar ini tanpa merebut perhatian dari angka.
 */
.wallet-mascot {
  position: absolute;
  z-index: 0;
  /* Tepat di bawah lencana tier, dan RATA KANAN dengan panel angka di bawahnya.
     Keduanya bukan angka bulat yang dipilih karena enak dilihat:
     - lencana tier latarnya TEMBUS (`--tier-bg` adalah color-mix dengan
       transparent), jadi maskot yang menyelinap di belakangnya akan terlihat
       menembus lencana, bukan tertutup olehnya;
     - kalau sisi kanannya melewati tepi panel, bagian bawah maskot muncul di
       samping panel alih-alih tertutup - termasuk bayangan tanahnya, yang di
       sana terbaca sebagai noda abu-abu yang tidak jelas apa.

     `top` mengikuti `margin-bottom` .wallet-brand: baris merek turun 12px
     (12px -> 24px), jadi maskot ikut turun 12px (62px -> 74px). Kalau salah
     satunya diubah tanpa yang lain, maskot menyelinap ke belakang lencana
     tier yang latarnya tembus. */
  top: 74px;
  right: 24px;
  width: 92px;
  height: auto;
  pointer-events: none;
  user-select: none;
}

/*
 * Nomor member sebagai ANGKA BESAR.
 *
 * Ini satu-satunya hal di kartu yang dibacakan orang lewat telepon dan diketik
 * kasir, dan sebagai baris 13px abu-abu di pojok ia tidak pernah pantas
 * mendapat pekerjaan itu.
 */
.wallet-id {
  position: relative;
  z-index: 1;
  /* Ruang untuk maskot yang mengintip: tanpa ini nomor yang panjang berjalan
     tepat di bawah balonnya. Nomor yang TERPOTONG tidak boleh terjadi di sini
     - ia satu-satunya hal di kartu yang diketik ulang kasir - jadi ukuran
     hurufnya yang disetel sampai muat, bukan ruang maskotnya yang dipaksakan. */
  padding-right: 78px;
  margin-bottom: var(--sp-4);
}
.wallet-id-label {
  margin: 0 0 1px;
  /* 11px huruf besar tebal - lantai ukuran huruf app ini, dan ini salah satu
     dari sedikit hal yang boleh menyentuhnya: keterangan sependek "ID Member"
     dibaca sebagai penanda, bukan sebagai kalimat. */
  font-size: var(--fs-badge);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* `--ink-2`, bukan `--muted`, dan ini satu-satunya keterangan di app yang
     menyimpang dari abu-abu itu. Alasannya bukan hirarki melainkan latarnya:
     ini SATU-SATUNYA teks yang duduk langsung di atas sapuan tier kartu, dan
     `--muted` di atas putih polos pun cuma 4,83:1 - tidak punya sisa untuk
     dibayar ke sapuan apa pun. Di atas Platinum 14% ia jatuh ke 3,97:1, masih
     di bawah AA meski sapuannya sudah ditipiskan. `--ink-2` menahan 7,1:1. */
  color: var(--ink-2);
}
.wallet-id-row {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  margin: 0;
}
/* Jarak huruf lebar, seperti nomor yang dicetak timbul di kartu sungguhan. */
.wallet-number {
  min-width: 0;
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  line-height: 1.22;
  letter-spacing: 0.03em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Sasaran sentuh 40px, gambar yang terlihat 20px: padding melebarkan area yang
   bisa ditekan, margin negatif menariknya kembali persis sebesar itu, jadi
   tidak ada satu piksel pun tata letak yang bergeser. */
.wallet-copy {
  flex: none;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  margin: -10px -10px -10px -4px;
  padding: 0;
  border: none;
  background: none;
  color: var(--brand-600);
  cursor: pointer;
  transition: transform 0.12s var(--ease), color 0.14s var(--ease);
}
.wallet-copy svg {
  width: 20px;
  height: 20px;
}
.wallet-copy:active {
  transform: scale(0.88);
}
.wallet-copy:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: -8px;
  border-radius: var(--r-md);
}
/* Sesudah ditekan: centang hijau di tempat yang sama. Tidak ada teks "Disalin"
   yang muncul dan menggeser nomornya - yang bergeser tepat sesudah ditekan
   selalu terbaca sebagai salah tekan. */
.wallet-copy-done {
  display: none;
}
.wallet-copy.is-done {
  /* `--brand-600`, bukan hijau logo apa adanya: hijau logo di atas kartu putih
     cuma 2.6:1, dan centang yang tidak terlihat tidak mengabarkan apa pun. */
  color: var(--brand-600);
}
.wallet-copy.is-done .wallet-copy-idle {
  display: none;
}
.wallet-copy.is-done .wallet-copy-done {
  display: block;
}

.wallet-note {
  position: relative;
  z-index: 1;
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-caption);
  color: var(--c-orange);
  font-weight: var(--fw-medium);
}

/*
 * Tiga angka dalam panel putihnya sendiri.
 *
 * Panel, bukan tiga kolom telanjang di atas kartu: ia yang memisahkan "apa
 * yang saya punya" dari nomor di atasnya dan dari tujuan di bawahnya, dan ia
 * pula yang menutupi separuh badan maskot.
 *
 * SUSUNANNYA TETAP EMPAT BARIS GRID - semua ikon di baris satu, semua
 * keterangan di baris dua, semua angka di baris tiga, semua satuan di baris
 * empat - bukan tiga kolom yang masing-masing menumpuk sendiri. Alasannya satu
 * dan tetap sama seperti dulu: tinggi baris keterangan diputuskan oleh
 * keterangan TERPANJANG, jadi "Lama bergabung" boleh patah dua baris di layar
 * sempit tanpa menjatuhkan angkanya sendirian sementara dua tetangganya
 * tinggal di atas.
 *
 * Konsekuensinya tidak ada garis pemisah vertikal antar kolom: dengan
 * `display: contents` kolomnya bukan kotak, jadi tidak ada tepi untuk digaris.
 * Yang memisahkan ketiganya adalah tiga pelat ikon berwarna berbeda, dan itu
 * memisahkan lebih tegas daripada garis rambut.
 */
.wallet-facts {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto auto auto auto;
  column-gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-2) var(--sp-3);
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--line);
}
/* Pembungkusnya dibuang dari tata letak supaya keempat span jadi anak langsung
   grid; div tanpa peran tidak kehilangan apa pun saat kotaknya hilang. */
.wallet-fact {
  display: contents;
}
/* Ikonnya bukan hiasan: tiga kolom angka tanpa penanda apa pun terbaca sebagai
   satu blok, dan yang dicari mata saat membuka app adalah SATU di antaranya. */
.wallet-fact-icon {
  grid-row: 1;
  justify-self: center;
  width: var(--plate-sm);
  height: var(--plate-sm);
  margin-bottom: var(--sp-2);
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: var(--accent-bg);
  color: var(--accent);
}
.wallet-fact-icon svg {
  width: 18px;
  height: 18px;
}
.wallet-fact-label {
  grid-row: 2;
  padding-bottom: var(--sp-1);
  /*
   * `--fs-caption` (12px), bukan lantai lencana 11px. Ini keterangan yang
   * DIBACA, dan lantai 11px hanya boleh disentuh huruf besar tebal.
   *
   * Dulu ia 11.5px dengan alasan "Lama bergabung" pada 12px patah dua baris di
   * layar 375px. Alasan itu tidak pernah bekerja - pada 11.5px pun ia sudah
   * patah dua baris di sana - dan patahnya memang tidak apa-apa: itu justru
   * pekerjaan yang dibuatkan grid empat baris ini, yaitu membiarkan keterangan
   * terpanjang menentukan tinggi barisnya tanpa menjatuhkan angka di bawahnya.
   */
  font-size: var(--fs-caption);
  line-height: 1.25;
  text-align: center;
  color: var(--muted);
}
.wallet-fact-value {
  grid-row: 3;
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  line-height: 1.2;
  letter-spacing: -0.01em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
/* Satuannya, karena angka telanjang tidak memberi tahu apa-apa: "1x" tidak
   memberi tahu 1x apa, dan "8 hari" tidak memberi tahu 8 hari sejak kapan. */
.wallet-fact-sub {
  grid-row: 4;
  margin-top: 2px;
  font-size: var(--fs-caption);
  line-height: 1.3;
  text-align: center;
  color: var(--muted);
}
.wallet-fact-sub:empty {
  display: none;
}

/* ---------------- Panel tier (di dalam kartu) ---------------- */
/*
 * Pekerjaannya satu: memberi tahu berapa lagi jaraknya.
 *
 * Dulu ia bilah tersendiri DI BAWAH kartu, dengan alasan bahwa di dalam kartu
 * ia selamanya kotak di dalam kotak. Alasan itu gugur begitu angka-angkanya
 * sendiri pindah ke panel putih: sekarang panel inilah tetangga sebaris
 * dengannya, dan "1 kunjungan" di atas dengan "1 dari 11" di sini adalah
 * kalimat yang sama yang akhirnya berdiri berdampingan.
 *
 * Ia IKUT TEMA, bukan gelap di kedua mode: di beranda mode terang, satu-satunya
 * benda gelap di layar tidak terbaca sebagai penekanan melainkan sebagai
 * lubang. Yang memisahkannya dari panel putih di atasnya adalah latarnya yang
 * bertinta, tepinya yang bergaris, dan isinya yang berwarna tier.
 */
.wallet-goal {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  margin-top: var(--sp-3);
  padding: var(--sp-3) var(--sp-4) var(--sp-3);
  border: 1px solid var(--strip-line);
  border-radius: var(--r-lg);
  /*
   * Bertinta warna TIER, dan campurannya ditulis DI SINI - bukan sebagai satu
   * token gradien di `:root`. Elemen inilah yang berada di dalam `[data-tier]`,
   * jadi elemen inilah satu-satunya tempat `var(--tier-solid)` punya nilai.
   * Lihat `--strip-base` di `:root` untuk kenapa.
   *
   * Tintanya tipis (10% lalu 4%): yang dicari kesan bahwa bilah ini milik tier
   * yang sedang dikejar, bukan kotak berwarna. Di atas 12% angka Rp di dalamnya
   * mulai kehilangan kontras terhadap latarnya sendiri.
   */
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--tier-solid, var(--brand-500)) 10%, var(--strip-base)) 0%,
    color-mix(in srgb, var(--tier-solid, var(--brand-500)) 4%, var(--strip-base)) 100%
  );
  color: var(--strip-ink);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: transform 0.12s var(--ease);
}
.wallet-goal:active {
  transform: scale(0.99);
}
.wallet-goal:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
.wallet-goal-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
}
.wallet-goal-icon {
  flex: none;
  width: var(--plate-sm);
  height: var(--plate-sm);
  border-radius: var(--r-sm);
  display: grid;
  place-items: center;
  background: var(--tier-bg, var(--surface-2));
  color: var(--tier, var(--ink-2));
}
.wallet-goal-icon svg {
  width: 20px;
  height: 20px;
}
/* Satu-satunya hal di panel ini yang perlu dibedakan per mode, dan alasannya
   bukan temanya melainkan latarnya: di mode gelap latar panel adalah navy, dan
   `--tier-bg` (34% kuning) di atas navy menghasilkan zaitun yang membuat ikon
   kuning di atasnya nyaris tidak lolos kontras. Tint yang lebih tipis
   membiarkan navy di belakangnya tembus, dan justru itu yang menaikkannya. */
[data-theme='dark'] .wallet-goal-icon {
  background: color-mix(in srgb, var(--tier-solid, var(--brand-600)) 20%, transparent);
}
.wallet-goal-text {
  flex: 1;
  min-width: 0;
}
/* Huruf besar tebal 11px: tinggi hurufnya setara teks biasa 14px, dan sebagai
   penanda ("MENUJU GOLD") ia memang bukan kalimat yang dibaca kata per kata. */
/* `balance` supaya "MENUJU GOLD · SISA 175 HARI" tidak menyisakan kata
   "HARI" sendirian di baris kedua saat layarnya sempit. */
.wallet-goal-eyebrow {
  display: block;
  text-wrap: balance;
  font-size: var(--fs-badge);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  line-height: 1.3;
  color: var(--strip-ink-2);
}
.wallet-goal-tier {
  color: var(--tier, var(--strip-ink));
}
.wallet-goal-days:empty {
  display: none;
}
.wallet-goal-need {
  display: block;
  margin-top: 3px;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  line-height: 1.35;
}
/*
 * Angka di dalam kalimat itu: "10" dan "Rp460.000".
 *
 * Kalimatnya sendiri tinggal di 500, dan justru itu yang membuat ini bekerja -
 * penekanan hanya ada kalau ada yang TIDAK ditekankan di sebelahnya. Kalau
 * seluruh barisnya 600, angka 700 di dalamnya nyaris tidak terlihat bedanya.
 *
 * `--brand-600` ikut berbalik per mode, jadi satu baris ini menutup keduanya:
 * hijau tua di atas bilah terang, hijau muda di atas bilah gelap.
 */
.wallet-goal-num {
  font-weight: var(--fw-bold);
  color: var(--brand-600);
  /* Angka sejajar, supaya "10" dan "9" sama lebarnya saat nilainya berubah. */
  font-variant-numeric: tabular-nums;
}
.wallet-goal-chev {
  flex: none;
  width: 16px;
  height: 16px;
  color: var(--strip-ink-2);
}
/*
 * Sisa jalan yang BELUM tercapai, di bilah tier.
 *
 * BUKAN `--strip-track`. Token itu putih bersih di mode terang, dan alasannya
 * masih tercatat di `:root` dari zaman ketika panel di sekitarnya masih pastel:
 * alur netral yang membuat isian berwarna tier apa pun tetap terbaca. Mode
 * terang sekarang putih bersih, jadi alur putih di atas panel putih tidak
 * terbaca sebagai "sisa jalan" - ia tidak terbaca sama sekali, dan yang
 * tertinggal di layar adalah pita warna yang berhenti mendadak di tengah.
 *
 * `--strip-ink` ikut berbalik per mode, jadi satu nilai melayani keduanya: alur
 * abu di atas panel terang, alur terang di atas panel gelap.
 *
 * `background-color`, bukan shorthand `background`: lapisan latar apa pun yang
 * kelak dipasang `.track` akan terhapus diam-diam dari bilah ini saja. Aturan
 * inilah yang pernah melakukannya persis begitu.
 */
.wallet-goal .track {
  background-color: color-mix(in srgb, var(--strip-ink) 9%, transparent);
}
/* Isiannya tidak lagi punya aturan sendiri di sini: sejak 0.24.0 batang di
   mana pun memakai warna logo (`.track-fill`), bukan warna tier. */
/* Penyebut di kedua ujung batang. Batang tanpa penyebut panjangnya tidak bisa
   diperiksa siapa pun - dan keduanya menyebut JALUR YANG SAMA dengan yang
   sedang digambar batangnya, bukan campuran kunjungan dan rupiah. */
.wallet-goal-scale {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-top: var(--sp-2);
  font-size: var(--fs-caption);
  line-height: 1.3;
  color: var(--strip-ink-2);
  font-variant-numeric: tabular-nums;
}
.wallet-goal-scale span {
  min-width: 0;
}

/*
 * Di bawah 360px yang mengalah adalah HIASANNYA, bukan angkanya.
 *
 * Seperempat lebar kartu di layar 320px tinggal 71px, dan di sana maskot serta
 * medali tier hanya merebut ruang dari nomor member dan dari nama tiernya.
 * Keduanya tidak mengatakan apa pun yang tidak sudah tertulis di sebelahnya.
 */
@media (max-width: 359px) {
  .wallet {
    padding: var(--sp-5) var(--sp-4) var(--sp-5);
  }
  .wallet-mascot {
    display: none;
  }
  .wallet-id {
    padding-right: 0;
  }
  .wallet-tier-medal {
    display: none;
  }
  .wallet-facts {
    padding: var(--sp-3) var(--sp-1) var(--sp-3);
    column-gap: var(--sp-1);
  }
  /* Keterangannya TIDAK ikut mengecil di sini. Yang boleh mengalah di layar
     sempit cuma hiasan; teks yang dibaca tetap di lantainya.

     Tombolnya diurus blok media SENDIRI, di bawah aturan dasarnya - lihat
     alasannya di sana. */
}

/*
 * Aksi utama seluruh app, jadi ia tombol yang paling kuat di layar.
 *
 * Jaraknya ke panel di atasnya sengaja lebih lebar daripada jarak antar panel
 * (22px lawan 13px): tombol yang menempel pada kotak di atasnya terbaca sebagai
 * bagian dari kotak itu, bukan sebagai kesimpulan seluruh kartu.
 */
.wallet-cta {
  position: relative;
  z-index: 1;
  margin-top: var(--sp-5);
  width: 100%;
  min-height: 54px;
  /*
   * Ruang kanan-kiri untuk chevron yang melayang di tepi kanan.
   *
   * Chevron itu `position: absolute` - ia TIDAK ikut dihitung dalam pemusatan,
   * jadi tanpa padding ini label yang panjang berjalan lurus ke bawahnya. Di
   * layar 320px "Tunjukkan kartu ke kasir" persis melakukan itu: huruf
   * terakhirnya tertimpa panah. Kedua sisi diberi ruang yang sama supaya
   * labelnya tetap di tengah optis, bukan tergeser ke kiri.
   */
  padding: 0 var(--sp-8);
  border: none;
  border-radius: var(--r-md);
  background: linear-gradient(105deg, var(--brand-solid), var(--brand-500));
  color: #fff;
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  box-shadow: 0 4px 14px rgba(20, 103, 42, 0.26);
}
.wallet-cta svg {
  flex: none;
  width: 19px;
  height: 19px;
}
/* Chevron menempel di tepi kanan, bukan mengekor label: label yang berpasangan
   dengan ikon QR-nya harus tetap di TENGAH tombol, dan chevron yang ikut
   dihitung ke dalam pemusatan akan menggeser keduanya ke kiri. */
.wallet-cta-chev {
  position: absolute;
  right: var(--sp-4);
  width: 17px;
  height: 17px;
  opacity: 0.85;
}
.wallet-cta:active {
  transform: scale(0.985);
}
.wallet-cta:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/*
 * Tombol utama di layar 320px, dan blok ini berdiri SESUDAH aturan dasarnya -
 * bukan ikut menumpang blok media milik kartu di atas sana.
 *
 * Media query tidak menambah kekhususan apa pun. `@media` yang ditulis di atas
 * aturan dasar dengan pemilih yang sama akan kalah oleh aturan dasar itu,
 * diam-diam, di semua peramban. Blok di atas memang benar untuk `.wallet` dan
 * `.wallet-facts` karena keduanya dinyatakan sebelum blok itu; `.wallet-cta`
 * dinyatakan sesudahnya, jadi override apa pun untuknya di sana adalah baris
 * mati - dan `margin-top` di sana memang sudah mati sejak lama tanpa ketahuan.
 *
 * Yang mengalah di sini dua hal, dan keduanya bukan kalimatnya: chevron
 * dilepas (ia hiasan - tombol selebar layar tidak butuh panah untuk mengaku
 * bisa ditekan), dan hurufnya turun satu tangga ke 14px, masih dua tangga di
 * atas lantai 12px. Pada 15px labelnya patah dua baris dan ikon QR-nya
 * tertinggal sendirian di baris pertama.
 */
@media (max-width: 359px) {
  .wallet-cta {
    margin-top: var(--sp-4);
    padding: 0 var(--sp-3);
    font-size: var(--fs-base);
  }
  .wallet-cta-chev {
    display: none;
  }
}

/*
 * Beranda tidak punya header berwarna lagi: banner yang paling atas, dan
 * sapaan turun jadi baris pertama isi.
 *
 * Karena itu `#tab-home` sendiri yang menahan bilah status ponsel. Dulu
 * pekerjaan itu dipegang `.hero` lewat paddingnya; sekarang tidak ada hero di
 * sini, dan dengan `viewport-fit=cover` isi yang tidak ditahan merambat ke
 * bawah jam dan baterai. Ditaruh di tab, bukan di banner, karena beranda tanpa
 * banner tetap butuh ditahan.
 */
#tab-home {
  padding-top: env(safe-area-inset-top);
}
/* Lembarnya menyambung tepat di tepi bawah gambar - tidak menaikinya. Menaiki
   gambar berarti memakan titik-titik carousel yang duduk 9px dari tepi itu. */
#tab-home .sheet {
  margin-top: 0;
  padding-top: var(--sp-4);
  border-radius: 0;
}
/* Tanpa banner, lembar inilah benda pertama di layar, dan sapaan yang menempel
   di tepi atas layar terbaca seperti teks yang lupa diberi jarak. */
#tab-home.no-banner .sheet {
  padding-top: var(--sp-5);
}

/* Sapaan. Satu baris, dan namanya saja yang tebal - nama yang dicocokkan
   kasir ada di layar QR, jadi di sini ia sapaan, bukan judul. */
.home-greet {
  margin: 0 2px var(--sp-4);
  min-width: 0;
  font-size: var(--fs-md);
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.home-greet-name {
  font-weight: var(--fw-semibold);
  letter-spacing: -0.01em;
  color: var(--ink);
}

/* ---------------- Tier colour ---------------- */
/*
 * Tier gets a colour of its own from the logo palette. A member should be able
 * to tell Gold from Silver across a table, without reading.
 *
 * The colour is declared ONCE as a pair of custom properties on whatever
 * carries `data-tier`, and every tier-coloured thing - the chip, the medal, the
 * label at the end of the progress track, the edge of a card on the tier page -
 * reads those two properties. Four components hand-picking their own gold is
 * how two golds end up on one screen.
 */
/*
 * Three properties, not one, because they do different jobs and one value
 * cannot do both: `--tier` is INK, darkened until it reads on a tint, and
 * `--tier-solid` is the brand colour itself, for things that are filled rather
 * than read - the progress bar, the glow behind the card. Gold ink is #7a5800,
 * which as a bar is mud; gold fill is #ffdd2f, which as text is invisible.
 */
/*
 * `--tier-on-dark` adalah properti KEEMPAT, dan ia ada karena tinta yang benar
 * di atas kartu putih adalah tinta yang tidak terbaca di atas bilah navy.
 * Sebelumnya nilai-nilai ini hanya hidup di dalam blok mode gelap; sekarang ia
 * dinyatakan sekali di sini dan dipakai DUA kali: oleh mode gelap, dan oleh
 * bilah tier yang gelap di kedua mode.
 */
[data-tier] {
  --tier: var(--ink-2);
  --tier-on-dark: var(--ink-2);
  --tier-bg: var(--surface-2);
  --tier-solid: var(--brand-600);
}
[data-tier='BRONZE'] {
  --tier: #8a4a06;
  --tier-on-dark: var(--c-orange);
  --tier-bg: color-mix(in srgb, var(--c-orange) 20%, transparent);
  --tier-solid: var(--c-orange);
}
/*
 * Silver = PERAK. Namanya sendiri sudah menyebut warnanya, dan itu mengalahkan
 * alasan apa pun untuk memberinya warna lain.
 *
 * Ia sempat biru lockup FRUITCITY PARK, dengan alasan logo FRUITCITY tidak
 * punya perak maupun abu-abu jadi lebih baik memakai warna merek yang sungguhan
 * daripada abu-abu yang dikarang. Alasan itu keliru sejak awal: member tidak
 * membaca palet merek, ia membaca kata "SILVER" lalu melihat lencana biru.
 *
 * Peraknya condong BIRU-DINGIN, bukan abu-abu netral, dan itu disengaja: nilai
 * cadangan `[data-tier]` (dipakai sebelum JS menyetel tiernya) adalah abu-abu
 * netral, dan perak yang netral juga akan terbaca sebagai "tier belum diketahui".
 *
 * Tiga nilai, tiga pekerjaan:
 *   --tier         #445565  tinta. 7.7:1 di atas putih - perak asli #c0c0c0
 *                           cuma 1.6:1, ia tidak pernah bisa jadi teks.
 *   --tier-solid   #7a8899  isian: batang kemajuan, cahaya kartu. 3.6:1 di atas
 *                           alur putih, jadi batangnya masih terlihat.
 *   --tier-on-dark #c3cdd8  tinta versi mode gelap.
 */
[data-tier='SILVER'] {
  --tier: #445565;
  --tier-on-dark: #c3cdd8;
  --tier-bg: color-mix(in srgb, #8e9baa 26%, transparent);
  --tier-solid: #7a8899;
}
[data-tier='GOLD'] {
  --tier: #7a5800;
  --tier-on-dark: var(--c-yellow);
  --tier-bg: color-mix(in srgb, var(--c-yellow) 34%, transparent);
  --tier-solid: var(--c-yellow);
}
[data-tier='PLATINUM'] {
  --tier: #5e2f74;
  --tier-on-dark: #c79bdc;
  --tier-bg: color-mix(in srgb, var(--c-purple) 20%, transparent);
  --tier-solid: var(--c-purple);
}
/* Hanya tintanya yang dibalik di mode gelap. `--tier-solid` tidak: warna isian
   adalah warna merek, dan ia sama di kedua mode. */
[data-theme='dark'] [data-tier] {
  --tier: var(--tier-on-dark);
}

/* Every read carries the neutral fallback inline. `var(--tier)` with nothing
   behind it is invalid at computed-value time, not "skip this rule" - so a chip
   painted before JS has set `data-tier` would lose its background entirely
   rather than fall back to the grey it is supposed to start at. */
.tier-chip {
  flex: none;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-full);
  font-size: var(--fs-badge);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background: var(--tier-bg, var(--surface-2));
  color: var(--tier, var(--ink-2));
}

/* ---------------- Kilau lencana tier ---------------- */
/*
 * Sapuan cahaya yang BERJALAN TERUS di atas lencana tier - satu lintasan penuh
 * tiap 5 detik, kecepatan tetap, tanpa jeda diam.
 *
 * Ia sempat dibuat menyapu sekali lalu diam lima detik, dengan alasan jeda
 * itulah yang membuat kilau terbaca sebagai pantulan pada logam alih-alih
 * spanduk diskon. Diminta diubah: yang dicari kilau yang mengalir pelan, bukan
 * yang sesekali muncul. Jadi jedanya dibuang, dan yang menjaganya tetap tenang
 * dipindahkan ke KECEPATAN.
 *
 * Angka yang menentukan di sini BUKAN durasinya, melainkan piksel per detik -
 * dan itu bergantung pada lebar bendanya. Lencana selebar 90px berarti cahaya
 * menempuh 180px per siklus; pada 5 detik itu 36 piksel per detik. Cukup pelan
 * untuk diikuti mata tanpa menarik perhatian dari angka poin di bawahnya.
 *
 * Bandingkan dengan bar kemajuan yang jauh lebih lebar (lihat `track-rainbow`,
 * 48 px/detik): di sana temponya dijaga dengan mengunci lebar ubinnya dalam
 * piksel, justru supaya durasinya BOLEH berbeda dari yang di sini. Menyamakan
 * DURASI dua benda yang beda lebar tidak menyamakan temponya - itu cara membuat
 * keduanya terasa tidak sinkron.
 *
 * `linear`, bukan easing. Kilau yang memelan di ujung lalu memburu di tengah
 * terbaca sebagai benda yang dilempar; cahaya yang melintas kecepatannya tetap.
 *
 * Sambungan siklusnya tidak terlihat karena kedua ujung pita gradiennya
 * transparan: saat ia melompat dari ujung kanan kembali ke ujung kiri, yang
 * berpindah adalah bagian yang memang tidak menggambar apa-apa.
 *
 * Tiga keputusan lain yang menopangnya:
 *
 *  - Pelatnya diberi GRADIEN tipis warna tier lebih dulu. Kilau hanya terlihat
 *    kalau ada beda terang-gelap untuk dilewatinya; di atas pelat rata,
 *    sapuan seterang apa pun tidak menghasilkan apa-apa.
 *  - Sapuannya di BELAKANG isi (`z-index: -1` + `isolation: isolate`), bukan di
 *    atasnya. Nama tier itu keterangan yang dicocokkan kasir - dan sekarang
 *    kilaunya lewat tiap 5 detik tanpa henti, jadi kalau ia menyapu di ATAS
 *    tulisan, tulisan itu berkedip pucat sepanjang layar terbuka. `isolation`
 *    yang membuat `z-index: -1` berhenti di lencana ini alih-alih tenggelam ke
 *    belakang kartunya.
 *  - Yang digerakkan `transform`, bukan `background-position`. Transform
 *    dikerjakan compositor tanpa menggambar ulang apa pun - dan karena animasi
 *    ini sekarang berjalan TERUS, bukan sesekali, bedanya bukan lagi soal
 *    kehalusan melainkan soal baterai.
 */
.wallet-tier,
.tier-chip {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  /*
   * Pelat LOGAM, dan seluruh kesan logamnya ada di sini - di lapisan yang DIAM.
   *
   * Dulu pelat ini cuma gradien dua warna dari tua ke muda, dan seluruh beban
   * "supaya terlihat logam" ditaruh pada kilau yang bergerak: pita putih
   * diapit dua pita gelap. Itu sumber tepi kerasnya. Pita gelap yang bergerak
   * dan bersebelahan langsung dengan putih menghasilkan batas tajam yang
   * terbaca sebagai potongan kertas, bukan pantulan.
   *
   * Sekarang pembagiannya jelas: LOGAMNYA di pelat diam ini - empat titik yang
   * naik-turun seperti permukaan melengkung yang ditekuk di tengah - dan
   * CAHAYANYA di lapisan bergerak, putih murni tanpa satu pun pita gelap.
   *
   * 52% adalah titik tergelapnya, dan itu batasnya: di atas itu tinta lencana
   * jatuh di bawah 4.5:1.
   */
  background: linear-gradient(
    145deg,
    color-mix(in srgb, var(--tier-solid, var(--ink-2)) 20%, transparent) 0%,
    color-mix(in srgb, var(--tier-solid, var(--ink-2)) 46%, transparent) 38%,
    color-mix(in srgb, var(--tier-solid, var(--ink-2)) 26%, transparent) 56%,
    color-mix(in srgb, var(--tier-solid, var(--ink-2)) 52%, transparent) 100%
  );
}
/*
 * Alas buram, dan HANYA untuk lencana di kartu member.
 *
 * Pelat di atas seluruhnya beralfa, jadi warnanya adalah warna apa pun yang
 * kebetulan ada di belakangnya. Itu tidak apa-apa selama yang di belakangnya
 * `--surface` - dan batas 52% di atas memang dihitung terhadap `--surface`.
 * Sejak kartu ikut disapu warna tier, lencana Bronze duduk di atas cokelat dan
 * lencana Gold di atas kuning: pelat dan kartu bergerak ke arah yang sama, dan
 * di mode gelap tinta lencana jatuh dari 5,3:1 ke 4,3:1 (Bronze) dan 5,0:1 ke
 * 3,9:1 (Gold) - lewat di bawah AA tanpa satu baris pun ikut berubah.
 *
 * `background-color` saja, bukan `background`: shorthand di atas akan terhapus
 * seluruhnya, sedangkan warna latar dicat TEPAT DI BAWAH gradiennya. Hasilnya
 * lencana ini kembali persis seperti sebelum kartunya berwarna.
 *
 * `.tier-chip` sengaja TIDAK ikut: ia dipakai di halaman tier dan di daftar
 * level, di atas panel yang bukan `--surface`, dan alas buram di sana justru
 * akan mencetak kotak pucat di sekeliling tiap chip.
 */
.wallet-tier {
  background-color: var(--surface);
  /*
   * Pelat yang TIMBUL, dan kelengkungannya dibuat dari empat bayangan dalam -
   * bukan dari gradien tambahan.
   *
   * Alasannya bukan hemat baris: latar lencana ini sudah dipakai penuh oleh
   * pelat logam 145deg di aturan bersama di atas, dan menumpuk gradien kedua di
   * sana berarti menulis ulang seluruh pelat itu hanya untuk lencana kartu -
   * dua salinan dari satu nilai, dan yang kedua pasti akan tertinggal saat yang
   * pertama disetel ulang.
   *
   * Urutannya dari luar ke dalam pada tiap bibir: garis tajam 1px yang menandai
   * TEPI, lalu kabut 4px yang melengkungkan permukaan tepat di belakangnya.
   * Garis tanpa kabut menghasilkan kertas yang dilipat; kabut tanpa garis
   * menghasilkan noda. Keduanya bersama yang menghasilkan kubah.
   */
  box-shadow:
    inset 0 1px 0 var(--emboss-top),
    inset 0 4px 6px -4px var(--emboss-top),
    inset 0 -1px 0 var(--emboss-bottom),
    inset 0 -4px 6px -4px var(--emboss-bottom),
    var(--emboss-lift);
}
/*
 * DUA pita, bukan satu, dan itu yang membuatnya benar-benar mengalir.
 *
 * Satu pita yang melintas dari tepi ke tepi hanya menyinggung lencana 70% dari
 * siklusnya - sisanya ia sudah lewat kanan tapi belum masuk lagi dari kiri, dan
 * lencananya polos selama lebih dari satu detik. Sedetik penuh tanpa apa-apa
 * persis yang terbaca sebagai "muncul sesekali", bukan sebagai kilau berjalan.
 *
 * Jadi ada dua, memakai `::before` dan `::after`, dengan animasi yang sama
 * persis - yang satu digeser setengah siklus lewat `animation-delay` NEGATIF.
 * Negatif, bukan positif: nilai negatif memulai animasinya di tengah jalan,
 * jadi pita kedua sudah berada di posisinya sejak bingkai pertama. Delay
 * positif akan membuat lencana kosong 2,5 detik pertama setiap kali layar
 * dibuka.
 *
 * Hasilnya: saat pita pertama keluar dari tepi kanan, pita kedua sudah masuk
 * dari kiri. Tidak pernah ada saat lencana ini benar-benar polos.
 */
.wallet-tier::before,
.wallet-tier::after,
.tier-chip::before,
.tier-chip::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /*
   * CAHAYA, bukan pita. Tiga hal yang membedakannya:
   *
   * 1. SEBELAS titik warna, bukan lima. Cahaya sungguhan meredup mengikuti
   *    lengkung, bukan garis lurus - dan gradien lima titik hanya bisa
   *    menghasilkan lereng lurus patah-patah. Nilai alfanya sengaja rapat di
   *    dekat puncak (0.72 - 0.58 - 0.36) lalu merenggang jauh di ekornya
   *    (0.16 - 0.06 - 0), yang mendekati bentuk redup cahaya asli.
   *
   * 2. SEMUANYA PUTIH, termasuk kedua ujungnya - `rgba(255,255,255,0)`, bukan
   *    `transparent`. Keduanya sama-sama tembus pandang, tapi `transparent`
   *    berarti HITAM tembus: gradien dari putih ke sana melewati abu-abu di
   *    tengah jalan, dan abu-abu itulah bayangan kotor di tepi kilau.
   *
   * 3. SEBARANNYA 64% lebar lencana (18% sampai 82%), bukan 40%. Cahaya yang
   *    sempit terbaca sebagai garis yang digeser; yang lebar terbaca sebagai
   *    permukaan yang tersinari.
   *
   * `blur(2px)` di akhir adalah jaring pengaman: pada lencana setinggi 30px,
   *  gradien semiring ini masih bisa menyisakan tangga halus di tepi
   *  diagonalnya. Blur-nya statis - yang dianimasikan cuma `transform` - jadi
   *  peramban cukup meraster lapisan ini sekali lalu menggesernya.
   *
   * Miringnya 104deg, bukan 90: pantulan yang tegak lurus terbaca sebagai
   * pemindai, yang miring terbaca sebagai cahaya.
   */
  background: linear-gradient(
    104deg,
    rgba(255, 255, 255, 0) 18%,
    rgba(255, 255, 255, 0.06) 27%,
    rgba(255, 255, 255, 0.16) 34%,
    rgba(255, 255, 255, 0.36) 41%,
    rgba(255, 255, 255, 0.58) 46%,
    rgba(255, 255, 255, 0.72) 50%,
    rgba(255, 255, 255, 0.58) 54%,
    rgba(255, 255, 255, 0.36) 59%,
    rgba(255, 255, 255, 0.16) 66%,
    rgba(255, 255, 255, 0.06) 73%,
    rgba(255, 255, 255, 0) 82%
  );
  filter: blur(2px);
  /* Animasinya berjalan selamanya, jadi lapisannya memang layak dijanjikan ke
     compositor sejak awal - tanpa ini peramban boleh meraster ulang tiap kali
     kartunya digulir. */
  will-change: transform;
  animation: tier-sheen 5s linear infinite;
}
.wallet-tier::after,
.tier-chip::after {
  animation-delay: -2.5s;
}
@keyframes tier-sheen {
  /* Tanpa titik henti di tengah: dua kunci saja, jadi kecepatannya benar-benar
     tetap dari ujung ke ujung. */
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(100%);
  }
}
@media (prefers-reduced-motion: reduce) {
  .wallet-tier::before,
  .wallet-tier::after,
  .tier-chip::before,
  .tier-chip::after {
    animation: none;
    opacity: 0;
  }
}

/* The medal. A tier is a rank, and a rank wants an object, not a word in a
   pill - this is the one place the app can say "you earned something". */
.tier-medal {
  flex: none;
  width: var(--plate-lg);
  height: var(--plate-lg);
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: var(--tier-bg, var(--surface-2));
  color: var(--tier, var(--ink-2));
}
.tier-medal svg {
  width: 26px;
  height: 26px;
}
/* ---------------- Section accents ---------------- */
/*
 * Six sections, six colours from the logo, declared the same way tiers are: a
 * `data-accent` sets two properties, and the icon plate on a section heading, a
 * stat, or an information row reads them.
 *
 * The colour is not information - a member never has to learn that pink means
 * promo. It is what stops eight identical blue squares down one scroll from
 * reading as one undifferentiated block of furniture.
 */
[data-accent] {
  --accent: var(--brand-600);
  --accent-bg: var(--brand-050);
}
[data-accent='promo'] {
  --accent: #b8194a;
  --accent-bg: color-mix(in srgb, var(--c-pink) 14%, transparent);
}
[data-accent='event'] {
  --accent: #6c3a83;
  --accent-bg: color-mix(in srgb, var(--c-purple) 15%, transparent);
}
[data-accent='activity'] {
  --accent: #1c7a2d;
  --accent-bg: color-mix(in srgb, var(--c-green) 16%, transparent);
}
[data-accent='tier'] {
  --accent: #8a6200;
  --accent-bg: color-mix(in srgb, var(--c-yellow) 30%, transparent);
}
[data-accent='info'] {
  --accent: #a35a06;
  --accent-bg: color-mix(in srgb, var(--c-orange) 18%, transparent);
}
/*
 * Cabang. Warna keenam - merah logo - dan ia ADA dipakai (`data-accent="outlet"`
 * di beranda, dan ikon `pin` di daftar informasi) tapi tidak pernah punya
 * nilai: ia jatuh ke bawaan `[data-accent]`, yaitu warna aksi app.
 *
 * Selama warna aksi masih biru, kesalahan itu tidak kelihatan - biru kebetulan
 * berbeda dari kelima warna lainnya. Begitu warna aksi jadi hijau, "Cabang"
 * dan "Aktivitas terakhir" mendapat pelat yang persis sama, dan bagian yang
 * seharusnya enam warna tinggal lima.
 */
[data-accent='outlet'] {
  --accent: #b8262b;
  --accent-bg: color-mix(in srgb, var(--c-red) 14%, transparent);
}
[data-theme='dark'] [data-accent='promo'] {
  --accent: #f2698f;
}
[data-theme='dark'] [data-accent='event'] {
  --accent: #bd93d1;
}
[data-theme='dark'] [data-accent='activity'] {
  --accent: var(--c-green);
}
[data-theme='dark'] [data-accent='tier'] {
  --accent: var(--c-yellow);
}
[data-theme='dark'] [data-accent='info'] {
  --accent: var(--c-orange);
}
[data-theme='dark'] [data-accent='outlet'] {
  --accent: #f2726f;
}

.section-mark {
  flex: none;
  width: var(--plate-xs);
  height: var(--plate-xs);
  border-radius: var(--r-sm);
  display: grid;
  place-items: center;
  background: var(--accent-bg);
  color: var(--accent);
}
.section-mark svg {
  width: 16px;
  height: 16px;
}

/* ---------------- Cards ---------------- */

.card {
  background: var(--surface);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  box-shadow: var(--lift);
}
.card + .card {
  margin-top: var(--sp-4);
}
.card + .section-head {
  margin-top: var(--gap-section);
}
.card-head,
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
}
/* Centred, not baseline: the heading carries an icon plate taller than its own
   text, and on a baseline the plate hangs below the "Lihat semua" beside it. */
.section-head {
  align-items: center;
  margin: var(--gap-section) 2px var(--sp-3);
}
.card-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
}
/*
 * Judul BAGIAN lebih besar daripada judul KARTU, dan itu perbedaan yang harus
 * ada: keduanya memakai kelas yang sama, tapi yang satu memimpin sepotong
 * layar yang berisi banyak kartu sementara yang lain cuma memimpin isi satu
 * kartu. Sama besar berarti tidak ada susunan sama sekali - cuma tumpukan
 * judul setinggi 16px sejauh mata menggulir.
 */
.section-head .card-title {
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  /* Poppins melebar di ukuran besar; tanpa ini judul 20px terbaca lebih longgar
     daripada teks di bawahnya, bukan lebih tegas. */
  letter-spacing: -0.015em;
}
.card-meta {
  font-size: var(--fs-caption);
  color: var(--muted);
}
.link-btn {
  background: none;
  border: none;
  /* Sasaran sentuh, bukan hiasan. Teksnya cuma setinggi dua baris kecil -
     seperempat lebar jempol - dan "Lihat semua" duduk di pojok kanan tempat
     jempol paling sering meleset. Padding melebarkan area yang bisa ditekan;
     margin negatif menariknya kembali persis sebesar itu, jadi tidak ada satu
     piksel pun tata letak yang bergeser. Keduanya WAJIB memakai tangga jarak
     yang sama, atau tautan ini bergeser dari tempatnya. */
  padding: var(--sp-3) var(--sp-2);
  margin: calc(-1 * var(--sp-3)) calc(-1 * var(--sp-2));
  color: var(--brand-600);
  /*
   * Satu tangga DI BAWAH teks isi, bukan sama dengannya.
   *
   * Dulu ia 14px tebal - seukuran teks isi - dan duduk sebaris dengan judul
   * bagian setinggi 20px. Dua benda sebaris dengan bobot yang sama berarti
   * tidak ada yang memimpin baris itu, dan yang kalah selalu judulnya, karena
   * yang satunya berwarna. Judul yang menyebut ISI bagian harus menang atas
   * tautan yang cuma menyebut "ada lagi".
   */
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  cursor: pointer;
}
.muted-text {
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-md);
  color: var(--ink-2);
}

/* ---------------- Progress track ---------------- */
/*
 * Used twice: on the member card and on the tier page. Isiannya WARNA LOGO -
 * enam warna yang mengalir ke kanan, sama untuk semua tier dan kedua mode.
 * Warna tier tidak menyentuh batang; ia tinggal di lencana, medali, dan cahaya
 * kartu.
 *
 * Sebuah PITA RATA, bukan kisi titik dan bukan butiran WebGL. Keduanya pernah
 * dicoba di sini dan keduanya sama-sama kalah oleh satu hal yang tidak bisa
 * ditawar pada batang setinggi 10px: apa pun yang memecah isian menjadi
 * satuan-satuan kecil membuat batangnya terbaca sebagai DERAU, bukan sebagai
 * panjang. Yang harus dibaca dari benda ini cuma satu, seberapa jauh ujungnya
 * sampai, dan pita rata yang mengatakannya paling jelas.
 */
.track {
  display: block;
  height: 10px;
  border-radius: var(--r-full);
  /* `background-color`, BUKAN shorthand `background`: lapisan latar apa pun
     yang kelak dipasang di sini akan terhapus diam-diam oleh aturan lain yang
     cuma bermaksud menimpa warnanya - dan memang ada satu, `.wallet-goal
     .track`. */
  background-color: color-mix(in srgb, var(--ink) 9%, transparent);
  overflow: hidden;
}
.track-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: var(--r-full);
  /*
   * Enam warna logo, urut seperti hurufnya - lalu MERAH LAGI di ujung.
   *
   * Perhentian ketujuh itu bukan warna ketujuh: ia salinan yang pertama, dan
   * tanpanya ubin gradien ini tidak menyambung dengan salinan berikutnya di
   * sebelah kanannya. Yang terlihat kalau ia dihapus adalah garis jahitan
   * ungu-ke-merah yang menyapu batang tiap siklus, dan garis itu jauh lebih
   * terbaca daripada kelihatannya.
   */
  background-image: linear-gradient(
    90deg,
    var(--c-red),
    var(--c-orange),
    var(--c-yellow),
    var(--c-green),
    var(--c-pink),
    var(--c-purple),
    var(--c-red)
  );
  /*
   * Lebar ubinnya dalam PIKSEL, bukan persen, dan itu yang membuat semua batang
   * di app ini bergerak dengan kecepatan yang sama.
   *
   * Ubin berukuran persen ikut melar bersama isiannya: bilah beranda yang lebar
   * akan menghanyutkan warnanya jauh lebih cepat daripada dua batang jalur yang
   * sempit di halaman tier, padahal ketiganya diberi durasi yang sama persis.
   * Dikunci 240px, satu siklus 5 detik berarti 48 piksel per detik di mana pun -
   * setempo dengan kilau lencana tier (51 px/detik) yang duduk di kartu yang
   * sama.
   *
   * Yang harus disamakan antar komponen adalah KECEPATAN, bukan durasi. Dua
   * benda selebar berbeda yang diberi durasi sama tidak akan pernah terasa
   * sinkron.
   */
  background-size: 240px 100%;
  background-repeat: repeat-x;
  animation: track-rainbow 5s linear infinite;
  transition: width 0.5s var(--ease);
}
/*
 * Arahnya bukan pilihan estetika: kiri ke kanan adalah arah menuju sasaran.
 * Warna yang mengalir ke sana terbaca sebagai "sedang menuju", sementara arah
 * sebaliknya - atau bolak-balik - akan terbaca sebagai gelisah.
 *
 * 0 sampai satu ubin penuh, jadi bingkai terakhir sama persis dengan yang
 * pertama dan pengulangannya tidak punya sambungan.
 *
 * SATU HAL YANG PERLU DIKETAHUI TENTANG PILIHAN INI: gerak berulang di dalam
 * bar kemajuan adalah bahasa baku untuk "masih memuat, angkanya belum final".
 * Yang menahannya di sini dua hal - panjang isiannya TIDAK ikut bergerak (hanya
 * warna di dalamnya), dan kedua ujung bar punya angka tertulis yang jelas
 * (Rp940.000 / Rp1.400.000). Kalau suatu saat bar ini dipakai untuk sesuatu
 * yang benar-benar sedang dihitung, animasi ini harus dilepas dari sana - dua
 * arti dalam satu bentuk adalah bentuk yang berbohong.
 */
@keyframes track-rainbow {
  from {
    background-position: 0 0;
  }
  to {
    background-position: 240px 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  /* Warnanya tetap enam; yang berhenti cuma alirannya. Batang yang dibekukan di
     bingkai pertama masih mengatakan hal yang sama. */
  .track-fill {
    animation: none;
    transition: none;
  }
}

/* ---------------- QR card ---------------- */

.card-qr {
  text-align: center;
  padding: var(--sp-6) var(--sp-4) var(--sp-5);
}
/*
 * The plate stays white in BOTH themes. An inverted QR - light modules on dark -
 * is refused outright by many supermarket scanners, and this code has one job.
 */
.qr-plate {
  display: inline-block;
  background: #fff;
  padding: var(--sp-3);
  border-radius: var(--r-md);
  line-height: 0;
  box-shadow: 0 2px 10px rgba(24, 39, 75, 0.1);
}
.qr {
  width: min(62vw, 250px);
  height: auto;
  display: block;
}
.qr-bg {
  fill: #ffffff;
}
.qr-fg {
  fill: #000000;
}
.qr-name {
  margin: var(--sp-4) 0 2px;
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
}
.qr-number {
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-base);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.card-note {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  background: var(--brand-050);
  box-shadow: none;
}
.card-note svg {
  flex: none;
  width: 20px;
  height: 20px;
  color: var(--brand-600);
  margin-top: 1px;
}
.card-note p {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--ink-2);
  line-height: 1.5;
}

/* ---------------- Segmented ---------------- */

.segmented {
  display: flex;
  gap: var(--sp-1);
  padding: var(--sp-1);
  background: var(--surface-2);
  border-radius: var(--r-md);
  margin-bottom: var(--sp-4);
}
.seg-btn {
  flex: 1;
  min-height: 42px;
  border: none;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-2);
  font-size: var(--fs-base);
  font-weight: var(--fw-medium);
  cursor: pointer;
  transition: background 0.14s var(--ease), color 0.14s var(--ease);
}
/* Yang aktif naik satu tangga bobot, yang tidak aktif tinggal di 500.
   Aman dari geseran tata letak karena tiap tombol `flex: 1` - lebarnya
   diputuskan kontrolnya, bukan panjang labelnya. Ini TIDAK dilakukan pada bilah
   tab bawah: di sana lebar kolom juga tetap, tapi labelnya rata tengah, jadi
   huruf yang menebal akan terlihat "melebar" sendiri saat tab berpindah. */
.seg-btn.is-active {
  background: var(--seg-active);
  color: var(--brand-600);
  font-weight: var(--fw-semibold);
  box-shadow: var(--lift);
}

/* ---------------- Tampilan: terang / gelap / ikut HP ---------------- */
/*
 * Kontrol yang sudah dipakai di Riwayat, Event dan Voucher, dipakai lagi di
 * sini. Pilihan tema tidak butuh komponen baru - yang dibutuhkan orang justru
 * bentuk yang sudah mereka kenali dari layar sebelah.
 */
.theme-card .segmented {
  margin-bottom: var(--sp-3);
}
.theme-hint {
  margin: 0;
}

/* ---------------- Lists ---------------- */

.list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.row {
  background: var(--surface);
  border-radius: var(--r-md);
  padding: var(--sp-4) var(--sp-4);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  box-shadow: var(--lift);
}
.row-icon {
  flex: none;
  width: var(--plate-md);
  height: var(--plate-md);
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: var(--brand-050);
  color: var(--brand-600);
}
.row-icon svg {
  width: 20px;
  height: 20px;
}
.row-icon.is-out {
  background: color-mix(in srgb, var(--c-pink) 14%, transparent);
  color: var(--c-pink);
}
/* The branch photo, when the app has one. A row that shows the PLACE is worth
   more than a fourth identical arrow - and it is the same picture the member
   saw on the branch card, so the two read as the same branch. */
.row-icon.is-photo {
  background: var(--surface-2);
  overflow: hidden;
}
.row-icon.is-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.row-main {
  min-width: 0;
  flex: 1;
}
.row-title {
  margin: 0;
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row-sub {
  margin: 1px 0 0;
  font-size: var(--fs-caption);
  color: var(--muted);
  /* One line, always. A long outlet name wrapping to three lines turns a tidy
     list into a wall and pushes the amount out of alignment. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row-amount {
  flex: none;
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}
/*
 * Poin masuk. `--brand-600`, bukan hijau logo apa adanya - hijau logo di atas
 * baris putih hanya 2.6:1, dan angka inilah satu-satunya hal yang dicari orang
 * saat membuka Riwayat. Angka yang paling dicari tidak boleh jadi angka yang
 * paling sulit dibaca.
 */
.row-amount.is-in {
  color: var(--brand-600);
}
.row-amount.is-out {
  color: var(--ink-2);
}

/*
 * Pesan "belum ada apa-apa". Satu kelas, sepuluh tempat - dari layar Voucher
 * yang kosong seluruhnya sampai satu baris di dalam kartu harga - jadi ia
 * tidak boleh jadi panel: panel sebesar itu di dalam kartu harga terbaca
 * sebagai kesalahan, bukan sebagai kabar.
 *
 * Yang bisa diberikan ke semuanya sekaligus dua hal: LEBAR BACA yang dibatasi,
 * supaya kalimatnya tidak melintang selebar layar, dan `text-wrap: balance`,
 * supaya kalimat dua baris tidak menyisakan satu kata sendirian di baris
 * kedua. Peramban yang belum mengenal `balance` mengabaikannya dan mendapat
 * perilaku lama - tidak ada yang rusak.
 */
.empty {
  margin: var(--sp-8) auto;
  max-width: 34ch;
  text-align: center;
  text-wrap: balance;
  color: var(--muted);
  font-size: var(--fs-md);
  line-height: 1.55;
}

/* ---------------- Detail list ---------------- */

.detail {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-3) var(--sp-4);
  align-items: baseline;
}
.detail dt {
  font-size: var(--fs-base);
  color: var(--muted);
}
.detail dd {
  margin: 0;
  font-size: var(--fs-base);
  font-weight: var(--fw-medium);
  text-align: right;
}

/* ---------------- Tab bar ---------------- */

.tabbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  display: flex;
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom);
}
.tab-btn {
  flex: 1;
  min-height: var(--tabbar-h);
  border: none;
  background: none;
  cursor: pointer;
  color: var(--muted);
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  transition: color 0.14s var(--ease);
}
/* The plate is what moves, not the icon: a pill sliding in behind the active
   tab marks the place far more clearly than a colour change, and it is one
   element rather than an underline that has to be positioned. */
.tab-icon {
  display: grid;
  place-items: center;
  width: 46px;
  height: 30px;
  border-radius: var(--r-full);
  background: transparent;
  transition: background 0.16s var(--ease);
}
.tab-icon svg {
  width: 23px;
  height: 23px;
}
.tab-btn.is-active {
  color: var(--brand-600);
}
.tab-btn.is-active .tab-icon {
  background: var(--brand-050);
}
.tab-btn:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: -5px;
}
@media (prefers-reduced-motion: reduce) {
  .tab-icon {
    transition: none;
  }
}

/*
 * The card tab stands proud of the bar.
 *
 * It is the one tab pressed under pressure - at the counter, one-handed, with
 * a queue behind - so it is the middle of five and the only one a thumb can
 * find without looking. The disc is drawn by a pseudo-element rather than by
 * resizing `.tab-icon`, so the LABEL stays on the same line as the other four:
 * a lifted button that also drags its caption up breaks the row it belongs to.
 */
.tab-btn-hero .tab-icon {
  position: relative;
  background: none;
  /* The lift is on the icon box, not on the disc: the disc, the glyph and the
     press animation then move as one thing, and the LABEL - a sibling - stays
     on the same line as the other four. A lifted button that drags its caption
     up with it breaks the row it belongs to. */
  transform: translateY(-15px);
  transition: transform 0.12s var(--ease);
}
.tab-btn-hero .tab-icon::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 54px;
  height: 54px;
  margin: -27px 0 0 -27px;
  border-radius: var(--r-lg);
  background: linear-gradient(150deg, var(--brand-500), var(--brand-deep));
  box-shadow: 0 6px 18px rgba(20, 103, 42, 0.34);
  transition: box-shadow 0.16s var(--ease);
}
.tab-btn-hero .tab-icon svg {
  position: relative;
  width: 25px;
  height: 25px;
  color: #fff;
}
/* Active state is a ring, not a colour swap: the disc is already the brand
   blue, so darkening it would read as "disabled" rather than as "you are here". */
.tab-btn-hero.is-active .tab-icon::before {
  box-shadow: 0 6px 20px rgba(20, 103, 42, 0.44), 0 0 0 4px var(--brand-050);
}
.tab-btn-hero:active .tab-icon {
  transform: translateY(-15px) scale(0.94);
}
@media (prefers-reduced-motion: reduce) {
  .tab-btn-hero .tab-icon,
  .tab-btn-hero .tab-icon::before {
    transition: none;
  }
}

/* ---------------- Event ---------------- */
/*
 * Posters are the reason this section exists, so the image leads and the text
 * follows. Everything here is sized around a 4:3 crop because that is what a
 * portrait Instagram poster survives being cropped to without losing its title.
 */

/* Horizontal rail on the home screen. Peeking the next card by ~24px is what
   tells a thumb there is more to the right without an arrow or a dot row. */
.rail {
  display: flex;
  gap: var(--sp-3);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* Bleeds to the screen edges so the first card lines up with the cards above
     it while the last one can still slide fully into view. The bottom padding
     is room for the cards' shadow, which `overflow-x: auto` would otherwise
     clip; the negative margin takes it back out of the layout so the gap to
     the next section is the same `--gap-section` as everywhere else. */
  margin: 0 calc(-1 * var(--sp-4)) calc(-1 * var(--sp-2));
  padding: 2px var(--sp-4) var(--sp-3);
  /* Without this the snap points sit at the SCROLLPORT edge, so the browser
     scrolls 16px on load to snap the first card flush left - silently cancelling
     the padding above and leaving the rail misaligned with every other card on
     the screen. scroll-padding moves the snap line to the padding edge. */
  scroll-padding: 0 16px;
}
.rail::-webkit-scrollbar {
  display: none;
}
.rail .event-card {
  flex: 0 0 min(78%, 280px);
  scroll-snap-align: start;
}

.event-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.event-card {
  display: block;
  width: 100%;
  padding: 0;
  border: none;
  text-align: left;
  background: var(--surface);
  border-radius: var(--r-lg);
  box-shadow: var(--lift);
  overflow: hidden;
  cursor: pointer;
  font-family: inherit;
  color: inherit;
  transition: transform 0.12s var(--ease);
}
.event-card:active {
  transform: scale(0.985);
}
.event-card:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

.event-thumb {
  position: relative;
  aspect-ratio: 4 / 3;
  background: var(--surface-2);
  overflow: hidden;
}
.event-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /*
   * Potongannya dari ATAS, bukan dari tengah.
   *
   * Poster event dan promo hampir selalu tegak, dan yang ada di puncaknya selalu
   * hal yang sama: logo, judul acara, kalimat utama promo. Kotak 4:3 ini cuma
   * menampilkan 60% tinggi poster tegak - diambil dari tengah, yang terpotong
   * justru judulnya, dan yang tersisa di kartu adalah separuh huruf yang
   * terpenggal melintang. Diambil dari atas, yang hilang bagian bawah poster
   * (ketentuan, alamat, akun media sosial) - bagian yang memang paling pantas
   * hilang di ubin selebar 280px, dan yang tetap utuh di halaman rincian.
   *
   * Untuk poster MELEBAR aturan ini tidak berpengaruh sama sekali: `cover`
   * memotong lebarnya, bukan tingginya.
   */
  object-position: center top;
  display: block;
}
/*
 * No poster is a normal state, not a broken one: a coloured panel with the
 * date reads as deliberate, a grey box with a broken-image icon does not.
 *
 * Pink ke ungu, BUKAN hijau merek - dan itu bukan selera. Lencana keadaan di
 * sudut panel ini hijau kalau eventnya sedang berlangsung; hijau di atas hijau
 * adalah lencana yang hilang. Sekaligus ia jadi satu bahasa dengan dua tempat
 * lain yang juga berarti "tidak ada gambarnya": `.voucher-thumb-empty` dan
 * kartu ajakan booking.
 */
.event-thumb-empty {
  display: grid;
  place-items: center;
  background: linear-gradient(140deg, var(--c-pink), var(--c-purple));
  color: #fff;
}
/*
 * A CLASS, not `.event-thumb-empty span`. The badge in the corner is a span in
 * this same box, so the descendant selector set it to 26px too - on every card
 * with no poster the badge blew up to two and a half times its size and ran
 * straight off the card. The date is the only span here that wants to be big.
 */
.event-thumb-date {
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  letter-spacing: -0.01em;
}

/* Bentuk netralnya - dipakai event yang sudah lewat - memakai cakram yang sama
   dengan tombol kembali: pelat bertepi dengan bayangan, mengambang di atas
   poster. Dulu ia putih mati dengan teks `--ink`, yang di mode gelap berarti
   teks nyaris putih di atas keping putih. */
.event-badge {
  position: absolute;
  top: 10px;
  left: 10px;
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--overlay-line);
  border-radius: var(--r-full);
  background: var(--overlay-bg);
  color: var(--ink);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.02em;
  box-shadow: var(--lift);
  backdrop-filter: blur(6px);
}
/*
 * PELAT PUCAT, TINTA BERWARNA - dan itu berlaku untuk semua keadaan.
 *
 * Sebelum ini "Sedang berlaku" adalah pelat hijau logo pekat bertinta hijau tua.
 * Kontras hurufnya sendiri lolos (5,6:1), dan justru itu yang menipu: yang
 * hilang bukan tulisannya, melainkan LENCANANYA. Poster promo FRUITCITY hampir
 * seluruhnya warna jenuh berterang sedang - oranye, merah, kuning - dan pelat
 * hijau jenuh berterang sedang di atasnya tidak terbaca sebagai benda yang
 * ditempelkan di atas gambar, melainkan sebagai satu bagian lagi dari posternya.
 *
 * Yang memisahkan sebuah benda dari gambar APA PUN di belakangnya bukan warna,
 * melainkan terang: pelat yang jauh lebih terang daripada isi poster - di mode
 * gelap, jauh lebih gelap - selalu menang, dan warna keadaannya pindah ke tinta,
 * tempat warna memang dibaca dan bukan ditumpuk.
 *
 * Bentuknya bukan bentuk baru: lencana yang sama di halaman rincian sudah
 * memakainya sejak awal. Yang berubah, kartu dan rincian sekarang memakai
 * lencana yang SAMA - bukan dua lencana berbeda yang kebetulan bertulisan sama.
 */
.event-badge[data-state='ONGOING'] {
  background: var(--brand-050);
  border-color: color-mix(in srgb, var(--brand-600) 20%, transparent);
  color: var(--brand-600);
}
/* Kuning tidak punya tangga tintanya sendiri di app ini - yang dipakai #7a5800,
   amber yang sama dengan lencana "Baru masuk" di daftar booking (6,0:1 di atas
   pelatnya). Kuning logo apa adanya tidak pernah boleh jadi tinta di mode
   terang: 1,2:1 di atas pelat pucat yang sama. */
.event-badge[data-state='UPCOMING'] {
  background: #fff6d0;
  border-color: color-mix(in srgb, #7a5800 20%, transparent);
  color: #7a5800;
}
[data-theme='dark'] .event-badge[data-state='UPCOMING'] {
  background: #33290c;
  border-color: color-mix(in srgb, var(--c-yellow) 24%, transparent);
  color: var(--c-yellow);
}

.event-body {
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
}
.event-title {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
  line-height: 1.35;
  /* Two lines maximum: a long title must not push the date out of the card. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.event-meta {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-caption);
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------------- Rincian event ---------------- */

.detail-view {
  position: fixed;
  inset: 0;
  z-index: 45;
  background: var(--canvas);
  background-image: var(--canvas-wash);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/*
 * Kanvas confetti - dipasang app.js saat rincian promo dibuka, lalu dibuang
 * sendiri setelah 3 detik.
 *
 * `fixed`, bukan `absolute`: alur gulirnya milik `.detail-view`, dan kanvas yang
 * ikut tergulir akan meninggalkan kertasnya di atas layar begitu jempol
 * menyentuh. Yang jatuh harus jatuh di LAYAR, bukan di halaman.
 *
 * `z-index: 5` - di atas panah kembali (4) dan lembar isi (2). Kertas yang lewat
 * DI BELAKANG tombol terbaca sebagai gambar latar; yang lewat di depannya
 * terbaca sebagai sesuatu yang sedang terjadi di ruangan ini.
 */
.confetti-layer {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 5;
}

/*
 * Panah kembali. Satu bentuk untuk dua pekerjaan: menutup layar dorong, dan -
 * dengan `data-back` di HTML - pulang ke Beranda dari keempat tab lainnya. Ia
 * `fixed` supaya posisinya sama persis di setiap layar; jempol yang sudah tahu
 * di mana letaknya tidak perlu mencarinya lagi setelah menggulir.
 *
 * SATU CAKRAM, tanpa bilah apa pun di belakangnya. Bilah selebar layar sempat
 * dicoba - supaya isi yang lewat di bawah panah tidak terlihat terpotong - dan
 * ditolak: yang dibayar sebuah pita putih permanen di puncak setiap layar
 * rincian, dan yang dibeli cuma kerapian di satu momen gulir. Cakram bertepi
 * yang pekat 92% dengan bayangan sudah cukup memisahkan dirinya dari apa pun
 * yang lewat; itu juga cara app foto dan app peta melakukannya.
 *
 * `z-index: 4` menaruhnya di atas lembar isi (2) - panah yang hilang di belakang
 * isinya adalah panah yang hilang justru saat paling dicari.
 */
.detail-back {
  position: fixed;
  top: calc(var(--back-x) + env(safe-area-inset-top));
  left: var(--back-x);
  z-index: 4;
  width: var(--back-size);
  height: var(--back-size);
  border: 1px solid var(--overlay-line);
  border-radius: var(--r-full);
  background: var(--overlay-bg);
  color: var(--ink);
  display: grid;
  place-items: center;
  cursor: pointer;
  box-shadow: var(--lift);
  backdrop-filter: blur(8px);
}
.detail-back:active {
  transform: scale(0.93);
}
.detail-back:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
.detail-back svg {
  width: 22px;
  height: 22px;
}
/*
 * Poster rincian: 4:3, SAMA dengan ubin di kartunya.
 *
 * Dulu ia 4:5 dengan alasan yang masuk akal sendiri - di layar rincian poster
 * adalah isinya, jadi artwork tegak pantas diberi ruang. Yang tidak dihitung:
 * satu berkas poster harus melayani KEDUA bentuk itu, dan tidak ada satu pun
 * bentuk yang selamat di dua rasio sekaligus. Yang diunggah HO selalu jadi salah
 * di salah satu tempat.
 *
 * Disamakan, semuanya menghilang sekaligus: satu berkas per promo, satu angka
 * untuk diingat orang yang membuatnya (1440x1080), dan yang terlihat di kartu
 * persis yang terlihat sesudah diketuk - tidak ada lagi bagian poster yang
 * muncul entah dari mana di layar berikutnya.
 *
 * Yang dikorbankan: poster TEGAK kini terpotong di sini, bukan cuma di kartu.
 * Itu pertukaran yang disengaja - dan `.detail-poster--tall` di bawah adalah
 * pintu keluarnya untuk layar yang posternya memang tegak.
 */
.detail-poster {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  /* Dari atas, sama dengan ubin kartu: yang ada di puncak poster selalu logo dan
     judul, yang di dasarnya selalu ketentuan dan akun media sosial. */
  object-position: center top;
  display: block;
  background: var(--surface-2);
}
/*
 * Kekecualian: rincian EVENT.
 *
 * Poster event FRUITCITY dibuat tegak - pipeline posternya sendiri menskalakan
 * berdasarkan TINGGI - dan di dalamnya ada hal yang tidak ada di tempat lain
 * mana pun di app: narahubung, biaya registrasi, dan susunan acara. Memotongnya
 * jadi 4:3 berarti membuang nomor telepon yang mungkin satu-satunya jalan
 * mendaftar.
 *
 * Begitu poster event ikut dibuat 4:3, kelas ini tinggal dilepas dari
 * `#detail-poster` di index.html.
 */
.detail-poster--tall {
  aspect-ratio: 4 / 5;
}
/*
 * Slideshow foto di halaman cabang: foto cabang lalu foto-foto mallnya.
 *
 * RASIONYA DIKUNCI DI SINI, 4:3 - angka yang sama dengan `.detail-poster` di
 * atas dan dengan ubin di daftar Cabang. Foto mall datang dari ponsel siapa pun
 * yang kebetulan ada di sana: ada yang tegak, ada yang panorama, dan trek yang
 * tingginya mengikuti gambar akan melompat setiap kali slide berikutnya termuat
 * - persis pada detik jempol sedang menggeser.
 *
 * Titiknya memakai `.banner-dot` apa adanya, bukan salinannya. Dua deret titik
 * yang bentuknya berbeda di app yang sama terbaca sebagai dua komponen, padahal
 * gerakannya satu.
 */
.detail-shots {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--surface-2);
}
.detail-shots-track {
  display: flex;
  height: 100%;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.detail-shots-track::-webkit-scrollbar {
  display: none;
}
.detail-shot {
  flex: 0 0 100%;
  /* `always`: satu geseran cepat tidak boleh melewati dua foto sekaligus. */
  scroll-snap-align: start;
  scroll-snap-stop: always;
  height: 100%;
}
.detail-shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Dari atas, sama dengan ubin kartu: pada foto etalase yang penting ada di
     bagian atas bingkai - papan namanya, bukan lantainya. */
  object-position: center top;
  display: block;
}
/* Gradien setipis jalur titiknya, dan HANYA kalau ada titiknya: tanpa ini titik
   putih hilang di atas foto atrium yang terang. */
.detail-shots.has-dots::after {
  content: '';
  position: absolute;
  inset: auto 0 0;
  height: 44px;
  background: linear-gradient(to top, rgba(8, 14, 22, 0.45), rgba(8, 14, 22, 0));
  pointer-events: none;
}
.detail-shots-dots {
  position: absolute;
  left: 0;
  right: 0;
  /* Lebih tinggi daripada titik banner: lembar isi halaman cabang naik menutupi
     dasar foto (`.detail-sheet` bermargin -sp-6), dan titik di 9px akan
     tertimbun olehnya. */
  bottom: calc(var(--sp-6) + 9px);
  z-index: 1;
  display: flex;
  justify-content: center;
  gap: var(--sp-2);
}
/* A pushed screen covers the tab bar, so its sheet is the last thing on the
   screen and has to clear the home indicator by itself - the tab bar used to do
   that for Riwayat and Event while they were still tabs. */
.detail-view .sheet {
  padding-bottom: calc(40px + env(safe-area-inset-bottom));
}
.detail-sheet {
  margin-top: calc(-1 * var(--sp-6));
  min-height: 46vh;
}
.detail-title {
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  line-height: 1.28;
  letter-spacing: -0.01em;
}
.detail-meta {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-base);
  color: var(--muted);
}
/*
 * Wadah, bukan paragraf. Isinya digambar `renderArticle()` - judul kecil,
 * paragraf, daftar bertanda, daftar bernomor - jadi ukuran huruf dan warnanya
 * datang dari `.article-p` / `.article-list`, sama persis dengan halaman
 * Informasi. Yang tersisa di sini cuma jarak ke benda di atasnya.
 *
 * `white-space: pre-line` sengaja DILEPAS: dulu ia yang membuat baris baru
 * kelihatan, dan ia pula yang membuat teks yang ditulis rapi di dashboard
 * mendarat sebagai satu blok tanpa bentuk - tanda hubung yang tidak pernah jadi
 * butir, "1." yang tidak pernah jadi langkah, dan baris judul yang seukuran
 * kalimat biasa.
 */
.detail-body {
  margin: var(--sp-4) 0 0;
}
/* Paragraf terakhir tidak menyeret jaraknya sendiri ke tepi kotak: kotak
   Ketentuan dan tombol di bawahnya punya jaraknya sendiri. */
.detail-body > :last-child {
  margin-bottom: 0;
}
/*
 * Lencana yang sama, cuma tidak mengambang lagi: di sini ia duduk di lembar
 * isi, bukan di atas poster, jadi bayangan dan blur yang tugasnya memisahkan
 * lencana dari gambar tidak punya pekerjaan apa pun - yang tersisa cuma
 * tepinya. Warnanya TIDAK ditulis ulang di sini: dulu baris ini memasang pelat
 * hijau pucat untuk semua keadaan, jadi promo yang belum mulai pun mendapat
 * lencana hijau di halaman rincian sementara di kartunya ia kuning.
 */
.detail-view .event-badge {
  position: static;
  display: inline-block;
  box-shadow: none;
  backdrop-filter: none;
}

/* ---------------- Voucher ---------------- */

/* A voucher card is an event card with a ticket notch and an action row: same
   vocabulary, one extra idea, so the two never look like different apps. */
.voucher-card {
  position: relative;
  background: var(--surface);
  border-radius: var(--r-lg);
  box-shadow: var(--lift);
  overflow: hidden;
}
.voucher-card.is-spent {
  /* Used and expired vouchers stay visible - a member looks for proof they
     redeemed something - but they must never compete with a live one. */
  opacity: 0.62;
}
.voucher-top {
  display: flex;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-4);
}
.voucher-thumb {
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: var(--r-md);
  object-fit: cover;
  background: var(--surface-2);
}
.voucher-thumb-empty {
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: linear-gradient(140deg, var(--c-pink), var(--c-purple));
  color: #fff;
}
.voucher-thumb-empty svg {
  width: 24px;
  height: 24px;
}
.voucher-info {
  min-width: 0;
  flex: 1;
}
.voucher-title {
  margin: 0;
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  line-height: 1.35;
}
.voucher-sub {
  margin: 3px 0 0;
  font-size: var(--fs-caption);
  color: var(--muted);
}
.voucher-state {
  display: inline-block;
  margin-top: var(--sp-2);
  padding: 3px var(--sp-2);
  border-radius: var(--r-full);
  font-size: var(--fs-badge);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.03em;
  background: var(--surface-2);
  color: var(--muted);
}
/* Tidak ada pasangan `[data-theme='dark']` di sini lagi: `--brand-600` sudah
   berbalik sendiri per mode, jadi satu aturan menutup keduanya. */
.voucher-state[data-state='CLAIMED'] {
  background: var(--brand-050);
  color: var(--brand-600);
}

/* The notch: two half-circles biting into the card edges, with a dashed rule
   between them. It is the one detail that makes a rectangle read as a ticket. */
.voucher-tear {
  position: relative;
  height: 1px;
  margin: 0 var(--sp-4);
  border-top: 1.5px dashed var(--line);
}
.voucher-tear::before,
.voucher-tear::after {
  content: '';
  position: absolute;
  top: -9px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--canvas);
}
.voucher-tear::before {
  left: -25px;
}
.voucher-tear::after {
  right: -25px;
}

.voucher-actions {
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
}
.voucher-actions .btn {
  min-height: 46px;
  font-size: var(--fs-md);
}
.voucher-reason {
  margin: 0;
  font-size: var(--fs-caption);
  color: var(--muted);
  text-align: center;
}

/* ---------------- Layar kode di kasir ---------------- */

.voucher-show {
  background: linear-gradient(160deg, var(--brand-500), var(--brand-deep));
  color: #fff;
  display: grid;
  place-items: center;
}
.voucher-show-inner {
  text-align: center;
  padding: var(--sp-8) var(--sp-6) calc(var(--sp-8) + env(safe-area-inset-bottom));
}
.voucher-show-cap {
  margin: 0;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.85;
}
.voucher-show-title {
  margin: var(--sp-2) 0 var(--sp-6);
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  line-height: 1.3;
}
.voucher-code {
  margin: 0 0 var(--sp-8);
  font-size: var(--fs-code);
  font-weight: var(--fw-bold);
  /* Wide tracking because this is READ ALOUD across a counter, character by
     character, in a noisy room. */
  letter-spacing: 0.16em;
  text-indent: 0.16em;
  font-variant-numeric: tabular-nums;
}
.voucher-timer {
  position: relative;
  width: 104px;
  height: 104px;
  margin: 0 auto;
  display: grid;
  place-items: center;
}
.voucher-timer svg {
  position: absolute;
  inset: 0;
  transform: rotate(-90deg);
}
.timer-track,
.timer-fill {
  fill: none;
  stroke-width: 7;
}
.timer-track {
  stroke: rgba(255, 255, 255, 0.25);
}
.timer-fill {
  stroke: #fff;
  stroke-linecap: round;
  /* 2*pi*44, set from JS as the countdown runs. */
  stroke-dasharray: 276.46;
  stroke-dashoffset: 0;
}
.voucher-timer span {
  position: relative;
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}
.voucher-show-note {
  margin: var(--sp-6) auto 0;
  max-width: 280px;
  font-size: var(--fs-sm);
  line-height: 1.55;
  opacity: 0.9;
}

/* ---------------- Kartu ajakan (booking) ---------------- */

/* An invitation, not a menu item: full width, one clear sentence, arrow on the
   right. Booking is rare and high-intent, so it earns a card rather than a
   seventh tab nobody would ever notice. */
.promo-card {
  width: 100%;
  padding: var(--sp-4) var(--sp-4);
  border: none;
  border-radius: var(--r-lg);
  background: linear-gradient(135deg, var(--c-pink), var(--c-purple));
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  text-align: left;
  box-shadow: var(--lift);
  transition: transform 0.12s var(--ease);
}
.promo-card:active {
  transform: scale(0.985);
}
.promo-card:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
.promo-icon {
  flex: none;
  width: var(--plate-md);
  height: var(--plate-md);
  border-radius: var(--r-md);
  background: rgba(255, 255, 255, 0.22);
  display: grid;
  place-items: center;
}
.promo-icon svg {
  width: 20px;
  height: 20px;
}
.promo-text {
  flex: 1;
  min-width: 0;
}
.promo-title {
  display: block;
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
}
.promo-sub {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-caption);
  opacity: 0.9;
}
.promo-chev {
  flex: none;
  width: 20px;
  height: 20px;
  opacity: 0.85;
}
/* An invitation card with no heading of its own is a section in itself, so it
   takes the section gap rather than the tight gap a card under a heading gets. */
.promo-card-standalone {
  margin-top: var(--gap-section);
}
/* The list of the member's own bookings, under the "ajukan" invitation. */
#booking-mine {
  margin-top: var(--sp-4);
}

/* ---------------- Form booking ---------------- */

/*
 * Any pushed screen whose title shares a row with the fixed back button in the
 * corner - sepuluh layar memakainya, dari Event sampai Informasi.
 *
 * KETERANGANNYA IKUT MENGHINDAR, dan dulu tidak. Judulnya menghindari panah
 * kembali sementara keterangan di bawahnya tetap mulai dari tepi lembar, jadi
 * dua baris yang seharusnya satu blok berdiri di dua tepi kiri yang berbeda -
 * dan keterangan itu duduk tepat di bawah cakram panah, satu-satunya tempat di
 * layar yang memang sudah ada isinya. Yang terlihat bukan "keterangan yang
 * menjorok", melainkan judul yang tampak salah tempat.
 *
 * Keterangannya diambil sebagai ADIK LANGSUNG judul, bukan lewat kelas kedua
 * yang harus ditulis di sepuluh tempat: kelas yang harus diingat adalah kelas
 * yang suatu hari lupa dipasang di layar kesebelas, dan bentuk `<h1>` disusul
 * `<p>` itu sudah sama di kesepuluhnya. Kalau suatu hari ada elemen yang
 * disisipkan di antara keduanya, JARAK INI YANG HILANG - pindahkan aturannya,
 * jangan tambahkan kelas.
 */
.hero-title-inset,
.hero-title-inset + .hero-caption {
  padding-left: var(--title-inset);
}
.field-label-gap {
  margin-top: var(--sp-4);
}
.field-optional {
  font-weight: var(--fw-regular);
  color: var(--muted);
  font-size: var(--fs-sm);
}
select.field {
  appearance: none;
  /* Room for the chevron drawn below. */
  padding-right: 40px;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 20px) 24px, calc(100% - 15px) 24px;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
textarea.field {
  min-height: 92px;
  resize: vertical;
  line-height: 1.5;
}
.booking-submit {
  margin-top: var(--sp-5);
}
.booking-disclaimer {
  margin: var(--sp-3) 0 0;
  text-align: center;
}

/* ---------------- Kartu permintaan booking ---------------- */

.booking-row {
  background: var(--surface);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  box-shadow: var(--lift);
}
.booking-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-3);
}
.booking-ref {
  margin: 0;
  font-size: var(--fs-caption);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}
.booking-package {
  margin: 2px 0 0;
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
}
.booking-status {
  flex: none;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-full);
  font-size: var(--fs-badge);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.03em;
  background: var(--surface-2);
  color: var(--muted);
}
/*
 * Tiga status, tiga warna logo, dan urutannya bercerita: kuning "baru masuk",
 * ungu "sedang ditangani", hijau "beres".
 *
 * Yang UNGU dulu memakai pelat biru yang sama dengan warna aksi app. Begitu
 * warna aksi pindah ke hijau, "sedang ditangani" dan "beres" jadi dua lencana
 * hijau yang persis sama - dua keadaan berbeda yang tidak bisa dibedakan lagi.
 */
.booking-status[data-status='NEW'] {
  background: color-mix(in srgb, var(--c-yellow) 34%, transparent);
  color: #7a5800;
}
.booking-status[data-status='CONTACTED'] {
  background: color-mix(in srgb, var(--c-purple) 15%, transparent);
  color: #6c3a83;
}
.booking-status[data-status='CONFIRMED'] {
  background: color-mix(in srgb, var(--c-green) 18%, transparent);
  color: var(--brand-600);
}
[data-theme='dark'] .booking-status[data-status='NEW'] {
  color: var(--c-yellow);
}
[data-theme='dark'] .booking-status[data-status='CONTACTED'] {
  color: #bd93d1;
}
.booking-meta {
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-sm);
  color: var(--ink-2);
  line-height: 1.6;
}
.booking-cancel {
  margin-top: var(--sp-3);
  min-height: 44px;
  font-size: var(--fs-base);
}

/* ---------------- Cabang ---------------- */
/*
 * A branch is a ROW, not a poster card. Members arrive here with a question
 * that has a text answer - where is it, what does it cost - and a stack of
 * 4:3 images would put nine of them on a screen instead of five.
 */

.outlet-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.outlet-card {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  text-align: left;
  padding: var(--sp-3) var(--sp-3);
  border: none;
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--lift);
  cursor: pointer;
  font-family: inherit;
  color: var(--ink);
  transition: transform 0.16s var(--ease);
}
.outlet-card:active {
  transform: scale(0.985);
}
.outlet-card:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
.outlet-thumb {
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface-2);
}
.outlet-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.outlet-thumb-empty {
  display: grid;
  place-items: center;
  font-size: var(--fs-xl);
  font-weight: var(--fw-semibold);
  color: var(--brand-600);
  background: var(--brand-050);
}
.outlet-body {
  flex: 1;
  min-width: 0;
}
.outlet-name {
  margin: 0;
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  line-height: 1.3;
}
.outlet-where,
.outlet-foot {
  margin: 3px 0 0;
  font-size: var(--fs-caption);
  color: var(--muted);
  /* One line each: a wrapped address turns a scannable list into a wall. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.outlet-foot {
  color: var(--brand-600);
  font-weight: var(--fw-medium);
}
.outlet-foot:empty {
  display: none;
}
.outlet-chev {
  flex: none;
  color: var(--muted);
  display: grid;
  place-items: center;
}
.outlet-chev svg {
  width: 18px;
  height: 18px;
}
.outlets-nearest {
  min-height: 46px;
  margin: var(--sp-3) 0 var(--sp-4);
  font-size: var(--fs-base);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
}
.outlets-nearest svg {
  width: 18px;
  height: 18px;
  flex: none;
}
/*
 * Menyala saat daftarnya benar-benar urut menurut jarak - warna dan bentuk yang
 * sama dengan lencana provinsi terpilih, karena keduanya menjawab pertanyaan
 * yang sama: "yang mana yang sedang aktif".
 *
 * Tanpa ini satu-satunya yang menyala di layar ini lencana provinsi, dan daftar
 * yang urut menurut jarak terbaca sebagai hasil menekan "Banten".
 *
 * `--brand-solid`, bukan `--brand-600`, alasan yang sama dengan `.btn-primary`:
 * yang kedua berbalik jadi hijau muda di mode gelap dan label putih di atasnya
 * jatuh di bawah ambang kontras.
 */
.outlets-nearest.is-active {
  background: var(--brand-solid);
  border-color: transparent;
  color: #fff;
}

/*
 * ---- SARINGAN PROVINSI ----
 *
 * Bentuk yang sama dengan halaman /lokasi di fruitcity.id: sebaris lencana yang
 * digulir mendatar, satu menyala. Yang TIDAK ikut dibawa dari website saringan
 * mereknya - di sana FRUITCITY dan FRUITCITY PARK dipisah lebih dulu, jadi
 * memilih provinsi butuh dua ketukan dan dua pertanyaan yang harus dijawab
 * berurutan. Member yang membuka layar ini punya satu pertanyaan, dan
 * pertanyaannya "di kota saya ada tidak".
 *
 * BUKAN `.segmented`: kontrol itu membagi lebarnya rata untuk dua-tiga pilihan
 * yang semuanya harus terlihat sekaligus. Sepuluh provinsi dengan nama sepanjang
 * "Sumatera Selatan" pada lebar layar yang sama akan jadi sepuluh potong teks
 * terpenggal.
 */
.outlet-filter {
  display: flex;
  gap: var(--sp-2);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* Menembus ke tepi layar, alasan yang sama dengan `.rail` di Beranda: lencana
     terakhir harus bisa masuk penuh, sementara lencana pertama tetap segaris
     dengan kotak cari di atasnya. */
  margin: 0 calc(-1 * var(--sp-4)) var(--sp-3);
  padding: 2px var(--sp-4) var(--sp-2);
  /* Tanpa ini `scrollIntoView` mendaratkan lencana tepat di tepi SCROLLPORT,
     yang diam-diam membatalkan padding di atas: mengetuk "Semua" lalu
     menggeser barisnya kembali ke kiri meninggalkan lencana pertama menempel
     ke tepi layar, tidak lagi segaris dengan kotak cari di atasnya. */
  scroll-padding: 0 var(--sp-4);
}
.outlet-filter::-webkit-scrollbar {
  display: none;
}
.outlet-chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  /* 42px, tinggi yang sama dengan `.seg-btn` - dua kontrol pilihan di app ini
     tidak perlu dua tinggi yang berbeda. */
  min-height: 42px;
  padding: 0 var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--ink-2);
  font-family: inherit;
  font-size: var(--fs-base);
  font-weight: var(--fw-medium);
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.14s var(--ease), color 0.14s var(--ease),
    border-color 0.14s var(--ease);
}
/* Angkanya keterangan, bukan label kedua: ia turun satu tangga ukuran dan
   berhenti di `--muted`, supaya yang dibaca lebih dulu tetap nama provinsinya. */
.outlet-chip-count {
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  color: var(--muted);
}
/* `--brand-solid`, bukan `--brand-600`: yang kedua adalah TINTA dan ia berbalik
   jadi hijau muda di mode gelap - dengan label putih di atasnya lencana terpilih
   jatuh ke 2,1:1. Isian merek sama di kedua mode. */
.outlet-chip.is-active {
  border-color: transparent;
  background: var(--brand-solid);
  color: #fff;
  font-weight: var(--fw-semibold);
}
.outlet-chip.is-active .outlet-chip-count {
  color: rgba(255, 255, 255, 0.78);
}
.outlet-chip:active {
  transform: scale(0.97);
}
.outlet-chip:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .outlet-chip {
    transition: none;
  }
}

/* ---------------- Cabang favorit (beranda) ---------------- */
/*
 * The one branch a member actually goes to, as a photograph. The LIST stays a
 * stack of rows - twenty-four places is a lookup - but a single branch is not
 * a lookup, it is where they take their kids on Saturday, and it earns the
 * picture the branch profile already carries.
 */

.outlet-hero {
  display: block;
  width: 100%;
  padding: 0;
  border: none;
  text-align: left;
  background: var(--surface);
  border-radius: var(--r-lg);
  box-shadow: var(--lift);
  overflow: hidden;
  cursor: pointer;
  font-family: inherit;
  color: inherit;
  transition: transform 0.12s var(--ease);
}
.outlet-hero:active {
  transform: scale(0.985);
}
.outlet-hero:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
.outlet-hero-media {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--surface-2);
  overflow: hidden;
}
.outlet-hero-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.outlet-hero-media.is-empty {
  display: grid;
  place-items: center;
  background: linear-gradient(140deg, var(--brand-500), var(--brand-deep));
}
/* Huruf air, bukan teks: satu inisial cabang setinggi separuh kartu, putih 50%
   di atas hijau. Ia sengaja di luar tangga ukuran huruf - tangga itu untuk yang
   dibaca, dan yang ini tidak dibaca siapa pun, ia mengisi kotak yang seharusnya
   berisi foto. */
.outlet-hero-media.is-empty span {
  font-size: 56px;
  font-weight: var(--fw-bold);
  color: rgba(255, 255, 255, 0.5);
}
/* The scrim is on the TEXT block, not on the image: it only has to darken the
   strip the words sit in, and a wash over the whole photo would flatten the
   picture this card exists to show. */
.outlet-hero-body {
  position: absolute;
  inset: auto 0 0;
  padding: 42px var(--sp-4) var(--sp-3);
  display: flex;
  flex-direction: column;
  gap: 2px;
  color: #fff;
  /* Three stops, not two: a straight linear fade is already transparent by the
     time it reaches the eyebrow, and branch photos are daylight shots of white
     mall interiors. */
  background: linear-gradient(
    to top,
    rgba(6, 14, 24, 0.92) 0%,
    rgba(6, 14, 24, 0.72) 46%,
    rgba(6, 14, 24, 0) 100%
  );
}
.outlet-hero-eyebrow {
  font-size: var(--fs-badge);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.85;
}
.outlet-hero-name {
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  line-height: 1.25;
  letter-spacing: -0.01em;
}
.outlet-hero-where {
  font-size: var(--fs-caption);
  opacity: 0.88;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.outlet-hero-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4) var(--sp-3);
}
.outlet-hero-facts {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--ink-2);
}
.outlet-hero-facts:empty {
  display: none;
}
.outlet-hero-more {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  margin-left: auto;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--brand-600);
}
.outlet-hero-more svg {
  width: 16px;
  height: 16px;
}

/* ---------------- Rincian cabang ---------------- */

.brand-chip {
  display: inline-block;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-full);
  background: var(--brand-050);
  color: var(--brand-600);
  font-size: var(--fs-badge);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

/* Find it, ask about it, buy for it - the three things a branch page is for,
   as three equal buttons rather than links buried in the address line. */
.action-row {
  display: flex;
  gap: var(--sp-2);
  margin: var(--sp-4) 0 var(--sp-1);
}
.action-btn {
  flex: 1;
  min-height: 62px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink-2);
  text-decoration: none;
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
}
.action-btn svg {
  width: 21px;
  height: 21px;
  color: var(--brand-600);
}
.action-btn:active {
  background: var(--brand-050);
}

/* Price list: label left, number right, with a hairline between rows so the
   eye can travel across a wide gap without losing the line. */
.price-list {
  margin-top: var(--sp-1);
}
.price-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  flex-wrap: wrap;
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--line);
}
.price-row:last-child {
  border-bottom: none;
}
/* Label 500 lawan nilai 700: yang dicari orang di daftar harga adalah
   ANGKANYA, dan selisih dua tangga bobot itulah yang membuatnya menonjol
   tanpa perlu warna. Dua-duanya 600 berarti tidak ada yang menonjol. */
.price-label {
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
}
.price-value {
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  margin-left: auto;
}
/* Companion tickets, coins and other add-ons: real prices, but not a way in,
   so they sit back half a step from the admission rows above them. */
.price-row.is-extra .price-label,
.price-row.is-extra .price-value {
  font-weight: var(--fw-medium);
  color: var(--ink-2);
}
.price-note {
  flex-basis: 100%;
  font-size: var(--fs-caption);
  color: var(--muted);
  margin-top: -2px;
}

/* ---------------- Paket acara ---------------- */
/*
 * Harga paket acara ADA DUA, dan itu yang membentuk kartunya: pesta yang sama
 * 55 ribu per anak di FRUITCITY dan 75 ribu di FRUITCITY PARK. Tiap brand
 * berdiri di bawah namanya sendiri - keping merek yang sama dengan yang dipakai
 * halaman cabang, jadi kata "FRUITCITY PARK" selalu berwujud sama di app ini.
 *
 * Yang memisahkan dua kelompok itu JARAK, bukan garis. Baris harga di dalam satu
 * brand sudah dipisahkan garis; garis ketiga di antara dua daftar bergaris
 * membuat keduanya terbaca sebagai satu tabel panjang - persis yang tidak boleh
 * terjadi di layar yang seluruh gunanya memberi tahu bahwa keduanya berbeda.
 */
.price-group + .price-group {
  margin-top: var(--sp-4);
}
.price-group .price-list {
  margin-top: var(--sp-2);
}
/* Daftar fasilitas di dalam kartu: kartunya sudah punya `padding`, jadi jarak
   bawah milik `.article` (yang ada demi lembar isi halaman Informasi) di sini
   jadi jarak kedua yang menggantung di kaki kartu. */
.card > .article {
  padding-bottom: 0;
}
.card > .article > :last-child {
  margin-bottom: 0;
}

/* ---------------- Informasi ---------------- */

.info-grid {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}
.info-tile {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  text-align: left;
  padding: var(--sp-3);
  border: none;
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--lift);
  cursor: pointer;
  font-family: inherit;
  color: var(--ink);
  transition: transform 0.16s var(--ease);
}
.info-tile:active {
  transform: scale(0.985);
}
.info-tile:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
.info-icon {
  flex: none;
  width: var(--plate-md);
  height: var(--plate-md);
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: var(--accent-bg);
  color: var(--accent);
}
.info-icon svg {
  width: 20px;
  height: 20px;
}
.info-text {
  min-width: 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.info-chev {
  flex: none;
  display: grid;
  place-items: center;
  color: var(--muted);
}
.info-chev svg {
  width: 18px;
  height: 18px;
}
.info-title {
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  line-height: 1.3;
}
.info-sub {
  font-size: var(--fs-caption);
  color: var(--muted);
  line-height: 1.35;
}
.info-sub:empty {
  display: none;
}

/* Article body. Plain text in, so the only typography here is paragraph
   rhythm, a small heading, and a bullet list. */
.article {
  padding-bottom: var(--sp-3);
}
/*
 * Tautan yang DIBUAT SENDIRI oleh iOS.
 *
 * Safari menyulap tanggal, alamat, dan nomor telepon di dalam teks biasa menjadi
 * tautan, dan pada sebagian versi ia melakukannya walau `format-detection` di
 * `<head>` sudah melarangnya. Tautan itu mewarisi warna tautan app - hijau merek
 * - jadi kalimat promo tampil dengan kata-kata hijau berserakan yang terbaca
 * seperti kesalahan.
 *
 * Yang dilepas cuma TAMPILANNYA; tautannya sendiri tidak bisa dicabut dari CSS.
 * Menekannya masih membuka Peta atau Kalender, dan itu tidak apa-apa: yang salah
 * bukan kemampuannya, melainkan teks yang berpura-pura punya arti lain.
 */
a[x-apple-data-detectors] {
  color: inherit;
  text-decoration: none;
}

.article-head {
  margin: var(--sp-5) 0 var(--sp-2);
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
  line-height: 1.35;
}
.article-head:first-child {
  margin-top: var(--sp-1);
}
.article-p {
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-md);
  line-height: 1.62;
  color: var(--ink-2);
}
/*
 * Daftar berwarna - dan warnanya HIASAN, bukan arti.
 *
 * Isi daftar ini diketik bebas oleh HO di dashboard: benefit peserta, hadiah,
 * ketentuan, langkah klaim. Tidak ada satu pun cara app mengetahui bahwa
 * "Snack" pantas ikon gelas dan "Tas spunbond" pantas ikon tas - menebaknya dari
 * kata kunci akan tepat sembilan kali lalu memasang ikon kaus kaki di sebelah
 * "uang tunai Rp500.000", dan yang satu itu yang diingat orang.
 *
 * Jadi yang diwarnai bentuk yang SUDAH pasti benar: nomor langkah dan titik
 * butir. Keduanya memang milik daftar itu, apa pun isinya. Enam warna logo
 * berputar per baris - sama dengan bahasa warna di seluruh app - dan itu yang
 * membuat daftar panjang terbaca sebagai sesuatu yang dirancang, bukan sebagai
 * dinding teks.
 */
.article-list {
  margin: 0 0 var(--sp-3);
  padding-left: 0;
  list-style: none;
}
.article-list li {
  position: relative;
  margin-bottom: var(--sp-2);
  font-size: var(--fs-md);
  line-height: 1.58;
  color: var(--ink-2);
}
/* Enam warna logo, urut seperti hurufnya, lalu mengulang. `nth-child` dan bukan
   variabel yang diwarisi: daftar berikutnya harus mulai dari merah lagi, bukan
   melanjutkan hitungan daftar sebelumnya. */
.article-list li:nth-child(6n + 1) {
  --item-color: var(--c-red);
}
.article-list li:nth-child(6n + 2) {
  --item-color: var(--c-orange);
}
.article-list li:nth-child(6n + 3) {
  --item-color: var(--c-yellow);
}
.article-list li:nth-child(6n + 4) {
  --item-color: var(--c-green);
}
.article-list li:nth-child(6n + 5) {
  --item-color: var(--c-pink);
}
.article-list li:nth-child(6n) {
  --item-color: var(--c-purple);
}

/* --- Butir berikon: ikon yang dipilih dari kalimatnya (app.js) --- */
ul.article-list li.has-icon {
  padding-left: 32px;
}
ul.article-list li.has-icon::before {
  /* Titiknya menyingkir, bukan menemani: dua penanda untuk satu butir membuat
     tepi kiri daftar terbaca bergerigi. */
  display: none;
}
.article-icon {
  position: absolute;
  left: 0;
  top: 0.06em;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  /* Campuran tinta yang sama dengan titik, dan alasannya sama: goresan kuning
     setipis 1,9px di atas kanvas putih hampir tidak ada. */
  color: color-mix(in srgb, var(--item-color) 88%, var(--ink));
}
.article-icon svg {
  width: 20px;
  height: 20px;
}

/* --- Butir biasa: titik warna --- */
ul.article-list li {
  padding-left: var(--sp-5);
}
ul.article-list li::before {
  content: '';
  position: absolute;
  left: 4px;
  /* 0.62em dari puncak baris, bukan tengah butir: butir dua baris harus punya
     titik yang sebaris dengan baris PERTAMA, seperti titik daftar sungguhan. */
  top: 0.62em;
  width: 8px;
  height: 8px;
  border-radius: var(--r-full);
  /*
   * Sedikit tinta dicampurkan, dan itu bukan sekadar "biar lebih gelap":
   * `--ink` berbalik per mode, jadi campuran yang sama menggelapkan titik di
   * mode terang dan MENERANGKANNYA di mode gelap. Satu nilai, dua arah.
   *
   * Yang menuntutnya kuning #ffdd2f: sebagai titik 8px di atas kanvas putih ia
   * nyaris hilang, sementara lima warna lainnya baik-baik saja. 12% cukup untuk
   * menyelamatkannya dan terlalu sedikit untuk mengubah rona kelimanya.
   */
  background: color-mix(in srgb, var(--item-color) 88%, var(--ink));
}

/* --- Langkah: keping bernomor --- */
ol.article-list {
  counter-reset: langkah;
}
ol.article-list li {
  counter-increment: langkah;
  padding-left: 34px;
  /* Langkah bernafas lebih lega daripada butir: yang dibaca di sini urutan
     mengerjakan sesuatu, dan langkah yang berdempetan terbaca sebagai satu
     paragraf yang kebetulan bernomor. */
  margin-bottom: var(--sp-3);
}
ol.article-list li::before {
  content: counter(langkah);
  position: absolute;
  left: 0;
  top: 0.1em;
  width: 24px;
  height: 24px;
  border-radius: var(--r-full);
  display: grid;
  place-items: center;
  font-size: var(--fs-caption);
  font-weight: var(--fw-bold);
  /*
   * Latar bertinta warna, ANGKANYA tinta biasa.
   *
   * Angka berwarna logo di atas keping sewarna adalah angka yang hilang: kuning
   * #ffdd2f di atas kuning 18% jatuh jauh di bawah 4.5:1, dan itu justru warna
   * yang paling ceria dari enamnya. Warna dipakai untuk kepingnya, keterbacaan
   * dipegang tinta - dan hasilnya sama-sama berwarna tanpa satu langkah pun
   * yang lebih sulit dibaca daripada langkah lainnya.
   */
  background: color-mix(in srgb, var(--item-color) 22%, transparent);
  color: var(--ink);
  line-height: 1;
}

.terms-box {
  margin-top: var(--sp-6);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
}

/* ---------------- Tier & benefit ---------------- */
/*
 * Dua layar, satu bahan.
 *
 * Halaman TIER bercerita tentang satu orang: tier yang dipegang sebagai judul
 * halaman, dan satu kartu kemajuan di bawahnya. Halaman TINGKATAN bercerita
 * tentang tangganya, satu tier per tab.
 *
 * Pemisahan itu yang membuat keduanya bisa punya bentuk yang benar. Sebagai
 * satu halaman, "di mana saya" dan "ada tier apa saja" berebut layar yang
 * sama, dan yang kalah selalu yang pertama - padahal itu yang dicari orang
 * yang menekan bilah tier di beranda.
 */

/* --- Header halaman tier: nama tier SEBAGAI judul halaman --- */

.tier-hero {
  padding-bottom: 44px;
}
/* Baris ini sebaris dengan panah kembali yang melayang di pojok kiri, jadi
   isinya rata kanan - satu-satunya sisi yang pasti tidak tertimpa. */
.tier-hero-top {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  min-height: 42px;
  margin-bottom: var(--sp-2);
}
.tier-hero-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  /* Sasaran sentuh, bukan hiasan: padding melebarkan area yang bisa ditekan,
     margin negatif menariknya kembali persis sebesar itu. Kedua angka WAJIB
     kembar - kalau paddingnya naik ke tangga berikutnya dan marginnya tidak,
     tautan ini bergeser dari tempatnya. */
  padding: var(--sp-3) var(--sp-2);
  margin: calc(-1 * var(--sp-3)) calc(-1 * var(--sp-2));
  border: none;
  background: none;
  /* Warna aksi, bukan `inherit`. Dulu ia mewarisi tinta putih header berwarna;
     dengan header putih ia mewarisi tinta judul, dan tautan yang sewarna judul
     tidak mengaku sebagai tautan. */
  color: var(--brand-600);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  cursor: pointer;
}
.tier-hero-link svg {
  width: 16px;
  height: 16px;
}
.tier-hero-link:active {
  opacity: 0.7;
}
.tier-hero-link:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
.tier-hero-id {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-4);
}
/*
 * Berwarna TIER sekarang, bukan pelat tembus.
 *
 * Alasan lamanya benar pada zamannya: pelat berwarna tier di atas header yang
 * juga berwarna adalah dua warna yang saling menumpulkan. Header itu sudah
 * putih, jadi alasannya gugur - dan yang tersisa cuma pelat abu-abu di halaman
 * yang seluruhnya bercerita tentang satu warna.
 */
.tier-hero-medal {
  flex: none;
  width: var(--plate-lg);
  height: var(--plate-lg);
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: var(--tier-bg, var(--surface-2));
  color: var(--tier, var(--ink-2));
}
.tier-hero-medal svg {
  width: 26px;
  height: 26px;
}
.tier-hero-text {
  min-width: 0;
}
/* Nama tier, sebesar judul layar - karena di halaman ini ia MEMANG judulnya.
   Huruf besar semua: "Silver" dan "Gold" panjangnya beda jauh, dan huruf
   besar membuat keduanya sama-sama terbaca sebagai pangkat, bukan sebagai
   kata biasa yang kebetulan diperbesar. */
.tier-hero-name {
  font-size: var(--fs-3xl);
  line-height: 1.14;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}
.tier-hero-member {
  margin: 3px 0 0;
  font-size: var(--fs-base);
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tier-hero-member:empty {
  display: none;
}
/* Tanggal periode: satu baris di bawah nama tier, dan tetap di sana. Tier
   tanpa batas waktunya adalah setengah kabar. */
.tier-hero .hero-caption {
  margin-top: var(--sp-3);
}

/* --- Kartu kemajuan --- */

.tier-standing {
  margin-bottom: var(--sp-1);
}
/* "Menuju" sebagai teks biasa, lalu tier TUJUAN sebagai lencana berwarnanya
   sendiri - susunan yang sama dengan bilah tier di beranda, supaya orang yang
   baru saja menekan bilah itu mendarat pada bentuk yang ia kenali. */
.tier-goal {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
}
.tier-goal-label {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--muted);
}
.tier-standing-line {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  line-height: 1.45;
}

/*
 * DUA batang, bukan satu.
 *
 * Kunjungan dan belanja adalah dua jalur berbeda menuju pintu yang sama. Satu
 * batang untuk keduanya memaksa memilih salah satu untuk digambar - dan jalur
 * satunya, yang mungkin justru akan lebih dulu selesai bagi orang ini, tidak
 * pernah kelihatan sama sekali.
 *
 * Yang menjaga supaya dua batang tidak terbaca sebagai dua SYARAT adalah kata
 * "atau" di antaranya, dengan garis di kiri-kanannya: itu bentuk yang sudah
 * dikenal orang sebagai "salah satu saja".
 */
/* Jaraknya biasanya datang dari "atau" di antara keduanya. Kalau salah satu
   jalur tidak punya sasaran sama sekali, pemisah itu tidak digambar - dan dua
   jalur yang berdempetan tanpa jarak terbaca sebagai satu blok. */
.tier-route + .tier-route {
  margin-top: var(--sp-4);
}
.tier-route-need {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  letter-spacing: -0.01em;
  line-height: 1.35;
}
/* Penyebutnya, di bawah batangnya sendiri. Angka kemajuan tanpa penyebut
   adalah batang yang panjangnya tidak bisa diperiksa siapa pun. */
.tier-route-meta {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-caption);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.tier-or {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: var(--sp-4) 0;
  font-size: var(--fs-caption);
  color: var(--muted);
}
.tier-or::before,
.tier-or::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--line);
}
/* Batas waktunya, dipisahkan garis: ia berlaku untuk KEDUA jalur di atasnya,
   jadi ia tidak boleh terlihat menempel pada salah satunya. */
.tier-standing-foot {
  margin: var(--sp-4) 0 0;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
  font-size: var(--fs-caption);
  color: var(--muted);
}
/* A carried-over tier looks identical to an earned one right up to the day it
   vanishes, so it is the one thing on this page that gets a coloured box. */
.tier-standing-keep {
  margin: var(--sp-3) 0 0;
  padding: var(--sp-3) var(--sp-3);
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--c-yellow) 20%, transparent);
  color: var(--ink);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  line-height: 1.5;
}
[data-theme='dark'] .tier-standing-keep {
  background: color-mix(in srgb, var(--c-orange) 16%, transparent);
}

.tier-flag {
  flex: none;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-full);
  /* 11px adalah lantai ukuran huruf di app ini, dan lantai itu hanya boleh
     disentuh oleh lencana: huruf besar tebal setinggi 11px punya tinggi huruf
     yang setara teks biasa 14px. Teks yang DIBACA tidak pernah turun ke sini. */
  font-size: var(--fs-badge);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  background: var(--brand-050);
  color: var(--brand-600);
}
.tier-flag[data-kind='next'] {
  background: var(--surface-2);
  color: var(--muted);
}

/* --- Baris keuntungan, dipakai di kedua layar --- */
/*
 * Ikon berpelat + judul + keterangan, bukan daftar bercentang.
 *
 * Bentuk daftar benar selama isinya kalimat sejenis. Begitu barisnya berbeda
 * JENIS - berapa poinnya, secepat apa, voucher apa, berlaku sampai kapan -
 * centang yang sama di depan keempatnya justru menyembunyikan bedanya, dan
 * pelat ikon per jenis itulah yang membuat empat baris bisa dipindai sekali
 * lihat.
 */
.perk-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}
.perk-row {
  display: flex;
  /* Rata ATAS, bukan tengah. Selama isinya dua baris pendek, tengah dan atas
     terlihat sama; begitu nilainya patah tiga baris - "Masa berlaku" di tier
     atas, daftar voucher yang panjang - pelat ikon yang dipusatkan melayang di
     tengah blok teks dan berhenti menunjuk barisnya sendiri. */
  align-items: flex-start;
  gap: var(--sp-3);
}
.perk-icon {
  flex: none;
  width: var(--plate-md);
  height: var(--plate-md);
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: var(--tier-bg, var(--surface-2));
  color: var(--tier, var(--ink-2));
}
.perk-icon svg {
  width: 20px;
  height: 20px;
}
.perk-text {
  min-width: 0;
}
/*
 * LABEL DI ATAS, NILAI DI BAWAH - dan yang keras nilainya.
 *
 * Susunan ini dulu terbalik: label 15px tebal bertinta penuh, nilainya 12px abu
 * redup di bawahnya. Artinya "Perolehan poin" - kata yang tidak mengabarkan apa
 * pun yang belum tertulis di judul layar - digambar lebih keras daripada "200
 * poin setiap Rp1.000.000 belanja", satu-satunya kalimat di kartu ini yang
 * membuat orang membukanya.
 *
 * Sekarang urutannya sama dengan panel angka di beranda (`.wallet-fact`):
 * label kecil sebagai patokan, lalu nilainya. Labelnya tetap ada dan tetap
 * kembar di setiap tab - itu yang membuat Bronze dan Silver bisa diadu - tapi
 * ia berhenti berteriak.
 */
.perk-label {
  margin: 0;
  font-size: var(--fs-caption);
  /* 500, pekerjaan yang memang jatahnya: "label, nilai sekunder, tab tidak
     aktif". 400 membuatnya melebur dengan kalimat di bawahnya, 600 memulai
     lagi persis perlombaan yang baru saja diselesaikan. */
  font-weight: var(--fw-medium);
  line-height: 1.3;
  color: var(--muted);
}
.perk-value {
  margin: 2px 0 0;
  font-size: var(--fs-lg);
  line-height: 1.35;
  color: var(--ink);
}
/* Baris beraturan, bukan berangka - "Masa berlaku". Satu anak tangga di bawah
   baris nilai, dan itu selisih yang harus ada: kalimat yang berbunyi SAMA di
   keempat tab tidak boleh segagah satu-satunya baris yang berbeda antar tab.
   Tetap `--ink-2`, bukan `--muted`: ia masih kalimat yang dibaca. */
.perk-value.is-note {
  font-size: var(--fs-base);
  color: var(--ink-2);
}
/*
 * BESARANNYA saja yang tebal, bukan seluruh baris.
 *
 * 700 di app ini jatah untuk judul layar, judul bagian, dan ANGKA YANG DICARI
 * ORANG - dan "200 poin" tepat yang ketiga. Yang TIDAK ikut ditebalkan
 * "setiap Rp1.000.000 belanja": itu penyebut, bukan nilai, dan menebalkan
 * keduanya mengembalikan baris ini ke keadaan "semuanya tebal berarti tidak ada
 * yang tebal".
 *
 * `tabular-nums` karena angka di sini dibandingkan ANTAR TAB: 200 di tab Bronze
 * dan 1.000 di tab Silver harus berdiri di lebar digit yang sama, atau
 * perbandingannya ikut bergeser setiap kali tabnya berganti.
 */
.perk-num {
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}

/* --- Cara kerjanya: aturan poin & tier --- */
/*
 * Dulu bagian ini enam paragraf `.article-p` telanjang di atas kanvas, tepat di
 * bawah dua kartu putih - jadi satu-satunya bagian halaman yang tidak punya
 * bidangnya sendiri, dan ia terbaca persis seperti itu: catatan kaki yang
 * kelupaan dimasukkan ke dalam sesuatu.
 *
 * Yang memperbaikinya bukan hiasan, melainkan SUSUNAN. Tiap butir sekarang
 * pokok + penjelasan, dan pokoknya - 15px tebal - yang membuat enam aturan bisa
 * dipindai tanpa membaca keenamnya. Pelat ikon di kiri memberi tiap baris
 * jangkar yang bisa dituju mata, dan warnanya bergilir menghabiskan keenam
 * warna logo persis sekali di sepanjang bagian ini.
 *
 * Garis rambut antarbaris, bukan jarak saja: tiga blok teks dua tingkat yang
 * cuma dipisahkan udara terbaca sebagai satu paragraf panjang yang kebetulan
 * berikon.
 */
.how-card {
  /* Tegak dari BARIS, bukan dari kartu: tiap baris membawa jarak atas-bawahnya
     sendiri supaya garis rambut di antaranya membentang penuh setinggi baris.
     Kalau kartunya yang memberi jarak, garis itu berdiri terlalu rapat pada
     teks di atasnya dan terlalu jauh dari teks di bawahnya. */
  padding-top: 0;
  padding-bottom: 0;
}
.how-row {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-4) 0;
}
.how-row + .how-row {
  border-top: 1px solid var(--line);
}
.how-icon {
  flex: none;
  width: var(--plate-sm);
  height: var(--plate-sm);
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: var(--accent-bg);
  color: var(--accent);
}
.how-icon svg {
  width: 19px;
  height: 19px;
}
.how-text {
  min-width: 0;
}
/* Pokoknya. Bobot 600 dan bukan 700: ini judul BARIS, dan 700 di app ini jatah
   judul layar, judul bagian, dan angka. */
.how-lead {
  margin: 0;
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  line-height: 1.3;
}
/* Penjelasannya. `--ink-2` dan bukan `--muted`: ini kalimat yang DIBACA, bukan
   keterangan yang dilirik - dan abu-abu redup adalah cara membuat orang berhenti
   membacanya. Satu anak tangga di bawah pokoknya sudah cukup untuk susunan. */
.how-body {
  margin: 3px 0 0;
  font-size: var(--fs-base);
  line-height: 1.55;
  color: var(--ink-2);
}

/* ---------------- Tingkatan Membership ---------------- */
/*
 * Tangga tiernya, satu tier per tab.
 *
 * Sebagai DAFTAR ia memaksa empat kartu berlomba di satu layar, dan yang
 * menang selalu yang paling atas. Sebagai tab, tiap tier dapat seluruh lebar
 * layar untuk dirinya sendiri - lengkap dengan medali sebesar benda dan
 * syaratnya - dan yang dibandingkan orang jadi tab ini dengan tab sebelahnya.
 */

/*
 * Gambar di atas tab: SATU gambar untuk keempat tier.
 *
 * Isinya hadiah dan keuntungan member, dan itu tidak berubah menurut tab yang
 * sedang dibuka - jadi ia juga tidak ikut berganti saat tabnya berganti.
 * Perbandingannya dikunci 16:9 DI SINI, bukan oleh gambarnya, dengan alasan
 * yang sama seperti banner beranda: gambar yang tingginya mengikuti berkasnya
 * melompat saat termuat dan menggeser tab persis pada detik jempol menekannya.
 */
.level-image {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  margin-bottom: var(--sp-4);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  /* Garis rambut, karena gambarnya diganti orang lain dan bisa saja pucat:
     gambar pastel di atas kanvas pastel kehilangan tepinya sama sekali. */
  border: 1px solid var(--line);
}

/* Kepala kartu tier: medali, nama, dan SYARATNYA - yang dulu duduk di panel
   besar sebelum panel itu jadi gambar. */
.level-card-head {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
}
/* Garisnya menempel pada DAFTAR KEUNTUNGAN, bukan pada kepala kartu: di
   antara keduanya kadang ada baris jarak, dan garis yang menempel di kepala
   akan berdiri di tempat yang salah persis di tier yang paling sering
   dibuka - tier yang sedang dikejar. */
.level-card .perk-list {
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
}
.level-card-text {
  flex: 1;
  min-width: 0;
}
.level-card-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-height: 24px;
}
.level-card-name {
  margin: 0;
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  letter-spacing: -0.01em;
  color: var(--tier, var(--ink-2));
}
.level-card-need {
  margin: 3px 0 0;
  font-size: var(--fs-caption);
  line-height: 1.4;
  color: var(--muted);
}
/*
 * Jarak member ke tier ini, di tier yang sedang dikejar saja.
 *
 * Sebagai kalimat tebal di antara tab dan kartu ia tidak dimiliki keduanya:
 * terlalu berat untuk keterangan, terlalu telanjang untuk judul, dan menunjuk
 * ("sampai ke sini") ke kartu yang belum kelihatan. Sekarang ia baris di DALAM
 * kartu itu, dengan bentuk yang sama seperti baris keuntungan di bawahnya -
 * satu-satunya bedanya latar berwarna tier, karena baris ini bercerita tentang
 * MEMBER sementara yang lain bercerita tentang tiernya.
 */
.level-gap {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-3);
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--tier-solid, var(--brand-600)) 13%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--tier-solid, var(--brand-600)) 22%, transparent);
}
.level-gap-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--plate-sm);
  height: var(--plate-sm);
  border-radius: var(--r-sm);
  /* Pelat PUTIH di atas strip berwarna, kebalikan dari baris keuntungan yang
     pelatnya berwarna di atas kartu putih. Dua peran, dua arah. */
  background: var(--surface);
  color: var(--tier, var(--ink-2));
}
.level-gap-icon svg {
  width: 20px;
  height: 20px;
}
.level-gap-text {
  min-width: 0;
}
.level-gap-label {
  margin: 0;
  font-size: var(--fs-badge);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--tier, var(--muted));
}
.level-gap-value {
  margin: 2px 0 0;
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  line-height: 1.35;
  color: var(--ink);
}

/* Tab tier. Bentuknya segmented yang sudah dipakai di Riwayat dan Voucher -
   yang berbeda cuma warnanya: tab aktif memakai warna TIER-nya sendiri, jadi
   berpindah tab benar-benar mengganti warna halamannya. */
.tier-tabs .seg-btn {
  min-width: 0;
  padding: 0 var(--sp-1);
  font-size: var(--fs-sm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tier-tabs .seg-btn.is-active {
  background: var(--tier-bg, var(--surface));
  color: var(--tier, var(--brand-600));
}
/* Di 320px seperempat baris tinggal 67px, dan "Platinum" pada 13.5px butuh
   62px di antaranya - satu huruf terakhir hilang jadi elipsis. Yang mengalah
   ukurannya, bukan namanya: nama tier yang terpotong adalah nama yang salah,
   dan 12.5px masih jauh di atas lantai 11px yang cuma boleh disentuh lencana. */
@media (max-width: 359px) {
  .tier-tabs .seg-btn {
    padding: 0 2px;
    font-size: var(--fs-caption);
  }
}

/* Visible to a screen reader, absent from the layout. Used for the labels of
   inputs whose placeholder already says what they are. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Province heading in the branch list. Sticky, because 24 branches scroll past
   the heading that explains why Cilegon comes before Jakarta. */
.outlet-group {
  position: sticky;
  top: 0;
  z-index: 1;
  margin: var(--sp-4) 0 2px;
  padding: var(--sp-2) 2px var(--sp-2);
  /* Tembus + blur, bukan `--canvas` datar: lembar di belakangnya adalah gradien
     pastel, dan sepotong warna datar yang ikut menempel saat digulir akan
     terbaca sebagai pita yang salah warna di tengah daftar. */
  background: color-mix(in srgb, var(--canvas) 78%, transparent);
  backdrop-filter: blur(10px);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}
.outlet-group:first-child {
  margin-top: 2px;
}

/* A detail screen with no artwork.
   `.detail-sheet` is pulled up under the poster it expects above it; with no
   poster the sheet starts at the top of the screen and the fixed back button
   sits on the title. Most branch profiles and many promos have no image, so
   this is the normal state here, not the exception. */
.detail-view.no-poster .detail-sheet {
  margin-top: 0;
  padding-top: calc(64px + env(safe-area-inset-top));
  border-radius: 0;
}

/* A promo with no poster gets a mark, not a wall of text: the summary is
   already the line under the title, and at the size that fills a 4:3 panel it
   runs straight under the badge in the corner. */
.promo-thumb-empty svg {
  width: 44%;
  max-width: 96px;
  color: rgba(255, 255, 255, 0.88);
}

/* Penanda jalur uji pada layar kode.
   Kuning, bukan biru merek: ini bukan bagian dari alur member yang normal, dan
   satu-satunya cara salah membacanya adalah kalau ia tampak seperti alur biasa. */
.dev-note {
  margin: -10px 0 18px;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--c-yellow) 22%, transparent);
  border: 1px solid color-mix(in srgb, var(--c-orange) 45%, transparent);
  color: var(--ink);
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  line-height: 1.4;
}
[data-theme='dark'] .dev-note {
  background: color-mix(in srgb, var(--c-orange) 18%, transparent);
  color: var(--c-yellow);
}
