@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/inter-regular.woff2?v=202610200201") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/inter-semibold.woff2?v=202610200201") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("fonts/inter-medium.woff2?v=202610200201") format("woff2");
}

/* =========================================================
   Portfolio UI/UX: sistem visual
   Monokrom. Aset project jadi satu-satunya sumber warna.

   SISTEM SPASI
   Sama seperti situs referensi: seluruh tata letak dinyatakan dalam rem,
   dan ukuran 1rem ikut lebar layar. Jadi proporsinya tetap identik di
   layar mana pun, tidak perlu belasan breakpoint.

   Desktop  1rem = 100vw / 144   (di 1440px jadi 10px, sama persis referensi)
   Phone    1rem = 100vw / 40

   Bedanya dengan referensi, ada batas bawah dan atas. Referensi murni vw,
   akibatnya di tablet judulnya menyusut sampai 18px dan tombolnya di bawah
   ukuran sentuh yang layak. Batas bawah menahan itu.

   Yang tetap dalam vw hanya posisi horizontal kolom, supaya perbandingan
   kiri-kanannya persis mengikuti referensi.
   ========================================================= */

html {
  font-size: clamp(8.6px, 0.6944vw, 15px);
}

@media (max-width: 767px) {
  html {
    font-size: clamp(11px, 2.5vw, 19.5px);
  }
}

:root {
  /* Suisse Intl kalau terpasang, kalau tidak jatuh ke Helvetica Neue
     yang bentuk hurufnya paling dekat. */
  /* Inter dibundel sendiri (fonts/), jadi Mac, iPhone, Android, dan pen.dev
     memakai huruf yang sama. Cadangan hanya dipakai selama font dimuat. */
  --font: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;

  --bg: #f6f6f6;
  --ink: #111111;
  --ink-2: #6f6f6f;
  --ink-3: #a3a3a3;
  --ink-4: #c9c9c9;
  --surface: #ffffff;
  --card: #e8e8e8;
  --line: rgba(17, 17, 17, 0.08);

  /* Skeuomorph minimalis: semua permukaan dibuat dari satu bahan yang sama
     dengan latar. Wajahnya bergradasi tipis dari terang ke sedikit redup,
     tepi atas tersorot, tepi bawah sedikit gelap, lalu bayangan dekat yang
     tegas dan bayangan jauh yang lebar dan pudar. Hasilnya terasa timbul
     dari halaman, bukan benda lain yang ditempel. */
  --sk-face: linear-gradient(180deg, #fdfdfd 0%, #f2f2f2 100%);
  --shadow-sm:
    inset 0 1px 0 rgba(255, 255, 255, 0.95),
    inset 0 -1px 0 rgba(17, 17, 17, 0.035),
    0 0 0 1px rgba(17, 17, 17, 0.055),
    0 1px 2px rgba(17, 17, 17, 0.06),
    0 0.6rem 1.6rem -0.6rem rgba(17, 17, 17, 0.1);
  --shadow-press:
    inset 0 1px 3px rgba(17, 17, 17, 0.1),
    inset 0 0 0 1px rgba(17, 17, 17, 0.06),
    0 1px 0 rgba(255, 255, 255, 0.9);
  --shadow-md:
    0 0 0 1px rgba(17, 17, 17, 0.045),
    0 1px 2px rgba(17, 17, 17, 0.05),
    0 1rem 2.2rem -1rem rgba(17, 17, 17, 0.12),
    0 3.6rem 6.4rem -2.8rem rgba(17, 17, 17, 0.18);
  /* sorot di bibir kartu media, dipasang di atas video lewat ::after */
  --sk-rim:
    inset 0 1px 0 rgba(255, 255, 255, 0.85),
    inset 0 0 0 1px rgba(255, 255, 255, 0.3),
    inset 0 -0.2rem 0.6rem rgba(17, 17, 17, 0.04);

  --r-card: 3.2rem;
  --r-media: 2.4rem;
  --r-btn: 1.5rem;

  --title-fs: 3.2rem;

  /* --- geometri, diambil dari pengukuran referensi di 1440px --- */
  --rail-x: 17.85vw;      /* 257px di 1440 */
  --deck-x: 70.5vw;       /* titik tengah kartu */
  --deck-size: 33.5rem;   /* 335px di 1440 */

  /* Dek disusun seperti drum: kartu tetangga diputar di sumbu X sampai
     hampir rebah, jadi bentuknya trapesium bukan persegi yang diperkecil.
     Jaraknya kelipatan ukuran kartu supaya konsisten di semua layar. */
  --deck-gap-1: 0.83;
  --deck-gap-2: 1.05;
  --deck-tilt-1: 66deg;
  --deck-tilt-2: 80deg;
  --dots-x: 5.76vw;       /* jarak titik indikator dari tepi kanan */
  --split-x: 50.5vw;      /* batas kolom di halaman project */

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  /* sedikit melewati tujuan lalu balik, biar perpindahan kartu terasa memantul */
  --ease-bounce: cubic-bezier(0.22, 1.14, 0.36, 1);
  --dur: 0.82s;
}

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

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

body { min-height: 100vh; }
/* Pantulan iOS di tepi halaman ikut menarik elemen yang menempel di layar,
   jadi navigasi bawah terlihat ikut ter-scroll. Pantulannya dimatikan. */
html, body { overscroll-behavior: none; }

/* latar ASCII (ascii-bg.js): di atas warna latar, di bawah semua konten */
/* Warna latar cukup di html. Kalau body ikut berwarna, latarnya digambar
   di atas kanvas z-index -1 dan ASCII-nya tertutup. */
body { background: transparent; }
#ascii-bg {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
}
body.no-scroll { overflow: hidden; }

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; border: 0; background: none; cursor: pointer; }

.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;
}

:focus-visible {
  outline: 0.2rem solid var(--ink);
  outline-offset: 0.3rem;
  border-radius: 0.4rem;
}

/* =========================================================
   Kolom kiri
   ========================================================= */

.rail {
  position: fixed;
  left: var(--rail-x);
  top: 50%;
  transform: translateY(-50%);
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: 5.5rem;
  pointer-events: none;
}
.rail > * { pointer-events: auto; }

