/* ============================================================
   Orratto — styles.css   |  Panduan lengkap: README.md
   ------------------------------------------------------------
   BREAKPOINT (hanya ini, urut dari default):
     default (HP)      : tema GELAP, video full-bleed, topbar melayang,
                         rail aksi di dalam video
     >=768 & <=1023    : tablet — feed jadi kolom 560px terpusat
     >=1024            : desktop — tema TERANG, sidebar, kartu 9/16,
                         rail aksi DI LUAR kartu, topbar disembunyikan
     max-height:520px  : HP lanskap — rail & caption dirapatkan
     max-width:360px   : HP sempit — topbar anti-sesak
   Tema ikut layout, bukan preferensi OS: var di :root = gelap,
   di-override jadi terang pada >=1024.
   ------------------------------------------------------------
   --botnav-h = tinggi nav bawah, DAN sekaligus yang menjaga .feed tidak persis
   seukuran viewport. Dua peran itu sengaja dipegang satu angka; riwayatnya:

   RIWAYAT --top-gap (DICABUT 30 Agustus 2026, jangan dihidupkan lagi)
   Chrome Android mempromosikan scroller yang PERSIS mengisi viewport (posisi
   0,0 + ukuran sama persis) menjadi implicit root scroller, sehingga scroll di
   dalamnya menggerakkan chrome peramban. Dua percobaan 23 Agt 2026 di produksi:
     inset:0  -> GAGAL. Bilah alamat hilang saat swipe turun, tinggi video
                 MELOMPAT saat scroll-snap mengunci, bilah muncul lagi saat naik.
     1px      -> LULUS di satu HP (milik DimDim) saja. TIDAK diadopsi: marginnya
                 terlalu tipis terhadap zoom, skala tampilan Android, dan
                 pembulatan sub-piksel di layar DPI tinggi — dan kegagalannya
                 SENYAP (tidak ada galat; pengguna cuma merasa feed kaku).
   Dipakailah 6px, selama berbulan-bulan, nol keluhan.
   30 Agt 2026 nav bawah masuk ke feed dan MENYITA ruang, jadi .feed kini
   viewport dikurangi 50px + safe-area. Syaratnya tetap dipenuhi — kali ini oleh
   50px STRUKTURAL, bukan 6px yang berdiri di tepi. 6px jadi mubazir dan
   dicabut. Yang HARUS tetap benar supaya pencabutan itu sah:
     1. di bawah 1024px, .feed yang DI-SCROLL peramban tidak pernah persis
        seukuran viewport. Sampai 19 September 2026 itu berarti "nav bawah
        TIDAK PERNAH absen, termasuk untuk tamu".
        SEJAK 20 SEPTEMBER 2026 (keputusan DimDim) syarat ini dipenuhi dengan
        DUA cara: pengguna yang masuk tetap punya nav 50px; TAMU tidak punya
        nav dan .feed-nya memang seukuran viewport — tapi di bawah 1024px
        .feed adalah PAGER (overflow:clip, digeser app.js), bukan scroller,
        jadi tidak ada yang bisa dipromosikan. Pager dan nav tamu SATU PAKET:
        mencabut pager tanpa mengembalikan nav tamu = bug 23 Agt kembali untuk
        tamu saja. tools/test-pager.mjs mengunci pasangan itu.
     2. potongan .shell dan tampil/tidaknya nav bergerak BERSAMA: nav dirakit
        shell.js, dan atribut data-nonav yang membatalkan potongannya dipasang
        tepat di atasnya dari syarat yang sama (test-mute-nonav.mjs memeriksa
        kedua syarat itu identik).
   Kalau salah satu syarat itu dilanggar, .feed kembali seukuran viewport di
   keadaan tersebut dan bug 23 Agt hidup lagi — senyap.
   BELUM ADA BUKTI LAPANGAN untuk pencabutan ini per 30 Agt 2026; tidak bisa
   diuji lewat jsdom/emulator, butuh HP fisik.
   --ui-top-pad:6px TIDAK ada hubungannya dengan itu (dan bukan sisa --top-gap):
   ia mengatur topbar/.mute/.toast. Jangan digabung.
   env(safe-area-inset-*) berfungsi karena <meta viewport-fit=cover>.
   ============================================================ */