/* Batas kanan kolom kiri. Judul tidak pernah pindah baris; kalau lebih
   panjang dari ruang ini, pasJudul() di app.js memperkecil ukurannya.
   Home dibatasi tepi kiri kartu, project dibatasi tombol expand. */
.rail { max-width: calc(var(--deck-x) - var(--deck-size) / 2 - var(--rail-x) - 4rem); }
.rail--project { max-width: calc(var(--split-x) - 7.4rem - var(--rail-x) - 4rem); }

/* Di about, rail dikosongkan. Nama, tombol, dan teksnya jadi satu aliran
   di dalam kolom .about supaya semuanya ikut ter-scroll bersama. */
.rail--about { display: none; }

.title-block {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

/* Kotak masker untuk animasi reveal. Kalau tingginya persis setinggi baris,
   ekor huruf seperti g dan y ikut terpotong. Jadi kotaknya dilonggarkan ke
   atas dan bawah, lalu ditarik balik dengan margin negatif supaya posisi
   tata letaknya tidak bergeser sedikit pun. */
.title-line {
  overflow: hidden;
  font-size: var(--title-fs);
  padding: 0.34em 0;
  margin: -0.34em 0;
}

.title,
.subtitle {
  margin: 0;
  font-size: var(--title-fs);
  line-height: 1.1;
  font-weight: 500;
  letter-spacing: -0.02em;
  /* boleh pindah baris di desktop dan tablet; kolom kiri ditambatkan dari
     bawah (tambatRail di app.js) jadi judul yang panjang tumbuh ke atas */
  white-space: normal;
}
.title { color: var(--ink); }
.subtitle { color: var(--ink-3); }
/* Di layar besar peran profil dibatasi selebar "Senior Product and", jadi
   "Graphic Designer" selalu turun ke baris kedua dan kolom kiri tidak
   memanjang. Di HP peran tetap satu baris (aturan nowrap di bawah). */
@media (min-width: 768px) {
  .title-block.is-profile .subtitle { max-width: 10.4em; }
}

/* masking reveal saat halaman masuk */
.title-line > span {
  display: block;
  will-change: transform;
  animation: line-up 0.9s var(--ease) both;
}
.title-line:nth-child(2) > span { animation-delay: 0.06s; }
/* geseran awal harus melewati padding masker, kalau tidak teksnya
   sudah kelihatan mengintip sebelum animasinya mulai */
@keyframes line-up {
  from { transform: translateY(calc(100% + 0.45em)); }
  to   { transform: translateY(0); }
}

/* --- tombol ikon yang melebar jadi pil saat hover --- */

.actions {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  animation: fade-up 0.9s var(--ease) 0.18s both;
}

@keyframes fade-up {
  from { opacity: 0; transform: translateY(0.8rem); }
  to   { opacity: 1; transform: none; }
}
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: 0;
  height: 5.4rem;
  padding: 0 1.65rem;
  background: var(--sk-face);
  border-radius: var(--r-btn);
  box-shadow: var(--shadow-sm);
  transition:
    gap 0.5s var(--ease),
    border-radius 0.5s var(--ease),
    background 0.3s ease,
    color 0.3s ease,
    box-shadow 0.3s ease;
}

.pill .ico {
  display: grid;
  place-items: center;
  width: 2.1rem;
  height: 2.1rem;
  flex: none;
}
.pill .ico svg { width: 100%; height: 100%; display: block; }

/* dua ikon ditumpuk di posisi yang sama lalu ditukar dengan putaran */
.ico-swap { position: relative; }
.ico-swap > span {
  position: absolute;
  inset: 0;
  display: block;
  transition:
    opacity 0.25s ease,
    transform 0.45s var(--ease);
}
.ico-swap .ico-b {
  opacity: 0;
  transform: rotate(-70deg) scale(0.6);
}
.contact.is-open .ico-swap .ico-a,
.pill.is-alt .ico-swap .ico-a {
  opacity: 0;
  transform: rotate(70deg) scale(0.6);
}
.contact.is-open .ico-swap .ico-b,
.pill.is-alt .ico-swap .ico-b {
  opacity: 1;
  transform: none;
}

/* label disembunyikan lewat lebar 0, bukan display:none, supaya bisa dianimasi */
.pill .lbl {
  display: block;
  max-width: 0;
  overflow: hidden;
  white-space: nowrap;
  font-size: 1.55rem;
  letter-spacing: -0.01em;
  opacity: 0;
  transition:
    max-width 0.55s var(--ease),
    opacity 0.35s ease;
}

.pill:focus-visible {
  gap: 0.85rem;
  border-radius: 999px;
}
.pill:focus-visible .lbl {
  max-width: 16rem;
  opacity: 1;
}
/* Hover hanya untuk perangkat yang memang punya kursor. Di layar sentuh,
   ketukan meninggalkan status :hover yang menempel, jadi tombol tetap
   memanjang dengan labelnya sampai layar diketuk di tempat lain. */
@media (hover: hover) and (pointer: fine) {
  .pill:hover { gap: 0.85rem; border-radius: 999px; }
  .pill:hover .lbl { max-width: 16rem; opacity: 1; }
}

.pill.is-dark {
  background: linear-gradient(180deg, #2a2a2a 0%, #111 100%);
  color: var(--surface);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 0 0 1px rgba(17, 17, 17, 0.6), 0 1px 2px rgba(17, 17, 17, 0.2), 0 0.6rem 1.6rem -0.6rem rgba(17, 17, 17, 0.3);
}
/* ditekan: permukaannya masuk ke dalam */
.pill:active, .edge-btn:active { box-shadow: var(--shadow-press); }
.pill.is-dark:active { box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5), 0 1px 0 rgba(255, 255, 255, 0.6); }
.pill.is-round { border-radius: 999px; }
.pill.rev { flex-direction: row-reverse; }

/* pil yang labelnya selalu tampil, tidak menunggu hover */
.pill.is-static { gap: 0.85rem; }
.pill.is-static .lbl { max-width: 16rem; opacity: 1; }

/* --- kelompok tombol contact --- */

/* Tanpa gap. Kalau wadah ini punya gap sendiri, jaraknya tetap terhitung
   meski daftar kontaknya sedang tertutup dan berlebar nol, akibatnya tombol
   sesudahnya terdorong dua kali lebih jauh dari tombol sebelumnya.
   Jaraknya diurus lewat margin .socials yang ikut dianimasikan. */
.contact {
  position: relative;   /* acuan posisi daftar kontak yang ditumpuk */
  display: flex;
  align-items: center;
  gap: 0;
}

/* padding lalu margin negatif supaya bayangan pil tidak ikut terpotong
   saat lebarnya dianimasi */
.socials {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  overflow: hidden;
  padding: 0.9rem;
  margin: -0.9rem;
  max-width: 0;
  transition:
    max-width 0.6s var(--ease),
    margin-left 0.6s var(--ease);
}
.socials .pill {
  opacity: 0;
  transform: translateX(-1rem);
  transition:
    opacity 0.35s ease,
    transform 0.5s var(--ease),
    background 0.3s ease,
    color 0.3s ease;
}
/* 1,1rem jarak dikurangi 0,9rem padding, supaya celah terlihatnya
   sama persis dengan jarak antar tombol lain */
.contact.is-open .socials {
  max-width: 46rem;
  margin-left: 0.2rem;
}
.contact.is-open .socials .pill {
  opacity: 1;
  transform: none;
}

/* =========================================================
   Daftar kontak yang ditumpuk

   Kalau tidak muat ke samping, daftarnya ditumpuk. Kelas .is-stack dan arah
   naik/turunnya dipasang app.js setelah mengukur ruang nyata sampai halangan
   terdekat di kanan, bukan dari lebar layar. Dengan begitu tetap benar berapa
   pun jumlah kontak yang ditambahkan ke PROFILE.contacts.
   ========================================================= */

.contact.is-stack .socials {
  position: absolute;
  z-index: 60;          /* di about, teksnya datang sesudah di DOM jadi menimpa */
  left: 50%;
  transform: translateX(-50%);
  align-items: center;
  gap: 1.1rem;
  max-width: none;
  overflow: visible;
  padding: 0;
  margin: 0;
  pointer-events: none;
}
.contact.is-stack.stack-down .socials {
  top: 100%;
  margin-top: 1.1rem;
  flex-direction: column;
}
.contact.is-stack.stack-up .socials {
  bottom: 100%;
  margin-bottom: 1.1rem;
  flex-direction: column-reverse;
}
.contact.is-stack.stack-down .socials .pill { transform: translateY(-0.9rem); }
.contact.is-stack.stack-up .socials .pill { transform: translateY(0.9rem); }
.contact.is-stack.is-open .socials { pointer-events: auto; }
.contact.is-stack.is-open .socials .pill { transform: none; }

/* =========================================================
   Home: dek kartu vertikal
   ========================================================= */

.stage { position: relative; }

/* Lapisan dek menutupi seluruh layar supaya roda mouse dan geseran
   bekerja di mana saja, bukan cuma tepat di atas kartunya. */
.deck {
  position: fixed;
  inset: 0;
  z-index: 10;
  touch-action: pan-x;
  perspective: 118rem;
  perspective-origin: var(--deck-x) 50%;
}
.deck.is-dragging { cursor: grabbing; }

.deck-card {
  position: absolute;
  left: var(--deck-x);
  top: 50%;
  width: var(--deck-size);
  height: var(--deck-size);
  margin: calc(var(--deck-size) / -2) 0 0 calc(var(--deck-size) / -2);
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--bg);
  box-shadow: var(--shadow-md);
  transform-origin: 50% 50%;
  will-change: transform, opacity, filter;
  pointer-events: none;
  backface-visibility: hidden;
  /* Hanya transform yang boleh pelan. Bayangan dan blur harus cepat selesai.
     Kalau bayangan ikut beranimasi selama 0,55 detik, di tengah jalan dia
     kena filter blur milik kartu yang sedang rebah dan hasilnya jadi gumpalan
     gelap besar yang mengikuti kartu. Itu yang kelihatan seperti bug. */
  transition:
    transform var(--dur) var(--ease-bounce),
    opacity 0.4s var(--ease),
    box-shadow 0.16s ease-out;
}

/* Posisi relatif terhadap kartu aktif. Jaraknya kelipatan ukuran kartu,
   jadi susunannya tetap sama di semua ukuran layar. Referensi memberi
   jarak besar sehingga tetangganya cuma mengintip sedikit di tepi. */
/* transition di sini yang berlaku saat kartu MENJADI aktif: bayangannya
   masuk pelan dan diberi jeda, jadi kartunya mendarat dulu baru terangkat */
.deck-card[data-pos="0"] {
  transition:
    transform var(--dur) var(--ease-bounce),
    opacity 0.4s var(--ease),
    box-shadow 0.45s var(--ease) 0.22s;
  transform: translateY(0) rotateX(0deg);
  opacity: 1; z-index: 3;
  pointer-events: auto;
  cursor: pointer;
}
/* di atas kartu aktif, sisi atasnya yang menjauh */
/* Tanpa blur sama sekali. Dulu kartu tetangga di-blur untuk menyamarkan
   isinya, tapi isinya sekarang memang tidak dirender jadi tidak ada yang perlu
   disamarkan. Yang tersisa cuma kerugiannya: slab abu berpinggir lembut
   terbaca sebagai halo gelap, dan karena blur ikut beranimasi halonya bergerak
   mengikuti kartu. Itu yang terasa seperti bug saat kartu bergeser. Bentuk
   trapesium dan opasitas sudah cukup memberi kesan kedalaman.

   Kartu tetangga hanya menyisakan badan kartunya. Isinya disembunyikan
   supaya yang terlihat cuma slab abu-abu, sama seperti referensi. Media
   yang ikut dirender di posisi rebah justru bikin tampilannya ramai. */
.deck-card:not([data-pos="0"]) .card-name {
  opacity: 0;
}
/* Sekarang media tetangga diintipkan tipis: warnanya diredam dan memudar
   ke tepi yang menjauh, jadi terbaca sebagai sekilas project berikutnya
   tanpa bersaing dengan kartu aktif. */