/* THEME follows layout: narrow = dark (default) · wide = light (>=1024px) */
  :root{
    --bg:#121212;--surface:#1C1C1C;--text:#FFFFFF;--muted:#8B949E;
    --line:rgba(255,255,255,.14);--hover:rgba(255,255,255,.08);
    --bubble:rgba(255,255,255,.08);--bubble-hover:rgba(255,255,255,.16);
    --accent:#FFFFFF;--btn-bg:#FFFFFF;--btn-fg:#121212;
    --topbar-h:56px;--sidebar-w:240px;--pad-card:20px;--rail-w:44px;
    --botnav-h:50px;
    --ui-top-pad:6px;
    --top-inset:calc(var(--ui-top-pad) + env(safe-area-inset-top));
  }
  @media (min-width:1024px){
    :root{
      --bg:#FFFFFF;--surface:#FFFFFF;--text:#0F1419;--muted:#5B6875;
      --line:#E6E9EB;--hover:rgba(0,0,0,.05);
      --bubble:rgba(0,0,0,.05);--bubble-hover:rgba(0,0,0,.10);
      --accent:#0F1419;--btn-bg:#0F1419;--btn-fg:#FFFFFF;--rail-w:48px;
    }
  }
  *{box-sizing:border-box;margin:0;padding:0}

  /* ============================================================
     TRANSISI ANTAR HALAMAN (31 Agustus 2026)
     ------------------------------------------------------------
     Silang-pudar bawaan peramban saat berpindah halaman. TIDAK ada JavaScript,
     TIDAK ada router, TIDAK ada perubahan struktur — mencabutnya = menghapus
     blok ini.

     KENAPA DI styles.css: kedua dokumen (asal DAN tujuan) harus sama-sama
     memilih ikut, kalau tidak transisinya tidak menyala sama sekali.
     styles.css dimuat ke-24 halaman, jadi satu aturan menutup semua pasangan
     navigasi. JANGAN memindahkannya ke page.css — index.html tidak memuat
     page.css, dan feed justru halaman asal yang paling sering.

     DIGERBANGI no-preference, bukan dicabut di `reduce`. Polanya sengaja
     terbalik: default TIDAK ada transisi, lalu dinyalakan kalau orangnya tidak
     meminta gerak dikurangi. Kalau ditulis sebaliknya (nyala lalu dimatikan di
     `reduce`), peramban yang tidak mengerti kuerinya tetap dapat animasi.

     ANGKA YANG MENDASARINYA (diukur DimDim 31 Agt, Chrome Fast 4G, cache
     panas, 5 putaran, 3 dicatat): 575 / 590 / 556 ms feed -> /dashboard.
     Yang penting DIPAHAMI: jeda ~570 ms itu SUDAH ADA sekarang — peramban
     menahan halaman lama selama menunggu halaman baru. Transisi tidak
     menambahnya; ia hanya mengganti pergantian mendadak di ujungnya dengan
     silang-pudar. Kalau suatu saat navigasi jadi jauh lebih lambat, tinjau
     ulang: di atas 4 detik peramban MELEWATKAN transisinya sendiri
     (TimeoutError), jadi gejalanya = transisi hilang sesekali, bukan galat.

     DUKUNGAN: Chromium 126+ dan Safari 18.2+. Firefox BELUM merilis versi
     lintas-dokumennya di rilis stabil; MDN menandainya "Limited availability",
     bukan Baseline (diverifikasi 31 Agt 2026 — beberapa artikel 2026 mengklaim
     sebaliknya dan itu bertentangan dengan data kompatibilitas resmi).
     Peramban tanpa dukungan bernavigasi seperti biasa. Tidak ada yang rusak.

     TIDAK berlaku untuk: mengetik URL di bilah alamat, bookmark, dan segala
     bentuk muat ulang — hanya navigasi yang dipicu orang mengklik isi halaman.
     Jadi menguji dengan refresh TIDAK akan memperlihatkan apa pun; harus lewat
     tap nav bawah / tautan.

     SENGAJA TANPA view-transition-name di mana pun. Dua elemen terlihat yang
     berbagi nama yang sama membuat peramban MELEMPAR dan melewatkan seluruh
     transisi — dan Orratto punya banyak elemen berulang (kartu, baris, avatar).
     Silang-pudar root saja sudah seluruh ruang lingkupnya. Kalau suatu saat
     ingin memorf satu elemen lintas halaman, itu keputusan sendiri, dan
     keunikan namanya harus dibuktikan lebih dulu.
     ============================================================ */
  @media (prefers-reduced-motion:no-preference){
    @view-transition{navigation:auto}
  }

  /* ============================================================
     [hidden] HARUS MENANG — 17 Agustus 2026
     ------------------------------------------------------------
     Aturan `[hidden]{display:none}` dari peramban berasal dari UA stylesheet,
     dan SETIAP aturan author mengalahkannya. Jadi begitu sebuah kelas menyetel
     `display:` apa pun, `el.hidden = true` di JS berhenti berpengaruh —
     diam-diam, tanpa galat, tanpa satu pun tanda di konsol.

     TIGA kebocoran hidup ditemukan sekaligus saat uji produksi 17 Agustus:
       .cr-drop      -> kotak "Select a video" tetap tampil di atas komposer
                        yang sudah terbuka (ini yang terlihat di layar dan
                        memicu penyisiran ini)
       .bsheet__cta  -> tombol "Visit site" tetap tampil pada postingan yang
                        TIDAK punya tautan produk — tombol mati ber-href "#"
       .btn          -> tombol "Remove" foto tetap tampil pada akun yang belum
                        punya foto sama sekali

     Sebelumnya kelas ini ditambal satu per satu (.db-empty[hidden],
     .po-act[hidden], .pf-adm[hidden], dan empat lainnya). Menambal per
     selektor berarti kebocoran BERIKUTNYA cuma menunggu kelas baru yang
     menyetel display — dan tidak ada yang akan menyadarinya sampai seseorang
     kebetulan melihat layarnya.

     !important dipakai dengan sengaja: tanpa itu spesifisitasnya seri dengan
     selektor kelas (0,1,0) dan yang menang cuma soal urutan baris — rapuh
     persis pada hal yang sedang dijaga di sini.

     Penambal per-selektor yang lama SENGAJA DIBIARKAN: menghapusnya cuma
     menambah risiko tanpa menambah apa pun.
     Dijaga oleh tools/test-hidden-cascade.mjs. */
  [hidden]{display:none!important}
  html,body{height:100%;overflow:hidden;overscroll-behavior:none}
  body{font-family:Arial,Helvetica,sans-serif;background:var(--bg);color:var(--text);
    -webkit-font-smoothing:antialiased;overflow:hidden;transition:background-color .3s ease,color .3s ease}
  button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
  a{color:inherit;text-decoration:none}
  .ico{width:22px;height:22px;display:block;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
  .ico.fill{fill:currentColor;stroke:none}
  .logo svg{display:block;width:auto}

  /* ===== MOBILE TOPBAR ===== */
  /* TINGGI TOPBAR 56px, turun dari 68px (14 September 2026). 68 lebih tinggi
     dari semua pembanding: nav bar iOS 44pt, app bar Material 56dp, header
     Instagram/TikTok di HP sekitar 44-48px. Di HP tinggi adalah barang paling
     langka, dan 12px itu murni dibayar oleh video. Isinya setinggi 38-40px,
     jadi di 56px masih ada ~8px atas-bawah. Posisi pil mute dan toast membaca
     token yang sama, jadi keduanya ikut naik tanpa disentuh.

     SLOT AKSI KOSONG TIDAK BOLEH MEMBAYAR GAP. .topbar flexbox dengan gap:8px,
     dan anak selebar NOL tetap anak. Gaya .tb-act sendiri hidup di page.css —
     jadi di FEED ia bahkan tidak punya gaya apa pun, cuma <div> kosong yang
     tetap dihitung. Akibatnya tombol search terdorong 8px ke kiri: 28px dari
     tepi kanan sementara logo 20px dari tepi kiri, tidak lagi sejajar.
     Aturannya DI SINI, bukan di page.css, karena feed tidak memuat page.css —
     pelajaran yang sama yang menggigit tombol back 14 September.
     display:none mencabutnya dari perhitungan flex; visibility:hidden atau
     width:0 TIDAK cukup, keduanya tetap anak flex dan tetap membayar gap. */
  .tb-act:empty{display:none}
  .topbar{position:fixed;top:0;left:0;right:0;height:calc(var(--topbar-h) + var(--top-inset));z-index:40;display:flex;align-items:center;
    gap:8px;padding:var(--top-inset) var(--pad-card) 0;background:transparent;border-bottom:none;pointer-events:none}
  .topbar > *{pointer-events:auto}
  .topbar .menu{pointer-events:auto}
  .topbar .logo{filter:drop-shadow(0 1px 5px rgba(0,0,0,.45));color:#fff;display:inline-flex;align-items:center;min-height:44px}
  .topbar .iconbtn{background:rgba(0,0,0,.5);color:#fff;width:38px;height:38px}
  .topbar .iconbtn .ico{width:24px;height:24px;stroke-width:2}
  .topbar .btn-primary{background:rgba(0,0,0,.5);color:#fff;height:38px;font-weight:600}
  .topbar .logo svg{height:22px}
  /* Tombol back topbar: TERSEMBUNYI DI MANA-MANA secara bawaan, dan hanya
     page.css (§3b-pra) yang menampilkannya kembali di /@handle orang lain dan
     /post untuk pengguna yang sudah masuk.

     ATURANNYA DI SINI, BUKAN DI page.css. styles.css dimuat SEMUA halaman
     termasuk feed; page.css hanya yang ber-html.page. Aturan bawaan yang
     ditulis di page.css membuat tombol ini tidak pernah disembunyikan di feed
     — ia muncul di antara logo dan tombol search. Itu terjadi sungguhan pada
     14 September 2026: lolos uji (penjaganya membaca page.css saja, dan di
     sana aturannya memang ada) dan baru ketahuan dari HP DimDim. Pelajarannya
     bukan "hati-hati" melainkan: aturan BAWAAN milik berkas yang dimuat semua
     halaman; pengecualian per halaman milik page.css. */
  .topbar .tb-back{display:none}
  .grow{flex:1}
  .iconbtn{width:38px;height:38px;border-radius:50%;display:grid;place-items:center;color:var(--text);
    background:var(--bubble);transition:background .15s;flex:none}
  .btn{display:inline-flex;align-items:center;justify-content:center;font-weight:600;font-size:14px;
    height:38px;padding:0 16px;border-radius:999px;white-space:nowrap;transition:transform .1s,opacity .15s,background .15s}
  .btn:active{transform:scale(.98)}
  .btn-primary{background:var(--btn-bg);color:var(--btn-fg)}
  .avatar{width:38px;height:38px;border-radius:50%;box-shadow:inset 0 0 0 1px var(--line);flex:none}
  /* Foto profil pemilik akun di tombol menu. background-size:cover supaya
     gambar non-persegi tidak gepeng; warna solid tetap jadi latar selama
     gambarnya belum termuat. */
  .avatar.has-pfp{background-size:cover;background-position:center;background-repeat:no-repeat}

  /* ============================================================
     AVATAR BAWAAN — SATU aturan untuk SEMUA permukaan (15 Agustus 2026)
     ------------------------------------------------------------
     Ada di styles.css, bukan page.css, karena styles.css dimuat SETIAP
     halaman termasuk feed; page.css tidak. Sebelumnya siluetnya hanya
     didefinisikan di page.css, sementara `.avatar` di topbar cuma
     `background:#5b7183` — lingkaran biru-abu POLOS tanpa siluet. Hasilnya
     dua avatar bawaan yang berbeda di satu aplikasi, dan yang di topbar tidak
     pernah ikut diperbarui waktu siluetnya dibuat 3 Agustus.

     BENTUKNYA mengikuti pola Instagram/X/Google, dan dua hal itu yang dulu
     tidak ada: (1) LATAR BERTINT, supaya lingkarannya terbaca sebagai avatar
     kosong, bukan lubang; (2) proporsi kepala+bahu yang mengisi lingkaran —
     versi lama memakai figur kecil dengan celah lebar di atas kepala dan bahu
     yang terpotong di tepi, sehingga terlihat seperti gambar yang gagal muat.

     SVG inline sebagai data URI, bukan berkas di R2: memuatnya lewat jaringan
     berarti membayar satu permintaan untuk gambar yang sama di setiap profil
     tanpa foto. CSP sudah mengizinkan img-src data:.

     Foto sungguhan menimpanya lewat el.style.backgroundImage (inline, dipasang
     Shell.paintBg) yang selalu menang atas aturan lembar gaya. */
  .avatar,.pf-pic,.pfe-pic,.po-who .po-pfp,.db-avatar,.sr-av{
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' fill='%23E4E8EC'/%3E%3Ccircle cx='32' cy='22' r='10' fill='%239AA4AE'/%3E%3Cpath d='M9 64c0-14.3 10.3-26 23-26s23 11.7 23 26Z' fill='%239AA4AE'/%3E%3C/svg%3E");
    background-size:cover;background-position:center;background-repeat:no-repeat}
  body.signed-in .m-login{display:none}
  /* search hanya untuk yang sudah masuk (guest: tombolnya tidak ditampilkan) */
  body:not(.signed-in) #msearch{display:none}
  /* MENU AVATAR TOPBAR HP — DICABUT 30 Agustus 2026.
     Isinya empat item: Profile, Dashboard, Create, Settings. Tiga yang pertama
     sekarang ada di nav bawah, yang juga tampil di feed sejak hari yang sama.
     Yang tersisa cuma Settings, dan jalannya sudah ada: Profile -> Settings.

     Ini BUKAN jalur baru. page.css:45 sudah menyembunyikan .m-acct di SEMUA
     halaman non-feed sejak lama, dengan komentar yang menyebut jalur itu apa
     adanya. Feed adalah satu-satunya tempat menu ini masih hidup di HP — sisa
     dari zaman feed tidak punya nav bawah. Mencabutnya membuat feed berhenti
     jadi pengecualian, alasan yang sama dengan nav bawah itu sendiri.
     Norma pembanding: Instagram & TikTok di HP tidak punya menu avatar di
     topbar; Settings dicapai lewat tab Profile.

     KONSEKUENSI YANG DITERIMA: di HP, Settings kini HANYA bisa dicapai lewat
     halaman profil. Kalau pintu Settings di sana kelak dipindah atau
     disembunyikan, Settings jadi tak terjangkau sama sekali di HP.

     Aturannya dimatikan di sini, MARKUP-nya di shell.js TIDAK dihapus:
     #mAcctBtn masih dipakai paintMyAvatar() bersama #acctBtn, dan .m-acct
     tetap punya aturan sendiri di baris >=1024px. Menghapus markupnya berarti
     menyentuh keduanya tanpa keuntungan apa pun.
     DESKTOP TIDAK TERPENGARUH: avatar desktop memakai jalur lain sama sekali
     (.auth-in di .auth-float, #acctBtn), bukan .m-acct. */
  .m-acct{display:none}

  /* ===== SEARCH SURFACE (full-screen overlay) ===== */
  .search-surface{position:fixed;inset:0;z-index:80;background:var(--bg);display:none;flex-direction:column}
  body.search-open .search-surface{display:flex}
  body.search-open .media{animation-play-state:paused}
  .search-head{display:flex;align-items:center;gap:10px;
    padding:10px 12px;padding-top:calc(10px + env(safe-area-inset-top));border-bottom:1px solid var(--line)}
  .search-field{flex:1;display:flex;align-items:center;gap:8px;height:42px;padding:0 6px 0 14px;
    background:var(--bubble);border-radius:999px;min-width:0}
  .search-field input{flex:1;min-width:0;background:none;border:none;outline:none;color:var(--text);font-size:15px}
  .search-field input::placeholder{color:var(--muted)}
  .search-clear{width:30px;height:30px;border-radius:50%;display:none;place-items:center;color:var(--muted);flex:none}
  .search-field.has-text .search-clear{display:grid}
  .search-results{flex:1;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:4px 0;scrollbar-width:none}
  .search-results::-webkit-scrollbar{display:none}
  .sr-row{display:flex;align-items:center;height:44px;padding:0 18px;font-size:14px;color:var(--text);-webkit-tap-highlight-color:transparent}
  .sr-row:active{background:var(--hover)}
  .sr-empty{padding:22px 18px;color:var(--muted);font-size:14px}
  /* pengaman: overlay search HP tak boleh muncul di desktop (pemicunya #msearch
     memang display:none >=1024px; ini jaring kedua kalau-kalau body.search-open
     ikut tersetel). Desktop pakai dropdown sidebar, bukan overlay. */
  @media (min-width:1024px){body.search-open .search-surface{display:none}}
  @media (min-width:1024px){.search-head,.search-results{max-width:640px;margin-left:auto;margin-right:auto;width:100%}}

  /* ===== DESKTOP floating auth ===== */
  .auth-float{display:none;position:fixed;top:16px;right:20px;z-index:45;align-items:center;gap:8px}
  .auth-out{display:flex;gap:8px;align-items:center}
  body.signed-in .auth-out{display:none}
  .auth-in{display:none}

  /* ===== SHELL =====
     POTONGAN NAV BAWAH — SEKARANG DI SEMUA HALAMAN BER-NAV (31 Agustus 2026).

     Sampai 30 Agustus potongan ini hanya berlaku di feed, dan halaman lain
     memberi ruang lewat padding-bottom .page-main (nav MENIMPA). Dua model
     untuk satu nav, dan model kedua punya cacat yang tidak terlihat sampai
     DimDim melaporkannya dari HP:

       .shell{inset:0} penuh viewport -> .app height:100% -> .page-main flex:1
       => .page-main PERSIS seukuran viewport => Chrome mempromosikannya jadi
       implicit root scroller => bilah alamat hilang-muncul saat scroll.

     Itu bug 23 Agustus yang sama, cuma hidup di halaman lain dan tidak pernah
     dicari di sana. Komentar lama di page.css §2 malah mengklaim sebaliknya
     ("browser HP tidak menyembunyikan bar URL-nya") — ditulis sebelum potongan
     nav ada, tidak pernah dikoreksi, dan sudah dibetulkan sekarang.

     Menaikkan potongan ke .shell umum WAJIB dibarengi pencabutan --botnav-h
     dari padding-bottom .page-main (page.css). Kalau tidak, ruang nav terhitung
     DUA KALI dan isi halaman mengambang ~100px di atas dasar layar. Keduanya
     berubah dalam serahan yang sama; jangan pernah mencabut salah satunya.

     safe-area ikut dipotong di sini, jadi .meta/.actions (feed) dan
     .page-main TIDAK boleh menambahkannya lagi — itu terhitung dua kali.

     Potongan ini juga yang menggantikan --top-gap:6px sebagai penjaga agar
     .feed tidak persis seukuran viewport — riwayat lengkap di kepala berkas. */
  .shell{position:fixed;inset:0 0 calc(var(--botnav-h) + env(safe-area-inset-bottom)) 0}
  /* HALAMAN TANPA NAV BAWAH -> tanpa potongan, kalau tidak ada pita kosong
     50px di dasar layar untuk nav yang tidak ada di sana.
     Daftarnya SATU-SATUNYA di CSS (lihat page.css §4 untuk daftar yang sama
     dipakai menyembunyikan navnya) dan dikemudikan data-page, bukan kelas yang
     ditambahkan JS: shell.js dimuat di AKHIR body, jadi kelas dari JS datang
     setelah cat pertama dan halaman ber-HTML statis (/create, /settings*)
     akan berkedip. Atribut data-page sudah ada di HTML sejak byte pertama.
     [data-page^="settings"] mencakup kelimanya: settings, settings-profile,
     settings-handle, settings-account, settings-about.
     [data-page^="dashboard-"] — TANDA HUBUNGNYA WAJIB. Ia mencakup keempat
     SUB-halaman (dashboard-followers, -following, -posts, -post) tetapi TIDAK
     mencakup data-page="dashboard" itu sendiri, yang tetap ber-nav karena ia
     tujuan nav, bukan focused task. Tanpa tanda hubung, /dashboard ikut
     kehilangan navnya dan tab Dashboard jadi tidak bisa ditandai aktif. */
  /* [data-nonav] — SATU-SATUNYA anggota daftar ini yang datang dari JS, bukan
     dari HTML (9 September 2026). Dipasang shell.js pada /@handle MILIK ORANG
     LAIN, yang navnya dicabut tapi tidak bisa dikenali dari data-page: profil
     sendiri dan profil orang lain memakai data-page="account" yang sama.
     Perangkap kedip yang dijelaskan di atas TIDAK berlaku di sana — <main>-nya
     kosong sampai profile.js mengisinya, jadi tidak ada isi yang sempat
     tergambar dengan padding lama. Alasan lengkapnya di shell.js, blok
     PROFIL ORANG LAIN. */
  body[data-page="create"] .shell,
  body[data-page^="dashboard-"] .shell,
  body[data-page^="settings"] .shell,
  body[data-nonav] .shell{inset:0}
  .app{display:flex;height:100%}
  .sidebar{display:none}

  /* ===== FEED — card + peek ===== */
  .feed{flex:1;position:relative;overflow-y:auto;scroll-snap-type:y mandatory;scrollbar-width:none}
  .feed::-webkit-scrollbar{display:none}
  /* ===== PAGER HP (20 September 2026) =====
     .feed-track = pembungkus kartu yang dibuat app.js. Di 1024px KE ATAS ia
     display:contents: kotaknya tidak ada, kartu tetap anak langsung .feed
     dalam tata letak, jadi scroll-snap desktop persis seperti sebelumnya.

     Di BAWAH 1024px app.js memasang .is-pager (lewat matchMedia dengan batas
     YANG SAMA, 1023.98px) dan menggeser .feed-track dengan transform:
       overflow:clip  .feed BUKAN scroller -> Chrome Android tidak punya apa
                      pun untuk dipromosikan jadi root scroller (bug 23
                      Agustus). INILAH yang membuat nav tamu boleh dicabut.
                      `hidden` di depannya hanya cadangan untuk parser lama;
                      semua peramban dalam daftar dukungan membaca `clip`.
       touch-action   geseran jari ke JS, bukan ke peramban. Harganya:
                      cubit-perbesar di area feed HP mati (seperti aplikasi
                      TikTok/Reels).
       transition     durasinya ditulis app.js per gerakan; lengkungnya
                      quintic ease-out ViewPager Android. prefers-reduced-
                      motion mematikannya lewat aturan `*{transition:none}`.
     KELASNYA DARI JS, BUKAN DARI MEDIA QUERY SAJA: kalau app.js tidak jalan,
     .feed tetap scroller biasa (feed tanpa JS memang kosong, tapi tidak
     pernah terkunci). Gerbang media query di sini jaring kedua: kelas yang
     tertinggal di desktop tidak berbuat apa-apa. */
  .feed-track{display:contents}
  @media (max-width:1023.98px){
    .feed.is-pager{overflow:hidden;overflow:clip;scroll-snap-type:none;touch-action:none}
    .feed.is-pager>.feed-track{display:block;height:100%;
      transform:translate3d(0,0,0);transition:transform 0ms cubic-bezier(.23,1,.32,1)}
    .feed.is-pager.pg-drag{-webkit-user-select:none;user-select:none}
  }
  .card{height:100%;scroll-snap-align:start;scroll-snap-stop:always;
    position:relative;display:flex;align-items:stretch;justify-content:center}
  .stage{position:relative;height:100%;width:100%;display:flex;align-items:stretch;justify-content:center}
  @media (min-width:1024px){.stage{width:auto}}

  .media{position:relative;height:100%;width:100%;overflow:hidden;flex:1 1 auto;background:#0a0a0a;
    background-size:cover;background-position:50% 40%}
  .media::after{content:"";position:absolute;inset:0;pointer-events:none;
    background:linear-gradient(180deg,rgba(0,0,0,.42) 0%,rgba(0,0,0,.10) 10%,rgba(0,0,0,0) 24%,rgba(0,0,0,0) 60%,rgba(0,0,0,.28) 74%,rgba(0,0,0,.60) 100%)}
  .vid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0;background:#000}
  /* Video mendatar/persegi DILABUHKAN, tidak dipotong (keputusan DimDim
     6 Sep 2026). Pita atas-bawah HITAM POLOS — bukan latar blur dari video
     itu sendiri: blur berarti merender video kedua di belakang tiap kartu dan
     menjalankan filter sepanjang pemutaran, dan feed ini sengaja dijaga ringan.
     #000 datang dari background .vid di baris atas, jadi tidak ada properti
     tambahan. Yang berubah HANYA cara video mengisi kartu — geometri .card /
     .stage / .media tidak disentuh sama sekali, jadi scroll-snap dan tinggi
     kartu (yang punya sejarah rapuh, lihat --top-gap) tetap utuh. */
  .media.is-wide .vid{object-fit:contain}
  .playpause{position:absolute;inset:0;z-index:2;display:none;place-items:center;pointer-events:none}
  .card.paused .playpause{display:grid}
  .playpause svg{width:76px;height:76px;fill:rgba(255,255,255,.92);filter:drop-shadow(0 2px 14px rgba(0,0,0,.5))}

  .mute{position:fixed;left:20px;top:calc(var(--topbar-h) + var(--top-inset) + 2px);z-index:36;
    height:40px;padding:0 16px 0 13px;border-radius:999px;
    display:inline-flex;align-items:center;gap:7px;
    background:#FFFFFF;color:#0F1419;font-size:14px;font-weight:600;
    box-shadow:0 2px 10px rgba(0,0,0,.22);
    transition:transform .1s,opacity .2s,visibility .2s}
  .mute:active{transform:scale(.97)}
  .mute .ico{width:19px;height:19px;stroke-width:2;flex:none}
  /* Ikon mengikuti keadaan: speaker-dicoret saat bisu, speaker-bergelombang
     saat bersuara. Ditukar di sini, bukan di JS (lihat komentar di index.html). */
  .mute .ico-on{display:none}
  body.sound-on .mute .ico-off{display:none}
  body.sound-on .mute .ico-on{display:block}

  /* ---------- HP: sekali pakai, tombolnya pergi (tidak berubah) ----------
     GERBANG max-width WAJIB, dan ini inti perubahan 14 September 2026.
     Dulu aturan ini polos, jadi berlaku di mana-mana: sekali suara menyala,
     tombolnya lenyap di HP MAUPUN desktop dan suara tidak bisa dimatikan lagi
     di kedua-duanya.

     DimDim memutuskan HP tetap begitu — frame video adalah produk yang dibayar
     brand, dan pil putih permanen di atasnya terlalu berat. Konsekuensi yang
     DIA TERIMA setelah kutandai: di HP tidak ada cara mematikan suara selain
     memuat ulang halaman, dan HP justru tempat masalah itu paling sering
     terjadi (menonton di ruang publik). Ini keputusan produk, bukan kelalaian.

     Di desktop tombolnya MENETAP dan jadi toggle — lihat blok WIDE di bawah.
     JS tidak tahu apa-apa soal pembagian ini: ia selalu toggle, dan di HP
     pointer-events:none yang membuat klik kedua mustahil. Satu perilaku,
     satu gerbang. */
  @media (max-width:1023.98px){
    body.sound-on .mute{opacity:0;visibility:hidden;pointer-events:none}
  }

  /* meta + actions INSIDE .media
     bottom TANPA env(safe-area-inset-bottom) sejak 30 Agustus 2026: .shell
     sudah berhenti di atas safe-area (lihat .shell), jadi dasar kartu tidak
     lagi menyentuhnya. Menambahkannya di sini akan menghitungnya DUA KALI dan
     menaikkan caption ~24-34px tanpa alasan di HP bergestur/berponi.
     Desktop tidak terpengaruh: aturan >=1024px di bawah sudah memakai
     bottom:var(--pad-card) polos sejak dulu. */
  .meta{position:absolute;left:var(--pad-card);right:calc(var(--pad-card) + var(--rail-w) + 14px);bottom:var(--pad-card);z-index:3;color:#fff}
  .meta .who{display:flex;align-items:center;gap:8px;margin-bottom:8px}
  /* a.who: matikan tampilan link bawaan; warisi warna putih .meta. Layout
     tak berubah — selector .who yang sama sudah flex di atas. */
  a.who{text-decoration:none;color:inherit;-webkit-tap-highlight-color:transparent;
    position:relative;width:fit-content}
  /* area sentuh 44px tanpa menggeser tata letak (pola sama dgn .more-link di
     bawah): kotak tak terlihat setinggi 44px di tengah baris .who. */
  a.who::after{content:"";position:absolute;left:0;right:0;top:50%;
    transform:translateY(-50%);height:44px}
  @media (hover:hover) and (pointer:fine){a.who:hover .u{text-decoration:underline;text-underline-offset:2px}}
  .meta .pfp{width:32px;height:32px;border-radius:50%;border:1.5px solid rgba(255,255,255,.85);flex:none;background:#2a2a2a;background-size:cover;background-position:center}
  .meta .u{font-weight:600;font-size:15px}
  /* caption = tombol: ketuk -> memanjang di tempat. Isyaratnya hanya "..."
     bawaan -webkit-line-clamp (tanpa tombol "more" — disengaja, lihat README).
     Klamp ada di .ctxt, bukan di .c: line-clamp pada <button> bermasalah di
     Safari. JS men-disable tombol kalau teks tidak terpotong. */
  .meta .c{display:block;width:100%;max-width:42ch;text-align:left;
    font-size:14px;line-height:1.4;color:rgba(255,255,255,.92);
    -webkit-tap-highlight-color:transparent}
  .meta .c:disabled{cursor:default}
  .meta .ctxt{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  /* dibuka: klamp mati (display:block), tinggi dibatasi + bisa di-scroll
     supaya caption panjang tidak menutupi seluruh video */
  .meta .c.open .ctxt{display:block;max-height:38vh;overflow-y:auto;overscroll-behavior:contain}
  /* quiet "See more" link — reads as post content, not a shop CTA */
  .meta .more-link{display:inline-flex;align-items:center;gap:4px;width:fit-content;
    margin-top:11px;padding:7px 14px;font-size:14px;font-weight:600;color:#fff;
    background:rgba(0,0,0,.55);border-radius:999px;
    -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
    text-shadow:0 1px 3px rgba(0,0,0,.4);-webkit-tap-highlight-color:transparent}
  .meta .more-link svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:2;
    stroke-linecap:round;stroke-linejoin:round;transition:transform .15s}
  @media (hover:hover) and (pointer:fine){.meta .more-link:hover svg{transform:translateX(2px)}}

  .actions{position:absolute;right:var(--pad-card);bottom:var(--pad-card);z-index:5;display:flex;flex-direction:column;gap:15px;align-items:center}
  /* TAMU DI FEED HP (20 September 2026): tanpa nav bawah, .shell tidak lagi
     berhenti di atas safe-area — kartunya turun sampai dasar layar fisik. Di
     sini safe-area DIBAYAR, sekali, supaya judul dan rel aksi tidak duduk di
     bawah garis home iPhone / bilah gestur Android. Ini pasangan aturan
     `body[data-nonav] .shell{inset:0}` untuk feed, sama seperti page.css
     membayarnya untuk .page-main. Pengguna yang masuk tidak terkena: .shell
     mereka masih dipotong, jadi menambahkannya di sana = dihitung DUA KALI. */
  @media (max-width:1023.98px){
    body[data-page="home"][data-nonav] .meta,
    body[data-page="home"][data-nonav] .actions{bottom:calc(var(--pad-card) + env(safe-area-inset-bottom))}
  }
  .act{display:flex;flex-direction:column;align-items:center;gap:4px;color:#fff}
  .act .bubble{width:44px;height:44px;border-radius:50%;display:grid;place-items:center;background:rgba(0,0,0,.55);transition:transform .1s,background .15s}
  .act:active .bubble{transform:scale(.93)}
  .act .n{font-size:13px;font-weight:500;text-shadow:0 1px 6px rgba(0,0,0,.6)}
  .act .ico{width:23px;height:23px}
  .act[aria-pressed="true"] .ico{fill:#ff4d6d;stroke:#ff4d6d}
  .act-more .ico{fill:currentColor;stroke:none}

  /* ===== BOTTOM SHEETS (mobile) / MODAL (desktop) ===== */
  .bsheet-backdrop{position:fixed;inset:0;z-index:55;background:rgba(0,0,0,.55);opacity:0;pointer-events:none;transition:opacity .25s}
  .bsheet-backdrop.open{opacity:1;pointer-events:auto}
  .bsheet{position:fixed;left:0;right:0;bottom:0;z-index:56;background:var(--surface);color:var(--text);
    padding:8px 20px calc(24px + env(safe-area-inset-bottom));border-radius:20px 20px 0 0;
    transform:translateY(100%);pointer-events:none;transition:transform .28s cubic-bezier(.4,0,.2,1),opacity .2s ease}
  .bsheet.open{transform:translateY(0);pointer-events:auto}
  .bsheet__handle{width:40px;height:4px;border-radius:2px;background:rgba(255,255,255,.3);margin:6px auto 16px}
  .bsheet__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
  .bsheet__head h3{font-size:18px;font-weight:700}
  .bsheet__close{width:34px;height:34px;display:grid;place-items:center;border-radius:50%;color:var(--muted)}
  .bsheet__close svg{width:20px;height:20px}
  .bsheet__cta{width:100%;background:#fff;color:#111;border-radius:12px;padding:14px;
    display:flex;align-items:center;justify-content:center;gap:8px;font-weight:700}
  .bsheet__cta svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2}
  .price-row{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:8px}
  .price-amount{font-size:30px;font-weight:800}
  .price-orig{font-size:16px;color:var(--muted);text-decoration:line-through}
  .price-badge{font-size:12px;font-weight:700;color:#fff;background:#e5484d;padding:3px 8px;border-radius:6px}
  .price-desc{font-size:14px;line-height:1.5;color:var(--muted);margin:0 0 18px}
  .qr-wrap{display:flex;flex-direction:column;align-items:center;gap:14px}
  .qr-box{width:190px;height:190px;background:#fff;border-radius:14px;padding:12px;display:grid;place-items:center}
  .qr-box img,.qr-box canvas{width:100%;height:100%;display:block}
  .qr-cap{font-size:14px;color:var(--muted);text-align:center}
  .qr-actions{display:flex;gap:12px;margin-top:18px}
  .qr-btn{flex:1;background:var(--bubble);color:var(--text);border-radius:12px;padding:12px;font-size:14px;font-weight:700;
    display:flex;align-items:center;justify-content:center;gap:8px}
  .qr-btn svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2}
  .more-row{width:100%;display:flex;align-items:center;gap:14px;padding:16px 4px;color:var(--text);font-size:15px}
  .more-row svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:1.9}
  @media (min-width:1024px){
    .bsheet{left:50%;right:auto;bottom:auto;top:50%;width:380px;max-width:calc(100vw - 40px);
      border-radius:18px;padding:20px 22px 24px;transform:translate(-50%,-46%);opacity:0}
    .bsheet.open{transform:translate(-50%,-50%);opacity:1}
    .bsheet__handle{display:none}
  }

  /* chevrons (wide) */
  .scroll-nav{position:fixed;right:20px;top:50%;transform:translateY(-50%);z-index:35;display:flex;flex-direction:column;gap:12px;pointer-events:none;background:none}
  .scroll-nav > *{pointer-events:auto}
  .scroll-nav .chev-up,.scroll-nav .chev-down{display:none}
  .scroll-nav button{width:44px;height:44px;border-radius:50%;display:grid;place-items:center;background:var(--bubble);color:var(--text);transition:background .15s,transform .1s,opacity .15s}
  .scroll-nav button:active{transform:scale(.94)}
  body.at-top .chev-up,body.at-bottom .chev-down{opacity:.3;pointer-events:none}

  /* TIDAK ADA VIDEO DI LAYAR -> TIDAK ADA KENDALI VIDEO (12 September 2026).
     Berlaku pada kerangka muat, feed kosong, dan layar galat. Kelas
     `feed-blank` dipasang app.js; lihat setBlank() di sana untuk kenapa ini
     satu kelas di <body> dan bukan atribut `hidden` per tombol.
     display:none, bukan opacity: tombol yang tak terlihat tapi masih bisa
     diketuk adalah bug yang sama dengan sebelum perbaikan ini. */
  body.feed-blank .mute,
  body.feed-blank .scroll-nav{display:none}


  .toast{position:fixed;left:50%;top:calc(var(--topbar-h) + var(--top-inset) + 12px);transform:translate(-50%,-16px);z-index:70;
    background:var(--surface);color:var(--text);border:1px solid var(--line);border-radius:999px;
    padding:9px 16px;font-size:13px;opacity:0;pointer-events:none;transition:opacity .18s,transform .18s;box-shadow:0 6px 22px rgba(0,0,0,.24)}
  .toast.show{opacity:1;transform:translate(-50%,0)}



  /* profile dropdown (desktop) */
  .acct{position:relative}
  .menu{position:absolute;top:calc(100% + 10px);right:0;min-width:236px;z-index:60;
    background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:6px;
    box-shadow:0 10px 34px rgba(0,0,0,.18);display:none}
  .menu.open{display:block}
  .menu hr{border:none;border-top:1px solid var(--line);margin:6px 4px}
  .menu a,.menu button{display:flex;align-items:center;gap:11px;width:100%;padding:10px 10px;border-radius:9px;
    font-size:14px;font-weight:500;color:var(--text);text-align:left;background:none}
  .menu .ico{width:19px;height:19px}
  @media (hover:hover) and (pointer:fine){.menu a:hover,.menu button:hover{background:var(--hover)}}

  /* area sentuh minimal 44px untuk tombol-tombol kecil — ukuran visual tetap */
  .iconbtn,.avatar,.bsheet__close,.search-clear{position:relative}
  .iconbtn::after,.avatar::after,.bsheet__close::after,.search-clear::after{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:44px;height:44px}
  .btn,.meta .more-link{position:relative}
  .btn::after,.mute::after,.meta .more-link::after{content:"";position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);height:44px}

  :focus-visible{outline:2px solid var(--accent);outline-offset:2px}

  /* hover only on real pointers */
  @media (hover:hover) and (pointer:fine){
    .iconbtn:hover{background:var(--bubble-hover)}
    .btn-primary:hover{opacity:.88}
    .btn-ghost:hover{background:var(--hover)}
    .scroll-nav button:hover{background:var(--bubble-hover)}
    .navitem:hover{background:var(--hover)}
    .dsearch:hover{background:var(--bubble-hover)}
  }

  /* ===== WIDE (>=1024) ===== */
  @media (min-width:1024px){
    .topbar{display:none!important}
    .m-acct{display:none!important}
    /* ---------- PC: mute DI DALAM frame video (14 September 2026) ----------
       Dulu lingkaran abu-abu di rail kanan pada `top:calc(50% - 115px)` —
       satu kolom dengan chevron. Rail itu milik navigasi; kendali audio milik
       video yang sedang tampil (pola TikTok/YouTube Shorts, dan halaman post
       Orratto sendiri). Angka ajaib 50%-115px ikut mati bersama pemindahan
       ini: ia terikat pada tinggi grup chevron dan akan rusak diam-diam kalau
       ukuran chevron kelak diubah.

       NODE-nya dipindahkan app.js ke dalam .media kartu aktif (lihat blok
       MUTE PINDAH KE DALAM FRAME di sana) — bukan dihitung koordinatnya dari
       viewport. .media sudah position:relative, jadi absolute di sini punya
       acuan yang benar. Kalau JS-nya kelak dicabut, tombolnya akan jatuh ke
       acuan lain dan meleset dari frame: dua bagian ini satu paket.

       z-index 4: di atas .playpause (2) dan .vid (0), supaya ikon play besar
       yang muncul saat dijeda tidak menutupinya. */
    /* WARNA: CHIP GELAP + IKON PUTIH — 14 September 2026 (sesi kedua).
       Menggantikan var(--bubble) + var(--text), yang dipasang beberapa jam
       sebelumnya demi SERAGAM DENGAN TOMBOL AKSI (like/Price/QR/Share).
       Tujuan itu SALAH SASARAN dan sekarang resmi gugur:

         tombol aksi duduk di LUAR kartu, di atas latar halaman putih, jadi
         var(--bubble) = rgba(0,0,0,.05) dan var(--text) = #0F1419 terbaca
         di sana. Tombol ini menumpang DI ATAS isi video. Scrim desktop
         (.media::after) hanya menggelapkan bagian BAWAH frame, jadi pojok
         kiri atas adalah video MENTAH.

       Yang tidak terlihat saat itu bukan sekadar "kurang kontras": di klip
       GELAP, ikon #0F1419 di atas isi gelap hilang sama sekali, dan isian 5%
       hitam tidak menolong apa pun. Degradasi chip gelap + ikon putih berjalan
       satu arah saja — di video gelap chip-nya menyatu dengan latar TAPI ikon
       putihnya tetap terbaca. Itu sebabnya YouTube, Instagram Reels, dan TikTok
       memakai pola yang sama untuk kendali di atas media.

       Yang ditukar: keseragaman dengan tombol aksi -> keseragaman dengan norma
       kendali-di-atas-media. KONSEKUENSI YANG DIMDIM TERIMA: pil mute HP tetap
       PUTIH (tidak disentuh — sudah teruji produksi dan tidak bermasalah), jadi
       HP dan desktop kembali berbeda rupa. Kalau kelak HP disamakan, ubah blok
       .mute dasar di atas, bukan blok ini.

       BAYANGAN DIPERTAHANKAN: memberi tepi di klip gelap, tempat chip sendiri
       tidak punya batas. */
    .mute{position:absolute;left:16px;top:16px;right:auto;z-index:4;
      width:44px;height:44px;padding:0;gap:0;border-radius:50%;
      justify-content:center;
      background:rgba(0,0,0,.55);color:#FFFFFF;box-shadow:0 2px 10px rgba(0,0,0,.28)}
    .mute span{display:none}
    .mute .ico{width:21px;height:21px}
    /* ---------- DI LUAR FRAME = TIDAK TAMPIL (15 September 2026) ----------
       left/top di atas adalah koordinat DI DALAM .media. Selama tombol masih
       di rumah asalnya (anak langsung <body>), koordinat yang sama berarti
       POJOK KIRI ATAS HALAMAN. Dilaporkan DimDim: saat orratto.com dibuka,
       tombolnya berkedip di kiri atas sebelum pindah ke dalam feed.

       Dua jeda yang menyebabkannya, dan kenapa keduanya tidak bisa ditutup
       dari JS: (1) dari tampilan pertama sampai app.js berjalan — body belum
       punya `feed-blank` karena kelas itu dipasang app.js sendiri, setelah
       lima skrip termuat; (2) antara kartu pertama dirender (feed-blank
       dilepas) dan IntersectionObserver menyatakannya aktif (placeMute).

       Aturan ini juga benar untuk kartu gerbang login: kartu itu tidak punya
       .media, placeMute mengirim tombolnya pulang, dan tidak ada video untuk
       dikendalikan — prinsip yang sama dengan feed-blank.

       HANYA DESKTOP. Di HP rumah asal itu justru posisi yang benar (pil
       position:fixed di kiri atas). Selektor `body > .mute` bergantung pada
       #mute sebagai anak LANGSUNG <body> di index.html — dijaga
       tools/test-mute-toggle.mjs. */
    body > .mute{display:none}
    /* HOVER PUNYA NILAINYA SENDIRI, bukan var(--bubble-hover).
       Di desktop var(--bubble-hover) = rgba(0,0,0,.10) — LEBIH TERANG dari chip
       rgba(0,0,0,.55), jadi memakainya membuat tombol MEMUDAR saat disentuh:
       kebalikan dari yang seharusnya dirasakan. .68 menebalkan chip tanpa
       mengubah jenisnya.
       (Komentar lama di sini melarang var(--bubble-hover) dengan alasan
       "tombolnya putih" sambil barisnya memakai persis itu — sisa era putih,
       sudah dicabut. Aturan duplikat yang identik di ekor blok ini juga.) */
    @media (hover:hover){.mute:hover{background:rgba(0,0,0,.68)}}
    .scroll-nav{gap:14px}
    /* Nav bawah display:none di desktop, jadi potongannya harus dibatalkan —
       kalau tidak, SEMUA halaman menyisakan 50px kosong di dasar layar.
       Sejak 31 Agustus potongannya ada di `.shell` polos (0,1,0), jadi aturan
       ini juga boleh polos: spesifisitas sama, letaknya lebih bawah, menang.
       PERANGKAP YANG MASIH HIDUP: kalau kelak potongan itu ditulis ulang dengan
       selektor yang lebih spesifik (body[...] .shell = 0,2,0), aturan ini
       KALAH meskipun letaknya lebih bawah — media query tidak menambah
       spesifisitas apa pun. Kasus "aturannya ADA tapi tidak MENANG".
       Aturan tanpa-potongan untuk /create & /settings* (0,2,0) tidak perlu
       disebut di sini: nilainya sudah inset:0, sama dengan yang diinginkan. */
    .shell{inset:0}
      .scroll-nav .chev-up,.scroll-nav .chev-down{display:grid}
    .auth-float{display:flex}
    body.signed-in .auth-in{display:flex;align-items:center}
    .toast{top:20px}

    .sidebar{display:flex;flex-direction:column;width:var(--sidebar-w);flex:none;padding:18px 14px 14px;overflow-y:auto;scrollbar-width:none}
    .sidebar::-webkit-scrollbar{display:none}
    .sidebar .logo{padding:0 10px;margin-bottom:18px;color:#71A3BF}
    .sidebar .logo svg{height:30px}
    .dsearch{display:flex;align-items:center;gap:9px;height:42px;padding:0 14px;margin-bottom:16px;
      border-radius:999px;background:var(--bubble);color:var(--muted);transition:background .15s}
    .dsearch .ico{width:19px;height:19px;flex:none}
    .dsearch input{flex:1;min-width:0;background:none;border:none;outline:none;color:var(--text);font-size:15px}
    .dsearch input::placeholder{color:var(--muted)}
    .dsearch input:disabled{cursor:pointer}

    /* ----- panel saran live (sidebar desktop) — lihat shell.js SEARCH.
       HP tak pernah melihat ini: .sidebar display:none < 1024px. ----- */
    .dsuggest-wrap{position:relative;margin-bottom:16px}
    .dsuggest-wrap .dsearch{margin-bottom:0}          /* margin pindah ke wrapper */
    .dsuggest{position:absolute;left:0;right:0;top:calc(100% + 6px);z-index:45;
      background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:6px;
      box-shadow:0 12px 28px rgba(0,0,0,.12);max-height:320px;overflow-y:auto;scrollbar-width:none}
    .dsuggest::-webkit-scrollbar{display:none}
    .dsuggest[hidden]{display:none}
    .ds-row{display:flex;align-items:center;height:40px;padding:0 12px;border-radius:9px;
      color:var(--text);font-size:14px}
    .ds-row.is-active{background:var(--hover)}
    .ds-n{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    /* .ds-all (baris "See all results") DIHAPUS 6 September 2026 bersama
       halaman /search yang dituju baris itu.
       Penggantinya .ds-empty: BUKAN tombol, cuma teks. Dipakai untuk nol hasil
       DAN untuk galat/rate-limit. Sebelumnya panel ini tak pernah bisa kosong
       karena baris "See all" selalu terpasang; tanpa keadaan kosong, panel
       mengambang yang hampa tidak bisa dibedakan dari "masih memuat".
       Padding & warna disamakan dengan .sr-empty di overlay HP supaya dua
       permukaan yang sama perannya tidak terlihat seperti dua produk. */
    .ds-empty{padding:14px 12px;color:var(--muted);font-size:13.5px}

    .navitem{display:flex;align-items:center;gap:15px;height:52px;padding:0 10px;border-radius:10px;
      color:var(--text);font-weight:500;font-size:17px;transition:background .15s}
    .navitem[aria-current="page"]{font-weight:700}
    .navitem .ico{width:27px;height:27px;flex:none;stroke-width:1.6}
    .nav-an{display:none}
    body.signed-in .nav-an{display:flex}
    .foot{margin-top:14px;padding:16px 10px 0;border-top:1px solid var(--line);display:flex;flex-direction:column;gap:7px}
    .foot a{color:var(--muted);font-size:13px;font-weight:500}
    a{text-decoration:none}
    .foot .cc{color:var(--muted);font-size:13px;margin-top:4px;opacity:.75}

    .card{gap:0;padding:22px 16px;justify-content:center;height:100%}
    .media{border-radius:32px;box-shadow:0 14px 40px rgba(15,20,25,.16);width:auto;flex:0 0 auto;aspect-ratio:9/16;transform:none;opacity:1}
    .meta{right:var(--pad-card);left:var(--pad-card);bottom:var(--pad-card)}
    .actions{position:static;bottom:auto;right:auto;justify-content:flex-end;padding-bottom:8px;gap:18px;margin-left:var(--pad-card)}
    .act{color:var(--text)}
    .act .bubble{width:48px;height:48px;background:var(--bubble)}
    /* (Di sini dulu ada salinan kedua .mute:hover yang identik dengan yang di
       atas — tidak berefek selama nilainya sama, tapi ia BELAKANGAN di blok
       @media yang SAMA, jadi begitu hover mute punya nilai sendiri ia langsung
       menang dan membatalkannya. Dicabut 14 September 2026. Hover mute hidup
       di satu tempat: blok "PC: mute DI DALAM frame video".) */
    .act .n{color:var(--muted);font-size:13px;font-weight:600;text-shadow:none}
    .act[aria-pressed="true"] .ico{fill:#ff4d6d;stroke:#ff4d6d}
    @media (hover:hover){.act:hover .bubble{background:var(--bubble-hover)}}
  }

  /* ===== LOGIN WALL — batas feed untuk guest ===== */
  .gate-card{display:none}
  body:not(.signed-in) .gate-card{display:flex}
  .gate{position:relative;flex:1 1 auto;width:100%;display:flex;flex-direction:column;
    align-items:center;justify-content:center;text-align:center;gap:12px;
    padding:40px 30px calc(40px + env(safe-area-inset-bottom));background:#0d0d0d;color:#fff}
  .gate-title{font-size:23px;font-weight:800;letter-spacing:-.015em}
  .gate-actions{display:flex;flex-direction:column;gap:10px;width:100%;max-width:280px;margin-top:14px}
  .gate-actions .btn{width:100%;height:48px;font-size:15px}
  .gate-actions .btn-ghost{background:transparent;box-shadow:inset 0 0 0 1px var(--line)}
  @media (min-width:1024px){
    .gate-card .stage{width:auto}
    .gate{flex:0 0 auto;height:100%;width:auto;aspect-ratio:9/16;max-height:100%;
      border-radius:32px;background:var(--surface);color:var(--text);box-shadow:0 14px 40px rgba(15,20,25,.16)}
  }


  /* ===== DESKTOP (>=1024) — scrim hanya di bawah (topbar hilang, aksi di luar kartu) ===== */
  @media (min-width:1024px){
    .media::after{background:linear-gradient(180deg,rgba(0,0,0,0) 0%,rgba(0,0,0,0) 52%,rgba(0,0,0,.34) 76%,rgba(0,0,0,.60) 100%)}
    .meta .c{max-width:56ch}                        /* isi lebar kartu; masih dalam 45-75ch */
    .meta .ctxt{-webkit-line-clamp:3}               /* ruang vertikal lebih lega -> 3 baris */
  }
  /* ===== TABLET (768–1023) — feed jadi kolom terpusat, bukan full-bleed melebar ===== */
  @media (min-width:768px) and (max-width:1023px){
    .stage{max-width:560px;margin:0 auto}           /* media + rail aksi ikut ke kolom ini */
    .mute{left:calc(50% - 280px + 8px)}             /* dekatkan pill mute ke kolom */
  }
  /* ===== LAYAR SANGAT PENDEK (HP lanskap) — rapatkan rail & caption ===== */
  @media (max-height:520px) and (max-width:1023px){
    .actions{gap:10px}
    .act .bubble{width:38px;height:38px}
    .act .ico{width:20px;height:20px}
    .meta .ctxt{-webkit-line-clamp:1}
    .meta .c.open .ctxt{max-height:30vh}
    .meta .more-link{margin-top:7px}
  }
  /* ===== HP SANGAT SEMPIT (<=360) — cegah topbar sesak ===== */
  @media (max-width:360px){
    .topbar{gap:6px}
    .topbar .btn-primary{padding:0 12px}
  }

  @media (prefers-reduced-motion:reduce){.media{animation:none}*{transition:none!important}}

  /* ===== NAV BAWAH — HP saja =====
     PINDAH DARI page.css 30 Agustus 2026. Alasannya: index.html (feed) memuat
     styles.css SAJA, tanpa page.css. Sejak nav bawah tampil di semua halaman
     termasuk feed, aturannya harus ada di lembar gaya yang SEMUA halaman muat —
     kalau tidak, feed menyuntik <nav> tanpa gaya sama sekali.
     Tinggi nav = var(--botnav-h): angka yang sama juga memotong .shell, jadi
     mengubahnya di satu tempat mengubah tinggi .feed sekalian. Jangan pernah
     menulis 50px langsung di sini. */
  .bottomnav{position:fixed;left:0;right:0;bottom:0;z-index:50;display:flex;
    background:var(--bg);border-top:1px solid var(--line);
    padding-bottom:env(safe-area-inset-bottom)}
  /* 50px = standar tab bar iOS (49pt). Ini LANTAINYA: area sentuh tiap item =
     tinggi nav, minimum 44px; isinya ikon 24 + jarak 3 + label ~13 = 40px.
     Lebih pendek dari 48px -> label sesak & area sentuh menyentuh ambang. */
  .bn-item{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
    height:var(--botnav-h);color:var(--text);font-size:11px;font-weight:600;text-decoration:none;
    -webkit-tap-highlight-color:transparent}
  .bn-item .ico{width:24px;height:24px;flex:none}
  /* label tidak boleh membungkus jadi dua baris: empat item (bukan lagi tiga)
     berarti tiap item ~25% lebar. Di 360px itu 90px — "Dashboard" pada 11px
     muat, tapi ellipsis lebih baik daripada nav yang tiba-tiba setinggi dua
     baris kalau kelak ada label lebih panjang. */
  .bn-item span{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  /* warna semua sama -> pembedanya ikon terisi + label tebal */
  .bn-item[aria-current="page"]{font-weight:700}
  /* TAMU: di halaman yang MASIH ber-nav, hanya Dashboard yang disembunyikan.
     RIWAYAT (30 Agustus 2026): aturan ini dulu dasar kenapa tamu TIDAK boleh
     kehilangan seluruh nav — tinggi nav menentukan tinggi .feed, dan feed
     tamu tanpa nav jadi seukuran viewport -> bug root scroller 23 Agt.
     SEJAK 20 SEPTEMBER 2026 tamu tidak mendapat nav di feed (juga di /post
     sejak 14 Sep dan profil orang lain sejak 9 Sep). Itu SAH hanya karena
     feed HP kini pager, bukan scroller — lihat blok PAGER HP dan kepala
     berkas ini. Aturan .bn-an tetap berlaku untuk halaman tamu yang masih
     ber-nav (404 sungguhan). Profile di sana tetap terlihat dan melempar ke
     /login: jalur masuk, bukan jalan buntu. */
  body:not(.signed-in) .bn-an{display:none}
  @media (min-width:1024px){.bottomnav{display:none}}

  /* ===== NAV AKTIF (sidebar & nav bawah) =====
     Ikon berpasangan: .ico-out saat non-aktif, .ico-in (terisi) saat aktif.
     Aturan di bawah HANYA menambah selector baru — tidak mengubah aturan yang
     sudah ada, jadi cascade lama tidak tergeser (feed aman). */
  .ico-in{display:none}
  .navitem[aria-current="page"] .ico-out,.bn-item[aria-current="page"] .ico-out{display:none}
  .navitem[aria-current="page"] .ico-in,.bn-item[aria-current="page"] .ico-in{display:block}
  /* Warna semua item nav sama (#0F1419). Pembedanya = ikon terisi + huruf tebal,
     bukan warna — pola Instagram/TikTok. (Aturan redup var(--muted) dicabut.) */

/* ===== SKELETON (dipindah dari page.css, 4 Agustus 2026) =====
   Feed hanya memuat styles.css, jadi kerangka muat tidak bisa tinggal di
   page.css. DIPINDAH ke sini supaya berlaku di semua halaman — bukan
   diduplikasi; page.css sekarang hanya menyimpan varian per halamannya. */
@keyframes sk-pulse{0%,100%{opacity:1}50%{opacity:.45}}
.sk{background:var(--bubble);border-radius:8px;animation:sk-pulse 1.2s ease-in-out infinite}
@media (prefers-reduced-motion:reduce){.sk{animation:none}}
.sk-line{display:block;height:13px;border-radius:7px}

/* ===== KEADAAN FEED (kosong / galat / kerangka) =====
   Dipakai ulang dari .gate: satu "slide" seukuran kartu, jadi scroll-snap dan
   tinggi feed tidak perlu aturan khusus. */
.feed-note{font-size:14px;line-height:1.5;color:var(--muted);margin-top:8px;max-width:280px}
.feed-state .gate{gap:6px}

/* ===== KERANGKA FEED (1 September 2026) =====
   .feed-state (kosong/galat) TETAP memakai .gate dan TETAP terpusat — untuk
   PESAN itu benar. Kerangka muat tidak: ia memakai .card/.stage/.media/.meta/
   .actions yang SAMA dengan kartu asli, jadi seluruh geometri (padding kartu,
   aspect-ratio 9/16 di desktop, --rail-w, pemendekan rel di lanskap) datang
   gratis dari aturan yang sudah teruji. Yang ditambahkan di bawah HANYA ukuran
   balok penggantinya.
   Dua kelas garis versi lama DIHAPUS bersama showSkeleton() lama; namanya
   sengaja tidak ditulis di sini karena uji menegakkan bahwa keduanya tidak
   muncul lagi di berkas mana pun.

   Kenapa .meta perlu override warna: .sk memakai var(--bubble) yang ikut tema
   (rgba(0,0,0,.05) di >=1024px), sedangkan .media SELALU #0a0a0a di semua
   lebar. Tanpa baris ini, isi .meta tidak terlihat sama sekali di desktop.
   Rel aksi sengaja TIDAK diikutkan: di HP ia di ATAS media dan --bubble sudah
   terang; di desktop ia di LUAR kartu di atas latar terang dan --bubble sudah
   gelap. Sama persis dengan perlakuan .act .bubble yang asli. */
.feed-skel .meta .sk{background:rgba(255,255,255,.16)}
.feed-skel .fs-pfp{width:32px;height:32px;border-radius:50%;flex:none}   /* = .meta .pfp */
.feed-skel .fs-name{width:118px;height:15px}                             /* = .meta .u  */
.feed-skel .fs-t1{width:88%;max-width:42ch;height:13px}                  /* = baris 1 judul */
.feed-skel .fs-t2{width:62%;max-width:42ch;height:13px;margin-top:7px}   /* = baris 2 judul */
.feed-skel .fs-btn{display:block;width:124px;height:34px;border-radius:999px;margin-top:11px}
.feed-skel .fs-act{display:block;width:44px;height:44px;border-radius:50%}
@media (min-width:1024px){.feed-skel .fs-act{width:48px;height:48px}}
/* HP lanskap: rel asli menyusut jadi 38px (lihat blok max-height:520px di
   bawah). Kerangka ikut, kalau tidak ia jadi satu-satunya hal di feed yang
   tidak menyusut saat layar dimiringkan. */
@media (max-height:520px) and (max-width:1023px){.feed-skel .fs-act{width:38px;height:38px}}

/* Sprite <defs> logo — dulu style="display:none" inline yang diblokir CSP. */
.sprite-hidden{display:none}

/* ===== SHEET REPORT (5 Agustus 2026) =====
   Di styles.css, bukan page.css, karena feed hanya memuat styles.css dan
   Report ada di feed. Satu tempat, dua permukaan. */
.rp-lead{font-size:15px;font-weight:600;margin-bottom:12px}
.rp-note{font-size:13.5px;line-height:1.55;color:var(--muted);margin-bottom:16px}
.rp-note a{color:inherit;text-decoration:underline;text-underline-offset:2px}
.rp-list{display:flex;flex-direction:column}
/* Baris penuh, tinggi 48px: daftar ini diketuk di ponsel, dan pilihan yang
   salah ketuk di sini berarti laporan dengan kategori keliru. */
.rp-opt{display:flex;align-items:center;width:100%;min-height:48px;padding:12px 2px;
  border:0;border-top:1px solid var(--line);background:transparent;color:inherit;
  font:inherit;font-size:15px;text-align:left;cursor:pointer}
.rp-opt:first-child{border-top:0}
.rp-opt:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;border-radius:6px}
@media (hover:hover){.rp-opt:hover{opacity:.7}}
.rp-done{width:100%;margin-top:4px}