.deck-card:not([data-pos="0"]) .mock {
  filter: grayscale(0.55) contrast(0.92);
}
.deck-card[data-pos="-1"] .mock,
.deck-card[data-pos="-2"] .mock {
  -webkit-mask-image: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0.25) 85%);
  mask-image: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0.25) 85%);
}
.deck-card[data-pos="1"] .mock,
.deck-card[data-pos="2"] .mock {
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.25) 85%);
  mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.25) 85%);
}
.deck-card[data-pos="-1"] .mock,
.deck-card[data-pos="1"] .mock { opacity: 0.55; }
.deck-card[data-pos="-2"] .mock,
.deck-card[data-pos="2"] .mock { opacity: 0.25; }

/* Tanpa bayangan. Pada slab yang hampir rata, drop shadow bikin halo gelap
   yang menarik mata, padahal tugasnya cuma jadi latar. Blur dan opasitas
   sudah cukup memisahkannya dari kartu aktif. */
.deck-card:not([data-pos="0"]) {
  box-shadow: none;
}
.deck-card .mock,
.deck-card .card-name {
  transition: opacity 0.25s var(--ease), filter 0.35s var(--ease);
}

.deck-card[data-pos="-1"] {
  transform:
    translateY(calc(var(--deck-size) * var(--deck-gap-1) * -1))
    rotateX(var(--deck-tilt-1));
  opacity: 1; z-index: 2;
}
/* di bawah kartu aktif, sisi bawahnya yang menjauh */
.deck-card[data-pos="1"] {
  transform:
    translateY(calc(var(--deck-size) * var(--deck-gap-1)))
    rotateX(calc(var(--deck-tilt-1) * -1));
  opacity: 1; z-index: 2;
}
.deck-card[data-pos="-2"] {
  transform:
    translateY(calc(var(--deck-size) * var(--deck-gap-2) * -1))
    rotateX(var(--deck-tilt-2));
  opacity: 0.72; z-index: 1;
}
.deck-card[data-pos="2"] {
  transform:
    translateY(calc(var(--deck-size) * var(--deck-gap-2)))
    rotateX(calc(var(--deck-tilt-2) * -1));
  opacity: 0.72; z-index: 1;
}
.deck-card[data-pos="x"] {
  transform: translateY(0) rotateX(88deg);
  opacity: 0; z-index: 0;
}

.deck-card .card-link {
  position: absolute;
  inset: 0;
  display: block;
}

.deck-card .card-name {
  position: absolute;
  left: 3rem;
  bottom: 2.8rem;
  z-index: 4;
  font-size: 1.6rem;
  letter-spacing: -0.01em;
  color: var(--ink);
  transition: transform 0.5s var(--ease);
}

/* Di kartu yang medianya sudah ada, namanya dibalik jadi putih. Video isinya
   tidak bisa dijamin, dan tepi bawahnya sering kena bidang terang. Teks gelap
   di sana hilang. Putih di atas peredam gelap terbaca di isi apa pun. */
/* tebal dan abu-abu redup, tapi cukup gelap untuk lolos kontras AA
   (sekitar 4,7:1 di atas peredam terang) */
.deck-card.has-media .card-name { color: var(--ink-2); font-weight: 600; }
@media (hover: hover) and (pointer: fine) {
  .deck-card[data-pos="0"]:hover .card-name { transform: translateX(0.4rem); }
}

.deck.is-intro .deck-card { animation: deck-in 1s var(--ease) both; }
@keyframes deck-in {
  from { opacity: 0; }
}

/* --- titik indikator --- */

.dots {
  position: fixed;
  right: var(--dots-x);
  top: 50%;
  transform: translateY(-50%);
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.15rem;
  animation: fade-in 0.9s var(--ease) 0.3s both;
}
.dots[hidden] { display: none; }

.dot {
  width: 0.9rem;
  height: 0.9rem;
  display: grid;
  place-items: center;
}
.dot::after {
  content: "";
  width: 0.32rem;
  height: 0.32rem;
  border-radius: 999px;
  background: var(--ink-4);
  transition: all 0.45s var(--ease);
}
.dot:hover::after { background: var(--ink-3); }
.dot[aria-current="true"]::after {
  width: 0.55rem;
  height: 0.55rem;
  background: var(--ink);
}

/* =========================================================
   Halaman project
   ========================================================= */

.media {
  margin-left: var(--split-x);
  padding: 2rem 2rem 2rem 0;
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.media-item {
  position: relative;
  height: 88vh;
  min-height: 30rem;
  border-radius: var(--r-media);
  overflow: hidden;
  background: var(--bg);
  box-shadow: var(--shadow-md);
  animation: media-in 0.9s var(--ease) both;
}
/* bibir tersorot di atas video, sama dengan kartu dek */
.media-item::after,
.deck-card::after,
.note::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 5;
  border-radius: inherit;
  box-shadow: var(--sk-rim);
  pointer-events: none;
}
@keyframes media-in {
  from { opacity: 0; transform: translateY(2rem); }
}

/* kartu teks case study, menggantikan media saat mode info menyala */
.note {
  position: relative;
  min-height: 88vh;
  border-radius: var(--r-media);
  background: var(--sk-face);
  box-shadow: var(--shadow-md);
  padding: 6rem 5.6rem;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 6rem;
  animation: media-in 0.7s var(--ease) both;
}

.note-label {
  font-size: 1.35rem;
  letter-spacing: -0.005em;
  color: var(--ink);
}

.note-body {
  margin: 0;
  font-size: 2.4rem;
  line-height: 1.32;
  letter-spacing: -0.018em;
  color: var(--ink);
  max-width: 40ch;
}

.note-list {
  margin: 2.8rem 0 0;
  padding: 0;
  list-style: none;
  font-size: 2.4rem;
  line-height: 1.32;
  letter-spacing: -0.018em;
  color: var(--ink-3);
}

.note-credit {
  margin-top: 2.8rem;
  font-size: 1.35rem;
  color: var(--ink-3);
}

/* tombol tutup di garis batas dua kolom */
.edge-btn {
  position: fixed;
  left: calc(var(--split-x) - 7.4rem);
  /* --edge-top diisi app.js: tengah baris tombol di kolom kiri */
  top: var(--edge-top, 50%);
  transform: translateY(-50%);
  z-index: 60;
  width: 5.4rem;
  height: 5.4rem;
  display: grid;
  place-items: center;
  background: var(--sk-face);
  border-radius: var(--r-btn);
  box-shadow: var(--shadow-sm);
  transition: border-radius 0.45s var(--ease);
}
.edge-btn svg { width: 2rem; height: 2rem; flex: none; }

/* Label muncul saat hover seperti tombol pill lain. Tombolnya ditambat di sisi
   kanan supaya labelnya memanjang ke kiri dan tidak menutupi media. */
.edge-btn {
  left: auto;
  right: calc(100% - var(--split-x) + 2rem);
  width: auto;
  min-width: 5.4rem;
  padding: 0 1.7rem;
  display: inline-flex;
  align-items: center;
  gap: 0;
  transition:
    gap 0.5s var(--ease),
    border-radius 0.45s var(--ease);
}
.edge-btn .lbl {
  display: block;
  max-width: 0;
  overflow: hidden;
  white-space: nowrap;
  font-size: 1.55rem;
  letter-spacing: -0.01em;
  opacity: 0;
  transition:
    max-width 0.55s var(--ease),
    opacity 0.35s ease;
}
.edge-btn:focus-visible { gap: 0.85rem; border-radius: 999px; }
.edge-btn:focus-visible .lbl { max-width: 16rem; opacity: 1; }
.edge-btn.no-lbl:focus-visible { gap: 0; }
@media (hover: hover) and (pointer: fine) {
  .edge-btn:hover { gap: 0.85rem; border-radius: 999px; }
  .edge-btn:hover .lbl { max-width: 16rem; opacity: 1; }
  .edge-btn.no-lbl:hover { gap: 0; }
}
/* tetap display block supaya lebar teksnya masih bisa diukur app.js */
.edge-btn.no-lbl .lbl { max-width: 0; opacity: 0; }

/* =========================================================
   Halaman about
   ========================================================= */

.about {
  padding: 22vh var(--rail-x) 24vh;
  max-width: 116rem;
}

/* Nama dan tombol duduk di atas teks, ikut ter-scroll. Jaraknya lebar
   supaya layar pertama cuma berisi identitas, teksnya baru menyusul
   saat di-scroll. Pola ini diambil dari referensi. */
.about-head {
  /* position + z-index wajib. Tanpa ini daftar kontak yang ditumpuk tertimpa
     teks di bawahnya, karena teksnya datang belakangan di DOM sementara
     .about-head cuma elemen statis biasa. */
  position: relative;
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: 5.5rem;
  margin-bottom: 18vh;
  animation: fade-up 0.9s var(--ease) both;
}

.prose {
  font-size: 3rem;
  line-height: 1.28;
  letter-spacing: -0.022em;
  color: var(--ink-3);
  /* dijaga tetap sempit supaya tidak menabrak globe di kanan */
  max-width: 17em;
}
.prose p { margin: 0 0 4.4rem; }
.about > *:last-child .prose p:last-child { margin-bottom: 0; }

/* kata kunci dinaikkan kontrasnya. Ini trik skimming dari referensi. */
.prose strong { font-weight: 500; color: var(--ink); }
.prose a { font-weight: 500; color: var(--ink); border-bottom: 1px solid var(--line); }
.prose a:hover { border-bottom-color: var(--ink); }

.awards-label {
  font-size: 1.35rem;
  color: var(--ink);
  margin: 0 0 1.6rem;
}
.awards {
  margin: 0 0 4.4rem;
  padding: 0;
  list-style: none;
  font-size: 3rem;
  line-height: 1.28;
  letter-spacing: -0.022em;
  color: var(--ink);
}
.awards span { color: var(--ink-3); }

.about-block { animation: fade-up 0.9s var(--ease) both; }

/* =========================================================
   Globe clay di halaman about
   ========================================================= */

.globe {
  position: fixed;
  right: 8vw;
  top: 50%;
  transform: translateY(-50%);
  width: min(30vw, 46rem);
  aspect-ratio: 1 / 1;
  z-index: 20;
  cursor: grab;
  touch-action: none;
}
.globe.is-dragging { cursor: grabbing; }

.globe-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.globe-label {
  position: absolute;
  left: 0;
  top: 0;
  margin: -1.5rem 0 0 0.7rem;
  padding: 0.5rem 1.1rem;
  background: var(--sk-face);
  border-radius: 999px;
  box-shadow: var(--shadow-sm);
  font-size: 1.35rem;
  letter-spacing: -0.005em;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}

/* =========================================================
   Placeholder aset: mock UI abu-abu
   Ganti seluruh blok .mock dengan <video> atau <img> asli nanti.
   ========================================================= */

/* Slot berisi media asli: gradien dan animasi drift dimatikan karena
   videonya menutupi seluruh kotak. */
.mock.has-media {
  background: var(--bg);
  animation: none;
}

/* Peredam hanya dipasang di tepi yang memang ada teksnya. Video apa pun bisa
   kebetulan terang atau gelap di titik itu, jadi keterbacaan tidak boleh
   bergantung pada isi video. Di tepi yang tidak ada teksnya, peredam cuma
   memucatkan gambar tanpa memberi apa pun. */
.mock.has-media::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Halaman project: media tanpa label sama sekali, jadi tidak diredam.
   Peredam atas baru menyala kalau labelnya memang dirender. */
.mock.has-media.has-tag::after {
  background: linear-gradient(180deg, rgba(246, 246, 246, 0.78) 0%, rgba(246, 246, 246, 0) 16%);
}

/* Kartu home: tepi bawah jadi kaca buram putih, bukan peredam gelap.
   Blurnya makin kuat ke bawah (mask di atas backdrop-filter), lalu diberi
   lapisan putih susu supaya bidang di belakang nama selalu terang, apa pun
   isi videonya. Karena itu nama project ditulis gelap. Lapisan putih penuh
   sampai melewati baris nama (nama duduk di 8,4% sampai 14,1% dari bawah),
   baru meluruh, sama seperti alasan pada peredam gelap sebelumnya. */
.deck-card .mock.has-media::after {
  background: linear-gradient(
    0deg,
    rgba(246, 246, 246, 0.82) 0%,
    rgba(246, 246, 246, 0.7) 18%,
    rgba(246, 246, 246, 0.28) 30%,
    rgba(246, 246, 246, 0) 40%
  );
  -webkit-backdrop-filter: blur(18px) saturate(1.15);
  backdrop-filter: blur(18px) saturate(1.15);
  -webkit-mask-image: linear-gradient(0deg, #000 0%, #000 20%, rgba(0, 0, 0, 0.6) 30%, transparent 42%);
  mask-image: linear-gradient(0deg, #000 0%, #000 20%, rgba(0, 0, 0, 0.6) 30%, transparent 42%);
}

/* label harus di atas peredamnya, dan warnanya dinaikkan karena abu muda
   kalah kontras di atas video */
.mock-tag {
  position: absolute;
  z-index: 2;
}
.mock.has-media .mock-tag { color: var(--ink); }

.mock-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.mock {
  position: absolute;
  inset: 0;
  background: linear-gradient(150deg, #efefef 0%, #dcdcdc 45%, #e7e7e7 100%);
  background-size: 200% 200%;
  animation: drift 18s ease-in-out infinite alternate;
  display: grid;
  place-items: center;
  overflow: hidden;
}
@keyframes drift {
  from { background-position: 0% 0%; }
  to   { background-position: 100% 100%; }
}

.mock-frame {
  width: 74%;
  background: var(--surface);
  border-radius: 0.8rem;
  box-shadow: 0 1rem 3rem rgba(17, 17, 17, 0.1);
  overflow: hidden;
  font-size: 0;
}
.media-item .mock-frame { width: 62%; }

.mock-bar {
  height: 2.2rem;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0 0.8rem;
  border-bottom: 1px solid #ececec;
}
.mock-bar i {
  width: 0.4rem; height: 0.4rem;
  border-radius: 999px;
  background: #d8d8d8;
  display: block;
}
.mock-body { display: flex; min-height: 13rem; }
.media-item .mock-body { min-height: 22rem; }

.mock-side {
  width: 20%;
  padding: 1rem 0.8rem;
  border-right: 1px solid #f0f0f0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.mock-main {
  flex: 1;
  padding: 1.2rem;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
}
.mock-row { height: 0.6rem; border-radius: 999px; background: #ededed; }
.mock-row.w80 { width: 80%; }
.mock-row.w60 { width: 60%; }
.mock-row.w40 { width: 40%; }
.mock-row.tall { height: 0.8rem; background: #e2e2e2; }

.mock-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.8rem;
  margin-top: 0.4rem;
}
.mock-tile {
  aspect-ratio: 4 / 3;
  border-radius: 0.4rem;
  background: #f0f0f0;
}
.mock-tile:nth-child(2) { background: #e6e6e6; }
.mock-tile:nth-child(3) { background: #ededed; }

.mock-chart {
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
  height: 6.4rem;
  margin-top: 0.6rem;
}
.mock-chart i {
  flex: 1;
  border-radius: 0.2rem 0.2rem 0 0;
  background: #e4e4e4;
  display: block;
  animation: bar 4s ease-in-out infinite alternate;
}
.mock-chart i:nth-child(1) { height: 40%; animation-delay: 0s; }
.mock-chart i:nth-child(2) { height: 68%; animation-delay: .3s; }
.mock-chart i:nth-child(3) { height: 52%; animation-delay: .6s; }
.mock-chart i:nth-child(4) { height: 84%; background: #cfcfcf; animation-delay: .9s; }
.mock-chart i:nth-child(5) { height: 60%; animation-delay: 1.2s; }
.mock-chart i:nth-child(6) { height: 34%; animation-delay: 1.5s; }
@keyframes bar { to { transform: scaleY(0.82); transform-origin: bottom; } }

.mock-phone {
  width: 42%;
  max-width: 17rem;
  aspect-ratio: 9 / 17;
  background: var(--surface);
  border-radius: 2rem;
  box-shadow: 0 1.2rem 3.4rem rgba(17, 17, 17, 0.12);
  padding: 1.6rem 1.2rem;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
}
.mock-notch {
  width: 32%;
  height: 0.4rem;
  border-radius: 999px;
  background: #e6e6e6;
  margin: 0 auto 0.8rem;
}

.mock-tag {
  position: absolute;
  left: 3rem;
  top: 2.8rem;
  font-size: 1.25rem;
  letter-spacing: 0.02em;
  color: var(--ink-3);
}

/* =========================================================
   Transisi antar halaman
   ========================================================= */

.app.is-leaving .rail,
.app.is-leaving .stage,
.app.is-leaving .dots {
  opacity: 0;
  transition: opacity 0.18s ease;
}

/* Elemen bayangan yang terbang dari kartu ke media, dan sebaliknya. */
.morph {
  position: fixed;
  z-index: 200;
  overflow: hidden;
  background: var(--bg);
  box-shadow: var(--shadow-md);
  pointer-events: none;
  will-change: left, top, width, height;
}

/* dipakai sesaat untuk mengukur posisi akhir kartu tanpa terganggu animasi */
.deck.is-instant .deck-card {
  transition: none !important;
  animation: none !important;
}

.media-item.is-morph-target {
  visibility: hidden;
  animation: none !important;
}

/* =========================================================
   Tablet: 768 sampai 1100
   Tata letaknya masih terbelah dua kolom seperti desktop, tapi kolom kiri
   ditarik mendekat dan kartunya dikecilkan supaya keduanya tidak berhimpit.
   ========================================================= */

@media (min-width: 768px) and (max-width: 1100px) {
  :root {
    --rail-x: 8vw;
    --deck-x: 68vw;
    --deck-size: 30rem;
    --dots-x: 4vw;
    --split-x: 47vw;
  }

  .rail { gap: 4.5rem; }

  /* Kolom kiri tetap di tengah seperti desktop. Lebarnya dikurangi supaya
     judul pindah baris lebih awal dan jaraknya ke tombol kembali lega. */
  .rail--project { max-width: calc(var(--split-x) - 7.4rem - var(--rail-x) - 6.5rem); }

  .note { padding: 4.5rem 4rem; gap: 4rem; }
  .note-body, .note-list { font-size: 2.1rem; }
  .about { padding: 18vh var(--rail-x) 24vh; }
  .prose, .awards { font-size: 2.6rem; max-width: 18em; }
  .globe { right: 5vw; width: min(34vw, 40rem); }
}

/* =========================================================
   Phone: di bawah 768
   Kolom kiri jadi lapisan penuh layar: judul menempel di atas, tombol di
   bawah, kartu di tengah. Ini pola yang sama dengan referensi.
   ========================================================= */

@media (max-width: 767px) {
  /* di HP judul tetap satu baris, kalau kepanjangan diperkecil pasJudul() */
  .title, .subtitle { white-space: nowrap; }

  :root {
    --deck-x: 50vw;
    --deck-size: min(70vw, 48vh);
    /* di layar kecil putarannya sedikit dikurangi supaya tetangganya
       masih terbaca sebagai kartu, bukan garis */
    --deck-tilt-1: 60deg;
    --deck-tilt-2: 76deg;
    --deck-gap-1: 0.86;
    --deck-gap-2: 1.09;
    --r-card: 2.4rem;
    --r-media: 1.8rem;
    --r-btn: 1.2rem;
  }

  .rail {
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    transform: none;
    max-width: none;
    padding: 2.6rem 2rem calc(4rem + env(safe-area-inset-bottom, 0px));
    align-items: center;
    justify-content: space-between;
    text-align: center;
    gap: 0;
  }

  /* diubah lewat variabel supaya kotak masker ikut menyesuaikan */
  :root { --title-fs: 1.95rem; }
  .title-block { align-items: center; }

  .pill { height: 4.8rem; padding: 0 1.5rem; }
  .pill .ico { width: 2rem; height: 2rem; }
  .pill .lbl { font-size: 1.4rem; }
  .actions { gap: 1rem; }
  /* Deretan tombol ditambat langsung ke tepi bawah layar, bukan ke dasar
     wadah setinggi layar. Chrome Android dan Safari iOS hanya menggeser
     elemen bersama bilah alamat dengan mulus kalau elemen itu sendiri yang
     ditambat ke bawah. Ditambat lewat wadah, tombolnya tertinggal sesaat
     lalu menyusul, terasa terseret saat halaman di-scroll. Dipusatkan dengan
     margin otomatis, bukan transform, karena transform dipakai animasi masuk. */
  .rail .actions,
  .about-head .actions {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(4rem + env(safe-area-inset-bottom, 0px));
    width: max-content;
    margin-inline: auto;
  }
  .socials { gap: 1rem; }

  .deck-card .card-name { left: 2rem; bottom: 1.8rem; font-size: 1.4rem; }
  .mock-tag { left: 2rem; top: 1.8rem; font-size: 1.15rem; }

  .dots { right: 4vw; gap: 1rem; }

  /* peredam di tepi atas dan bawah supaya nama dan tombol tidak
     bertabrakan dengan kartu yang lewat di belakangnya */
  .rail::before,
  .rail::after {
    content: "";
    position: fixed;
    left: 0;
    right: 0;
    height: 9rem;
    z-index: -1;
    pointer-events: none;
  }
  .rail::before { top: 0;    background: linear-gradient(180deg, var(--bg) 28%, rgba(246, 246, 246, 0)); }
  .rail::after  { bottom: 0; background: linear-gradient(0deg,   var(--bg) 28%, rgba(246, 246, 246, 0)); }

  /* Di halaman project media ter-scroll lewat di belakang judul, jadi peredam
     atasnya dibuat lebih tinggi: padat sampai melewati subjudul, lalu meluruh
     pelan ke transparan. */
  .rail--project::before {
    height: 12.5rem;
    background: linear-gradient(180deg,
      var(--bg) 0%,
      var(--bg) 66%,
      rgba(246, 246, 246, 0.6) 84%,
      rgba(246, 246, 246, 0) 100%);
  }

  /* Peredam bawah: lebih tipis dari yang atas karena tombolnya sudah pil
     putih bershadow. Tugasnya cuma memberi napas, jadi padat sampai kira-kira
     tengah tombol lalu meluruh ke atas. */
  .rail--project::after {
    height: 13rem;
    background: linear-gradient(0deg,
      var(--bg) 0%,
      var(--bg) 45%,
      rgba(246, 246, 246, 0.7) 62%,
      rgba(246, 246, 246, 0.25) 82%,
      rgba(246, 246, 246, 0) 100%);
  }

  /* project: media turun ke bawah judul */
  .media {
    margin-left: 0;
    padding: 24vh 1.6rem 12rem;
    gap: 1.6rem;
  }
  .media-item { height: 58vh; min-height: 26rem; }
  .note {
    min-height: auto;
    padding: 3.2rem 2.4rem;
    gap: 3.2rem;
  }
  .note-body, .note-list { font-size: 1.8rem; max-width: none; }
  .note-label, .note-credit { font-size: 1.25rem; }

  /* Di tepi kanan, sebaris dengan deretan tombol bawah. Ditambat ke tepi
     bawah lewat CSS saja, tanpa ukuran dari JavaScript. Di Safari iPhone
     tinggi layar berubah saat bilah alamat mengecil dan membesar. Posisi
     piksel yang dihitung sekali membuat tombol ini tertinggal lalu melompat,
     sedangkan tambatan bawah ikut bergerak bersama deretan tombol. Tinggi
     tombol ini sama dengan pil di deretan (4,8rem), jadi sejajar. */
  .edge-btn {
    left: auto;
    right: 2rem;
    top: auto;
    bottom: calc(4rem + env(safe-area-inset-bottom, 0px));
    transform: none;
    width: auto;
    min-width: 4.8rem;
    height: 4.8rem;
    padding: 0 1.4rem;
  }

  /* teks mulai di bawah peredam atas, jadi saat diam tidak ada baris yang pudar */
  .about { padding: max(17vh, 14.5rem) 2rem 13rem; max-width: none; }
  .prose, .awards { font-size: 1.95rem; max-width: none; }
  .prose p { margin-bottom: 2.8rem; }
  .awards { margin-bottom: 2.8rem; }
  .awards-label { font-size: 1.25rem; }

  /* Di layar kecil about berbalik dari desktop: nama dipaku di atas dan
     tombol dipaku di bawah, cuma teksnya yang ter-scroll dan lewat di
     belakang keduanya. Pola ini diambil dari referensi, alasannya tombol
     jadi tetap terjangkau ibu jari sepanjang halaman. */
  .about-head {
    position: fixed;
    inset: 0;
    z-index: 50;
    margin: 0;
    padding: 2.6rem 2rem calc(4rem + env(safe-area-inset-bottom, 0px));
    gap: 0;
    justify-content: space-between;
    align-items: center;
    text-align: center;
    pointer-events: none;
  }
  .about-head > * { pointer-events: auto; }
  .about-head .title-block { align-items: center; }

  /* peredam di tepi atas dan bawah, sama seperti di home. Tombol berupa pil
     putih bershadow sebenarnya sudah terbaca di atas apa pun, tapi nama itu
     teks polos jadi butuh alasnya. */
  .about-head::before,
  .about-head::after {
    content: "";
    position: fixed;
    left: 0;
    right: 0;
    height: 9rem;
    z-index: -1;
    pointer-events: none;
  }
  /* ukuran dan gradien sama dengan peredam halaman project */
  .about-head::before {
    top: 0;
    height: 12.5rem;
    background: linear-gradient(180deg,
      var(--bg) 0%,
      var(--bg) 66%,
      rgba(246, 246, 246, 0.6) 84%,
      rgba(246, 246, 246, 0) 100%);
  }
  .about-head::after {
    bottom: 0;
    height: 13rem;
    background: linear-gradient(0deg,
      var(--bg) 0%,
      var(--bg) 45%,
      rgba(246, 246, 246, 0.7) 62%,
      rgba(246, 246, 246, 0.25) 82%,
      rgba(246, 246, 246, 0) 100%);
  }

  /* di layar kecil globe ikut alur teks, bukan menempel di kanan */
  .globe {
    position: relative;
    right: auto;
    top: auto;
    transform: none;
    width: min(72vw, 34rem);
    margin: 2rem auto 0;
  }
  .globe-label { font-size: 1.25rem; }
}

/* =========================================================
   Hormati preferensi gerak minimal
   ========================================================= */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .deck-card { filter: none !important; }
}

/* Petunjuk geser di dek. Pil kaca tipis di bawah kartu aktif, ikonnya
   bergerak pelan. Muncul setelah dek selesai masuk. Di HP dan layar sentuh
   tulisannya "Swipe to browse", di desktop dengan mouse "Scroll to browse". */
.swipe-hint {
  position: fixed;
  left: var(--deck-x);
  top: calc(50% + var(--deck-size) / 2 + 1.4rem);
  /* di atas lapisan dek (z-index 10), kalau tidak kartu bawah menutupinya */
  z-index: 11;
  display: flex;
  align-items: center;
  padding: 0.8rem 1.4rem 0.8rem 1rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.62);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  backdrop-filter: blur(14px) saturate(1.2);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 0 0 1px rgba(17, 17, 17, 0.055), 0 1px 2px rgba(17, 17, 17, 0.05), 0 8px 20px -8px rgba(17, 17, 17, 0.12);
  color: var(--ink-2);
  font-size: 1.25rem;
  letter-spacing: -0.005em;
  white-space: nowrap;
  pointer-events: none;
  transform: translateX(-50%);
  opacity: 0;
  animation: sh-in 0.6s var(--ease) 1.6s forwards;
  transition: opacity 0.4s var(--ease), transform 0.4s var(--ease);
}
.swipe-hint.is-gone { opacity: 0 !important; transform: translate(-50%, 0.6rem); animation: none; }
.sh-touch, .sh-mouse { display: flex; align-items: center; gap: 0.7rem; }
.sh-touch { display: none; }
@media (max-width: 767px), (pointer: coarse) {
  .sh-touch { display: flex; }
  .sh-mouse { display: none; }
}
.sh-arrows { display: flex; flex-direction: column; width: 1.4rem; animation: sh-bob 1.8s ease-in-out 2.2s infinite; }
.sh-arrows svg { width: 1.4rem; height: 1.1rem; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.sh-arrows svg + svg { margin-top: -0.3rem; }
.sh-wheel { width: 1.6rem; height: 1.6rem; fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; }
.sh-dot { animation: sh-roll 1.8s ease-in-out 2.2s infinite; }
@keyframes sh-in { from { opacity: 0; transform: translate(-50%, 0.6rem); } to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes sh-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-0.25rem); } }
@keyframes sh-roll { 0%, 100% { transform: translateY(0); opacity: 1; } 60% { transform: translateY(2.5px); opacity: 0.2; } }
@media (prefers-reduced-motion: reduce) { .sh-arrows, .sh-dot { animation: none; } }


/* ---------------------------------------------------------
   Tombol suara sebagai pemutar mini
   Menyala: tombol memanjang berisi visualizer ASCII, judul lagu di tooltip.
   Hover saat menyala: ikon berganti speaker dicoret, tanda ketuk = matikan.
   --------------------------------------------------------- */
.sound-pill .viz {
  display: block;
  max-width: 0;
  overflow: hidden;
  white-space: nowrap;
  opacity: 0;
  transition: max-width 0.55s var(--ease), opacity 0.35s ease;
}
.sound-pill .viz {
  font: 500 1.45rem/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  letter-spacing: 0.04em;
  color: var(--ink);
}
.sound-pill.is-playing { gap: 0.85rem; border-radius: 999px; }
.sound-pill.is-playing .viz { max-width: 10rem; opacity: 1; }
/* label hover "Music" hanya untuk keadaan diam */
.sound-pill.is-playing .lbl { display: none; }
@media (hover: hover) and (pointer: fine) {
  .sound-pill.is-playing:hover .ico-swap .ico-a { opacity: 0; transform: scale(0.6); }
  .sound-pill.is-playing:hover .ico-swap .ico-b { opacity: 1; transform: none; }
}
@media (max-width: 767px) {
  .sound-pill .viz { font-size: 1.15rem; letter-spacing: 0.02em; }
  .sound-pill.is-playing { gap: 0.6rem; }
}
