/* =========================================================================
   assets/app.css -- UI DESKTOP (Claude Design "Diamondo Warehouse")
   =========================================================================
   Revolusi UI desktop, 13 Agu 2026. Ditulis dari NOL mengikuti handoff
   design_handoff_diamondo_warehouse -- bukan turunan assets/style.css.
   File ini dan style.css TIDAK PERNAH dimuat bersamaan: includes/header.php
   memilih salah satu lewat gerbang $ui_baru per halaman, jadi tidak ada
   kaskade yang saling berebut dan tidak perlu !important di mana pun.

   AWALAN d-  Semua class di sini berawalan d- (desktop). Alasannya
   assets/mobile.css menimpa nama generik (.card .btn .badge .field .pill
   .row) untuk UI mobile; tanpa awalan, halaman yang sudah dimigrasi akan
   kena aturan mobile di lebar tertentu. Awalan bikin tabrakan itu mustahil.

   LEBAR  Seluruh layout dibungkus @media (min-width:769px). Di bawah itu
   UI mobile (assets/mobile.css) yang berkuasa, persis seperti sekarang --
   revolusi ini TIDAK menyentuh mobile sama sekali. Blok penutup di akhir
   file menyembunyikan chrome desktop (.d-rail, .d-shell) di <=768px.

   TIGA JEBAKAN dari CATATAN-UI yang dipegang sejak baris pertama, bukan
   ditambal belakangan -- jangan dibalik tanpa membaca alasannya:
   1. Animasi masuk memakai animation-fill-mode:backwards, BUKAN both.
      both menahan transform setelah animasi selesai, jadi hover lift/zoom
      pada kartu KPI & baris tabel mati total.
   2. Kartu memakai backdrop-filter -> bikin stacking context baru ->
      z-index di DALAM kartu tidak bisa menang melawan kartu sesudahnya.
      Popover/dropdown karena itu dirender lewat portal ke <body>
      (lihat Diamondo.portal() di assets/app.js), bukan diakali z-index.
   3. .d-seg memakai grid-auto-flow:column + grid-auto-columns:1fr, BUKAN
      flex. Dengan flex tiap tombol selebar teksnya sendiri, jadi thumb
      yang bergerak translateX(i*100%) tidak pernah lurus dengan tombolnya.
   ========================================================================= */

/* -------------------------------------------------------------------------
   1. TOKEN
   Nilainya disalin persis dari :root prototipe. --primary default burgundy
   #c04a2a; preset Warna Aksen di Pengaturan menimpanya lewat inline style
   di <html> (lihat skrip anti-kedip di header.php).
   ------------------------------------------------------------------------- */
:root {
    --bg: #f2f2f5;
    --bg2: #e8e8ed;
    /* Turun dari .74 ke .62 (5 Sep 2026, "kartu lebih kaca"). Angka lamanya
       hampir buram: di atas latar gradien yang lembut, kartu .74 tidak bisa
       dibedakan dari kartu putih pekat, dan seluruh biaya backdrop-filter
       dibayar tanpa ada yang terlihat menembusnya. .62 adalah nilai yang
       dipakai panel kaca aplikasi sekolah, dan di sana ia terbaca. */
    --card: rgba(255,255,255,.46);
    /* Permukaan yang MENIMPA konten, bukan latar halaman: bar aksi, dan apa
       pun yang menyusul kemudian. Sejak blur dibuang, ini satu-satunya yang
       memisahkan tulisannya dari tulisan di baliknya. */
    --d-kaca-tumpang: rgba(255,255,255,.99);

    /* BAR YANG MENEMPEL SAAT DIGULIR (.d-filter di Dasbor, .d-lap-bar di
       Laporan). Ini satu-satunya tempat di seluruh tema yang memakai blur DI
       ATAS KONTEN BERGULIR, dan itu keputusan sadar -- diminta pemilik
       (6 Sep 2026) setelah melihat isi halaman lewat tembus di balik bar
       filter: angka KPI dan garis sparkline terbaca menumpuk dengan pilnya.

       Sebabnya --card diturunkan ke .46 supaya kartu terasa tembus. Kartu
       boleh setembus itu karena di belakangnya cuma latar halaman; bar ini
       punya SELURUH ISI HALAMAN lewat di baliknya.

       .9 + blur 8px, dan kombinasinya yang penting, bukan salah satunya.
       Kepekatan .9 menyisakan sedikit saja yang tembus, dan blur 8px
       melumatkan sisa itu jadi warna tanpa bentuk. Karena yang tersisa untuk
       "berenang" saat digulir tinggal 10% yang sudah tak berbentuk,
       lelehannya tidak pernah sampai terbaca -- beda dengan blur 30-56px di
       atas permukaan tembus penuh yang dulu dibuang karena bikin pusing.
       Lihat catatan pembagian blur di --d-kaca-*. */
    --d-lengket-latar: color-mix(in srgb, var(--card-solid) 90%, transparent);
    --d-lengket-bias: blur(8px) saturate(190%);
    --card-solid: #fff;
    --rail: rgba(255,255,255,.42);
    --text: #1d1d1f;
    --muted: #6e6e73;
    --faint: #8e8e93;
    --border: rgba(0,0,0,.08);
    --hair: rgba(0,0,0,.05);
    --field: rgba(120,120,128,.09);
    /* Garis bantu horizontal di chart. Sengaja token sendiri, BUKAN --hair:
       --hair dipakai pemisah tipis di dalam kartu (nyaris tak terlihat, dan
       memang harus begitu), sedangkan garis chart perlu benar-benar terbaca
       sebagai kisi. Tetap abu-abu netral supaya tidak pernah tertukar dengan
       salah satu dari empat garis seri yang berwarna. */
    --d-grid-chart: rgba(0,0,0,.16);

    --primary: #c04a2a;
    --brand: #c04a2a;
    --green: #34c759;
    /* Hijau khusus TEKS. --green (#34c759) dirancang sebagai warna ISIAN
       (badge, toggle, pil) -- di atas kartu terang kontrasnya cuma ~2,2:1,
       jadi angka yang diwarnai dengannya terbaca pudar, bukan bertanda.
       Versi terang ini ~4,9:1 di atas putih. Lihat .d-omzet-riil. */
    --hijau-teks: #1a7f37;
    /* Kuning khusus TEKS, untuk angka yang sudah ditarik Otomasi Shopee tapi
       dananya BELUM dilepas. Sengaja BUKAN --orange (#ff9f0a): oranye itu
       sudah dipakai badge Pending & Diretur, jadi warnanya sudah punya arti
       "ada yang perlu diurus" -- sementara estimasi Shopee bukan masalah,
       cuma belum final. Dan #ff9f0a di atas kartu terang cuma ~1,9:1, jauh
       lebih pudar daripada --green sekalipun.
       Amber tua ini ~4,7:1 di atas putih. Lihat .d-omzet-estimasi. */
    --kuning-teks: #845a00;   /* #9a6700 -> diturunkan: di atas lencana kuning cuma 2,89:1 (pemindai kontras 11 Sep) */
    --red: #ff3b30;
    --orange: #ff9f0a;
    /* Oranye & merah khusus TEKS -- pola yang sama dengan --hijau-teks.
       #ff9f0a dan #ff3b30 dirancang sebagai ISIAN; sebagai tulisan di atas
       kartu terang kontrasnya 1,8-2,6:1 (terukur saat mode terang dipasang,
       gladi 11 Sep: "2 freezer", angka Keluar di Rekap, tombol Keluar). */
    --oranye-teks: #b45309;
    --merah-teks: #c9170d;
    --merek-shopee: #ee4d2d;   /* oranye Shopee */
    --merek-tiktok: #42b549;   /* hijau Tokopedia -- kode channel 'tiktok' menampung keduanya */
    --purple: #af52de;
    --teal: #30b0c7;

    /* Resep permukaan. Dipakai ulang .d-card, .d-kpi, .d-sheet supaya
       penyetelannya cukup di satu tempat, bukan berburu ke belasan aturan.

       BLUR HANYA DI DUA DARI EMPAT, DAN PEMBAGIANNYA PUNYA ATURAN.

       Keempatnya dulu memburamkan latarnya 30-56px. Dibuang seluruhnya atas
       permintaan pemilik (6 Sep 2026, "itu buat motion sickness"), lalu
       sebagian dikembalikan sebagai blur RINGAN 6px setelah pemilik memilih
       kompromi: glassmorphism boleh, asal yang berenang tidak kembali.

       Yang bikin pusing bukan blur-nya, melainkan GERAK RELATIF antara kaca
       dan latarnya. backdrop-filter menyampel ulang latar tiap frame; kalau
       latar itu bergeser di bawah kaca yang diam, isinya meleleh mengikuti
       guliran. Jadi patokannya satu pertanyaan: APA yang ada di belakang
       permukaan ini, dan apakah ia bergerak relatif terhadapnya?

         --d-kaca-card  BOLEH. Di belakang kartu cuma .d-ambient, dan itu
                        position:fixed berisi gradasi blob yang sudah lembut
                        tanpa satu pun tepi tajam. Memburamkan gradasi lembut
                        tidak menghasilkan lelehan -- tidak ada bentuk yang
                        bisa meleleh. Tidak pernah ada teks di belakang kartu.

         --d-kaca-rail  BOLEH, dan di sini justru PALING BESAR dari semuanya
                        -- 50px, bukan 6px. Lihat catatan panjang di
                        --d-rail-kaca; singkatnya: latar rail cuma .d-ambient
                        yang position:fixed, jadi ia tidak pernah bergeser
                        relatif terhadap rail sekalipun halaman digulir.
                        Tidak ada gerak relatif berarti tidak ada yang bisa
                        berenang, dan syarat 6px itu tidak berlaku di sini.

                        Saat melebar ia memang menutupi konten. Dulu alasan
                        amannya "--d-rail-kaca-buka sudah .9+"; itu SUDAH
                        TIDAK BENAR sejak nilainya diturunkan jadi .62/.54.
                        Yang membuatnya tetap aman sekarang alasan lain:
                        rail hanya melebar selama kursor ada di atasnya, dan
                        selama itu roda tetikus menggulir .d-rail-daftar --
                        bukan halaman di baliknya. Yang di belakang rail
                        yang terbuka, dengan begitu, diam.

         --d-kaca-bar   TIDAK. Dipakai .d-lap-bar dan .d-filter yang
                        position:sticky DI ATAS TABEL BERGULIR -- persis
                        kasus yang bikin pusing. (.d-stok-bar ikut memakainya
                        dan sebenarnya aman, tapi latarnya permukaan kartu
                        yang rata; blur di sana tidak menghasilkan apa pun
                        juga. Jadi tidak ada yang dikorbankan.)

         --d-kaca-pop   TIDAK. Popover & kalender sudah 99% pekat karena
                        harus terbaca di atas konten tanpa blur; di balik
                        permukaan sepekat itu tidak ada sisa untuk diburamkan.

       Pil segmented dan sorotan rail JUGA tetap tanpa blur, dengan alasan
       terpisah: keduanya BERGERAK, dan latar yang bergeser di bawah kaca
       yang meluncur itu lelehan yang sama. Lihat permintaan "efek blurr saat
       bergerak matikan total".

       6px, bukan 30. Cukup untuk melembutkan gradasi jadi kaca, jauh di
       bawah ambang yang membuat latar terasa berenang. */
    --d-kaca-card: blur(6px) saturate(180%);
    --d-kaca-bar: saturate(190%);
    --d-kaca-rail: blur(50px) saturate(220%);
    --d-kaca-pop: saturate(190%);
    --d-shadow-pop: 0 26px 54px -22px rgba(0,0,0,.45);
    --d-ease-pop: cubic-bezier(.34,1.4,.42,1);
    --d-ease-out: cubic-bezier(.2,.9,.3,1);
    /* Kurva rail: berangkat cepat lalu mendarat lembut, jadi rail yang
       melebar terasa punya bobot. Sengaja BUKAN --d-ease-pop: rail selebar
       168px yang memantul di ujungnya terbaca seperti panel yang lepas,
       bukan seperti laci yang ditarik. */
    --d-ease-rail: cubic-bezier(.32,.72,0,1);

    /* SATU jangka waktu untuk SELURUH pergantian tema.
       Dulu ada empat: sapuan lingkaran 520ms, matahari terbit 750ms
       (dengan lenting melewati batas), yang terbenam 500ms, dan kelas
       .berganti baru dilepas di 900ms. Akibatnya layar selesai berganti
       lebih dulu, lalu ikon kecil di pojok masih bergerak 230ms lagi dan
       memantul di ujungnya -- dilaporkan pemilik sebagai "seperti glitch",
       dan memang itu bentuknya: gerakan yang tidak selesai bersama-sama
       terbaca sebagai sesuatu yang tersendat, bukan sebagai satu gerakan.
       Angka ini dibaca juga oleh Uwms.gantiTema() di app.js, jadi mengubah
       satu baris ini mengubah seluruh pergantiannya sekaligus. */
    --d-tema-durasi: 520ms;

    /* ---- Cincin kaca ----
       Tepi kartu digambar sebagai inset box-shadow, BUKAN border. Tiga
       alasan, dan yang ketiga yang menentukan:

       1. border menambah 1px ke kotak. Baris tabel di sini memakai grid
          dengan jalur yang harus identik dengan judulnya; sel yang punya
          border tidak lagi lurus dengan judul kolomnya.
       2. Satu border abu (--border) terbaca sebagai garis yang DIGAMBAR di
          sekeliling kartu. Dua inset putih -- satu keliling, satu hanya di
          sisi atas -- terbaca sebagai cahaya yang MEMANTUL di tepi kaca.
          Itu bedanya kartu yang "punya garis tepi" dan kartu yang tebal.
       3. Ia bisa diletakkan dalam daftar yang sama dengan bayangan jatuhnya,
          jadi satu properti (box-shadow) menanggung seluruh permukaan --
          dan hover cukup mengganti satu nilai, bukan menganimasikan
          border-color dan box-shadow berbarengan dengan durasi berbeda. */
    --d-kaca-cincin: inset 0 0 0 1px rgba(255,255,255,.6), inset 0 1px 0 rgba(255,255,255,.92);
    --d-shadow-card: var(--d-kaca-cincin), 0 1px 1px rgba(0,0,0,.03), 0 20px 44px -26px rgba(16,24,40,.3);

    /* Latar kartu saat DISENTUH -- lebih TEMBUS, bukan lebih pekat.

       Terbalik dari yang biasa dilakukan, dan memang disengaja. Kartu yang
       menjadi lebih putih saat disentuh tidak menyatakan apa-apa selain
       "warnanya berubah". Kartu yang menjadi lebih tembus sementara blur-nya
       naik dua kali lipat menyatakan bahwa ia BERGERAK MENDEKAT ke pembaca:
       kaca yang diangkat dari latarnya membiaskan lebih banyak dan menutupi
       lebih sedikit. Itu yang membuat translateY(-4px) di bawah terbaca
       sebagai jarak, bukan sebagai kartu yang tersentak ke atas. */
    --card-hover: rgba(255,255,255,.38);

    /* Kaca rail. Gradien tiga henti, bukan satu warna rata: kaca sungguhan
       tidak pernah sama terangnya dari atas ke bawah, dan rail setinggi
       layar adalah tempat paling mudah untuk melihat bedanya. */
    /* GRADASI YANG TERLIHAT DI RAIL TIDAK DITULIS DI SINI.

       Ini yang lama membingungkan waktu mencoba menirunya dari tangkapan
       layar web sekolah: rail mereka berwarna hijau di atas dan biru di
       bawah, tapi CSS rail-nya cuma putih transparan -- nyaris sama persis
       dengan tiga henti di bawah ini. Warnanya sama sekali bukan miliknya.

       Yang sebenarnya terjadi: di belakang rail ada bidang-bidang cahaya
       berwarna (.d-blob), dan backdrop-filter dengan blur BESAR melumat
       bidang-bidang itu jadi gradasi tegak yang mulus. Rail cuma kaca
       bening; gradasinya milik latar, dan blur yang membuatnya terbaca
       sebagai gradasi alih-alih sebagai bercak.

       Karena itu ada dua syarat, dan keduanya harus dipenuhi bersamaan:

         1. --d-kaca-rail memakai blur 50px (bukan 6px seperti permukaan
            lain). Di bawah ~30px bercaknya masih terbaca sebagai bercak.
         2. Harus ADA warna di belakang SELURUH tinggi rail. Sebelum ini
            cuma .d-blob-a (kiri atas) yang sampai ke sana, jadi separuh
            bawah rail berdiri di atas gradien datar dan tidak ada gradasi
            apa pun untuk dilumat. .d-blob-f ditambahkan khusus untuk itu.

       Web sekolah menempuh jalan yang sama dan menulisnya terang-terangan
       di komentar .cahaya mereka: tiga bidang di tepi kiri ada KHUSUS supaya
       rail punya warna di belakangnya, karena tanpa itu rail membeku putih
       sementara sisa halaman tetap benar.

       Angkanya sendiri praktis sama dengan milik mereka (.28/.16/.22),
       dan memang tidak perlu diubah -- yang selama ini kurang bukan
       gradiennya, melainkan dua syarat di atas. */
    --d-rail-kaca: linear-gradient(180deg, rgba(255,255,255,.30), rgba(255,255,255,.17) 45%, rgba(255,255,255,.24));
    /* Permukaan rail saat MELEBAR -- panel yang membuka itu sendiri, bukan
       pil atau cincinnya.

       Sempat .9/.84, hampir pekat. Angka itu lahir waktu blur dibuang total
       dari seluruh tema: tanpa blur, kepekatan jadi satu-satunya yang
       memisahkan label rail dari tulisan tabel di baliknya. Sejak blur
       ringan 6px dikembalikan (lihat --d-kaca-rail), kepekatan setinggi itu
       tidak dibutuhkan lagi -- blur yang mengurus keterbacaannya, jadi
       kepekatannya bisa dipakai untuk hal lain: membuatnya benar-benar
       terlihat kaca.

       .62/.54 dipilih dari uji tekanan, bukan dikira-kira: konten dipaksa
       masuk ke balik rail (max-width .d-page dilepas) lalu dibandingkan.
       Di .40 bayangan tulisan di baliknya sudah mengganggu; di .62 ia
       terbaca sebagai tekstur, dan labelnya tetap tajam. */
    --d-rail-kaca-buka: linear-gradient(180deg, rgba(255,255,255,.62), rgba(255,255,255,.54));
    --d-rail-cincin:
        inset -1px 0 0 rgba(255,255,255,.7), inset 1px 0 0 rgba(255,255,255,.5),
        inset 0 1px 0 rgba(255,255,255,.9), 2px 0 34px -14px rgba(16,24,40,.24);
    /* Permukaan butir rail saat disentuh / sedang aktif. */
    --d-rail-butir-hover: rgba(255,255,255,.52);
    /* Yang aktif TIDAK rata warnanya: terang di tepi atas, meredup di
       tengah, naik lagi sedikit di dasar. Itu cara sekeping kaca cembung
       memantulkan cahaya dari satu arah, dan satu-satunya alasan pil ini
       terbaca sebagai benda yang punya TEBAL alih-alih tambalan warna. */
    --d-rail-butir-aktif: linear-gradient(180deg, rgba(255,255,255,.88), rgba(255,255,255,.58) 55%, rgba(255,255,255,.74));
    --d-rail-butir-cincin: inset 0 0 0 1px rgba(255,255,255,.78);
    /* Timbulnya dirakit empat lapis, masing-masing menjawab satu pertanyaan
       mata: (1) garis pantul di tepi atas -- dari mana cahayanya datang;
       (2) cincin kaca -- di mana tepinya; (3) bayangan DALAM di dasar --
       seberapa tebal kepingnya; (4) dua bayangan jatuh dengan jarak
       berbeda -- seberapa jauh ia terangkat dari rail. Buang salah satu,
       terutama (3), dan pil-nya kembali rata. */
    --d-rail-butir-timbul:
        inset 0 1px 0 rgba(255,255,255,.95),
        inset 0 0 0 1px rgba(255,255,255,.8),
        inset 0 -7px 13px -9px rgba(16,24,40,.22),
        0 7px 16px -9px rgba(16,24,40,.3),
        0 18px 34px -20px rgba(16,24,40,.3);
    /* Pembias pil aktif. Rail-nya sendiri sudah kaca, jadi ini lapis kedua:
       yang dibiaskan pil ini adalah rail yang SUDAH membiaskan halaman di
       belakangnya. Itu yang membuatnya terbaca menumpang DI ATAS rail,
       bukan terpotong dari badan rail.

       brightness DITURUNKAN 6 Sep 2026 (1.06 -> 1.02 di sini, 1.18 -> 1.06
       di mode gelap). Inilah satu-satunya sumbu yang mengangkat SELURUH
       BIDANG pil, bukan tepinya: isian dan rim cuma menerangi sebagian,
       sedangkan brightness menaikkan tiap piksel latar di bawah pil.
       Permintaannya justru itu -- "yang dibuat lebih transparan bukan
       pinggirannya, tetapi juga the whole bubble nya".

       Dipakai berdua dengan sorotan sidebar, dan itu disengaja: pemilik
       diberi tahu efeknya ikut ke sidebar dan menjawab "turunkan saja".
       Sidebar tidak jadi lemah karenanya -- ia punya rim 1px sendiri
       (--d-rail-butir-timbul) yang menanggung keterlihatannya, sementara
       pil segmented sengaja tidak punya garis sama sekali. */
    --d-rail-butir-bias: saturate(180%) brightness(1.02);
    /* Warna teks & ikon butir rail yang sedang aktif.

       Dulu ini var(--primary) -- burgundy. Diganti jadi warna teks biasa
       atas permintaan pemilik (6 Sep 2026). Alasannya sejalan dengan kenapa
       kotak tint aksen dibuang dari sini dulu: satu-satunya benda berwarna
       aksen di seluruh chrome terbaca sebagai peringatan atau tombol yang
       sedang ditekan, bukan sebagai penunjuk tempat. Yang menandai halaman
       terpilih sekarang murni permukaannya -- pil kaca yang terangkat --
       ditambah bobot huruf 800. Itu cukup, dan tidak menghabiskan
       satu-satunya warna aksen halaman untuk keperluan sekecil ini. */
    --d-rail-aktif-teks: var(--text);

    /* ---- Pil segmented: material Liquid Glass ----

       Diterjemahkan dari "WWDC25: Meet Liquid Glass" (Apple), rujukan yang
       diberikan pemilik 6 Sep 2026. Yang dipelajari dari rekamannya: pada
       lensa Apple, badan kacanya HAMPIR TIDAK BERWARNA -- yang ada di
       belakangnya tembus dan terbaca. Hampir seluruh efeknya ada di TEPI:
       cahaya terkumpul jadi garis tipis yang sangat terang di rim atas,
       melebar jadi pita lembut di rim bawah, dan di dasarnya terurai jadi
       semburat pelangi tipis. (Yang terakhir sempat dibuat lalu dibuang --
       lihat catatan di .d-seg-thumb.)

       PEMBIASNYA masih merujuk sidebar; ISIANNYA tidak lagi.

       Keduanya sempat sepenuhnya sama. Dipisah lagi 6 Sep 2026 atas
       permintaan pemilik: pil harus "sungguh sangat sangat seperti kaca",
       lebih bening daripada sorotan sidebar. Puncak gradiennya turun dua
       tahap, .88 -> .46 -> .34; yang di mode gelap .20 -> .10 -> .07.

       Yang TIDAK berubah: pembiasnya (--d-seg-bias) tetap dirujuk dari
       sidebar, jadi cara keduanya mengangkat warna latarnya tetap identik.
       Yang dibedakan cuma seberapa banyak isian yang menutupi latar itu --
       dan itu memang satu-satunya sumbu yang diminta berbeda.

       Catatan penting kalau nanti disetel lagi: pada pil setinggi 35px,
       yang membuatnya terlihat pekat BUKAN isiannya melainkan rim-nya.
       Diuji langsung -- menurunkan isian dari .20 ke .08 di mode gelap
       nyaris tidak mengubah apa pun, karena halo `inset 0 0 9px` menjangkau
       sampai ke tengah pil dan praktis ikut mengisinya. Yang benar-benar
       membeningkan pil adalah menarik cahaya rim kembali ke tepinya. */
    --d-seg-kaca: linear-gradient(180deg, rgba(255,255,255,.34), rgba(255,255,255,.15) 55%, rgba(255,255,255,.24));
    --d-seg-bias: var(--d-rail-butir-bias);
    /* Delapan lapis, tiga tugas: (1-4) rim empat sisi, paling terang di atas
       karena dari sanalah cahaya datang; (5-6) cahaya yang TERKUMPUL sedikit
       ke dalam dari rim atas dan bayangan yang mengendap di rim dasar --
       pasangan inilah yang memberi kesan kacanya punya TEBAL; (7-8) dua
       bayangan jatuh dengan jarak berbeda, supaya pil-nya terbaca mengambang
       di atas alurnya. */
    /* Rim TANPA GARIS. Versi sebelumnya merakit tepinya dari empat bayangan
       inset ber-blur NOL -- `inset 0 1.5px 0`, `inset 1px 0 0`, dan
       seterusnya. Bayangan tanpa blur adalah garis: tepinya berhenti
       mendadak di satu piksel, dan yang terlihat pil yang DIGARIS, bukan pil
       yang menangkap cahaya. Itu yang diminta dihilangkan (6 Sep 2026,
       "buat outlinenya lebih smooth seperti tidak bergaris").

       Penggantinya tiga cahaya yang semuanya BER-BLUR, jadi tidak satu pun
       punya tepi yang bisa terbaca sebagai garis:

         1. halo lembut mengelilingi seluruh tepi dalam -- yang memberi tahu
            mata di mana bendanya berakhir, tanpa menggambar batasnya;
         2. cahaya terkumpul di paruh atas -- dari mana cahayanya datang;
         3. bayangan mengendap di dasar -- yang memberi kesan TEBAL.

       Lalu dua bayangan jatuh berjarak beda, supaya pil terbaca mengambang
       di atas alurnya. */
    --d-seg-rim:
        inset 0 0 5px rgba(255,255,255,.5),
        inset 0 4px 7px -5px rgba(255,255,255,.72),
        inset 0 -8px 13px -9px rgba(16,24,40,.2),
        0 1px 2px rgba(16,24,40,.08),
        0 7px 16px -9px rgba(16,24,40,.3);

    /* Radius kartu. Satu token supaya kartu, KPI, bar filter, dan kotak
       statistik tidak pernah berbeda sudutnya. */
    --d-r-card: 26px;

    /* SOROTAN BARIS TABEL SAAT DISENTUH.

       Barisnya sendiri tidak punya permukaan dalam keadaan diam (lihat
       .d-tabel td) -- yang ada cuma sorotan ini, dan cuma selama kursor
       lewat.

       TIPIS DAN BERONA, dua-duanya disengaja. Alpha efektifnya ~0,20, turun
       dari ~0,55: isian sepekat itu menutup kartu di belakangnya dan
       barisnya terbaca sebagai pita yang ditempel, sedangkan setipis ini
       kartunya tetap tembus. Dan ronanya aksen, bukan putih polos -- putih
       cuma menghasilkan "lebih terang", dan lebih terang tanpa warna itu
       persis yang terbaca sebagai abu.

       Sempat ada cincin tepi di sini untuk menanggung keterlihatan yang
       hilang bersama kepekatan. Dibuang atas permintaan pemilik (6 Sep
       2026): garis mengelilingi baris membuatnya terbaca sebagai benda
       terpisah yang ditempel di atas tabel, bukan sebagai bagian tabel yang
       sedang disorot. Yang menanggung keterlihatannya sekarang rona +
       geseran 4px + bayangan jatuh, dan itu cukup. */
    --d-baris-hover: color-mix(in srgb, var(--primary) 5%, rgba(255,255,255,.16));
    /* Pembiasan baris yang disentuh. brightness & saturate yang menanggung
       keterlihatannya, bukan blur: di belakang baris cuma ada latar kartu
       yang sudah rata, jadi memburamkannya lagi nyaris tidak mengubah apa
       pun -- sementara menaikkan saturasi & terang membuat warna ambient
       yang menembus kartu jadi lebih hidup persis di baris itu saja. */
    --d-baris-bias: saturate(190%) brightness(1.06);

    /* Padding kartu & baris tabel. Mode "Tampilan padat" cuma menimpa dua
       angka ini (lihat html.compact di bawah) -- itulah kenapa dipakai
       sebagai variabel, bukan ditulis langsung di tiap komponen. */
    --d-pad-card: 24px;
    --d-pad-row: 13px 14px;

    color-scheme: light;
}

/* ---- Alias token lama ----
   Halaman di LUAR handoff (login, lupa password, set password, syarat,
   kebijakan, menu staff) tidak dirancang ulang -- keputusannya "ikut token
   baru, layout tetap". Aturan tata letaknya dipindah apa adanya dari
   style.css, dan aturan itu menyebut nama token lama. Dipetakan di sini ke
   token baru supaya CSS-nya jalan tanpa disunting satu per satu, DAN supaya
   halaman-halaman itu otomatis ikut warna aksen serta mode gelap yang sama.
   Blok ini ikut terhapus kalau nanti halaman-halaman itu dirancang ulang. */
:root {
    --primary-dark: #96371f;
    --danger:  var(--red);
    --success: var(--green);
    --warning: var(--orange);
    --hairline: var(--hair);
    --hairline-soft: var(--hair);
    --input-bg: var(--card-solid);
    --surface-tint: color-mix(in srgb, var(--text) 3%, transparent);
    --surface-tint-2: color-mix(in srgb, var(--text) 5%, transparent);
    --sidebar-color: #7a1226;
    --sidebar-color-light: #9c2438;
    --sidebar-color-dark: #4a0a16;
    --radius-lg: 22px;
    --radius-md: 14px;
    --radius-sm: 10px;
    --ease: cubic-bezier(.2, .9, .3, 1);
    --shadow-xs: 0 1px 2px rgba(0,0,0,.05);
    --shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
    --shadow-md: 0 12px 32px rgba(0,0,0,.10), 0 2px 8px rgba(0,0,0,.05);
    --shadow-lg: 0 24px 56px rgba(0,0,0,.16), 0 4px 14px rgba(0,0,0,.06);
    --scrollbar-thumb: rgba(140,140,150,.35);
    --scrollbar-thumb-hover: rgba(140,140,150,.55);
}

html[data-theme="dark"] {
    --bg: #0b0b0d;
    --bg2: #17171a;
    --card: rgba(28,28,30,.58);
    --d-kaca-tumpang: rgba(28,28,30,.99);
    /* Resepnya sama; --card-solid sendiri yang berganti gelap. */
    --d-lengket-latar: color-mix(in srgb, var(--card-solid) 90%, transparent);
    --d-lengket-bias: blur(8px) saturate(190%);
    --card-solid: #1c1c1e;
    --rail: rgba(28,28,30,.44);
    --text: #f5f5f7;
    --muted: #a1a1a6;
    --faint: #8e8e93;
    --border: rgba(255,255,255,.11);
    --hair: rgba(255,255,255,.07);
    --field: rgba(120,120,128,.24);
    --d-grid-chart: rgba(255,255,255,.20);
    --merek-shopee: #ff7a5c;
    --merek-tiktok: #5fd46a;
    /* Di atas #0b0b0d yang gelap justru kebalikannya: hijau tua tidak
       terbaca, jadi dipakai hijau terang Apple. */
    --hijau-teks: #30d158;
    /* Sama alasannya: amber tua tidak terbaca di atas #0b0b0d, jadi dipakai
       amber terang (~8:1). Tetap jelas berbeda dari --orange badge Pending. */
    --kuning-teks: #e3b341;
    /* Di latar gelap oranye & merah aslinya sudah terbaca -- dipakai apa adanya. */
    --oranye-teks: #ff9f0a;
    --merah-teks: #ff453a;

    /* ---- Cincin kaca, versi gelap ----
       Angkanya BUKAN versi terang yang dikecilkan sembarangan. Di atas
       permukaan #1c1c1e, putih 60% tidak terbaca sebagai pantulan melainkan
       sebagai garis putih yang digambar mengelilingi kartu -- persis
       kesalahan yang sedang dihindari. Yang dipertahankan cuma BENTUKNYA:
       satu cincin keliling yang nyaris tak terlihat, dan satu garis sisi
       atas yang sedikit lebih terang, karena di situlah cahaya jatuh. */
    --d-kaca-cincin: inset 0 0 0 1px rgba(255,255,255,.08), inset 0 1px 0 rgba(255,255,255,.14);
    --d-shadow-card: var(--d-kaca-cincin), 0 1px 1px rgba(0,0,0,.3), 0 22px 46px -26px rgba(0,0,0,.7);

    /* Sama-sama lebih tembus saat disentuh, tapi ke arah GELAP. Menaikkan
       transparansi permukaan gelap di atas latar gelap nyaris tidak terlihat
       kalau angkanya disalin dari mode terang; .52 di sini kira-kira sebesar
       .38 di sana dalam hal seberapa banyak latar yang menembus. */
    --card-hover: rgba(28,28,30,.52);

    --d-rail-kaca: linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.035) 45%, rgba(255,255,255,.055));
    --d-rail-kaca-buka: linear-gradient(180deg, rgba(34,34,38,.52), rgba(26,26,30,.44));
    --d-rail-cincin:
        inset -1px 0 0 rgba(255,255,255,.09), inset 1px 0 0 rgba(255,255,255,.05),
        inset 0 1px 0 rgba(255,255,255,.11), 2px 0 34px -14px rgba(0,0,0,.6);
    --d-rail-butir-hover: rgba(255,255,255,.09);
    --d-rail-butir-aktif: linear-gradient(180deg, rgba(255,255,255,.2), rgba(255,255,255,.075) 55%, rgba(255,255,255,.13));
    --d-rail-butir-cincin: inset 0 0 0 1px rgba(255,255,255,.1);
    /* Susunan lapisnya sama dengan mode terang, arah cahayanya juga; yang
       berubah cuma yang menanggung kontras. Di sini bayangan jatuhnya yang
       bekerja keras (latar gelap menelan bayangan tipis), sedangkan garis
       pantul atas ditahan di .28 -- lebih dari itu tepinya berpendar. */
    --d-rail-butir-timbul:
        inset 0 1px 0 rgba(255,255,255,.28),
        inset 0 0 0 1px rgba(255,255,255,.15),
        inset 0 -7px 13px -9px rgba(0,0,0,.5),
        0 7px 16px -9px rgba(0,0,0,.55),
        0 18px 34px -20px rgba(0,0,0,.5);
    --d-rail-butir-bias: saturate(190%) brightness(1.06);
    /* Aksen dicerahkan sampai terbaca di atas pil gelap, tapi tetap ADA
       warnanya -- dinaikkan sampai putih akan membuat butir aktif tidak bisa
       dibedakan dari butir biasa yang kebetulan sedang disentuh. 52% putih
       adalah titik di mana ia masih jelas burgundy. */
    --d-rail-aktif-teks: #ffffff;

    /* Pil segmented, mode gelap. Isiannya sendiri (lebih bening daripada
       sorotan sidebar), pembiasnya tetap merujuk sidebar -- pembagian yang
       sama seperti di mode terang. */
    --d-seg-kaca: linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.02) 55%, rgba(255,255,255,.04));
    --d-seg-bias: var(--d-rail-butir-bias);
    /* Susunan cahayanya sama dengan mode terang, ditahan lebih redup: di
       latar gelap halo putih gampang berpendar jadi lingkaran cahaya. */
    --d-seg-rim:
        inset 0 0 5px rgba(255,255,255,.18),
        inset 0 4px 7px -5px rgba(255,255,255,.34),
        inset 0 -7px 11px -8px rgba(0,0,0,.5),
        0 2px 5px rgba(0,0,0,.4),
        0 8px 18px -10px rgba(0,0,0,.5);

    /* Di mode gelap baris harus lebih TERANG daripada kartunya, bukan lebih
       gelap -- karena itu putih beropasitas rendah, bukan hitam. */
    /* Ronanya jauh lebih pekat daripada mode terang, dan itu memang harus:
       di belakang baris ada kartu rgba(28,28,30,.72) yang menutup hampir
       seluruh cahaya ambient, jadi tidak ada warna yang bisa dinaikkan
       saturate -- warnanya harus dibawa sendiri oleh isian ini. */
    --d-baris-hover: color-mix(in srgb, var(--primary) 16%, rgba(255,255,255,.02));
    /* Di mode gelap brightness harus lebih berani: latar di belakangnya
       nyaris hitam, dan kenaikan 6% seperti di mode terang tidak akan
       terlihat sama sekali. */
    --d-baris-bias: saturate(200%) brightness(1.5);

    /* Kaca segmented, versi gelap. Bentuknya dipertahankan persis -- kilau
       punggung atas tetap yang paling terang, tepi bawah tetap lebih redup --
       yang turun cuma opasitasnya. Menyalin angka mode terang apa adanya
       menghasilkan kotak putih menyala di atas latar gelap; menghapus
       kilaunya sama sekali menghasilkan kotak abu rata yang tidak lagi
       terbaca sebagai kaca. */
    color-scheme: dark;
}

/* Wallpaper personalisasi aktif -> rail dinaikkan opasitasnya supaya label
   9,5px-nya tetap kebaca di atas foto apa pun. Ditandai dari PHP sebagai
   data-latar="1" di <html>. */
html[data-latar="1"]                    { --rail: rgba(255,255,255,.72); }
html[data-theme="dark"][data-latar="1"] { --rail: rgba(28,28,30,.74); }

/* Rail di atas wallpaper foto.

   RIWAYAT, karena angkanya sudah dua kali berbalik arah dan alasannya beda
   tiap kali:

   Versi pertama (5 Sep 2026) MENAIKKAN kepekatannya. Rail waktu itu memakai
   gradien tiga henti yang henti tengahnya cuma 17%, dan di atas foto
   berbintang separuh rail praktis transparan sehingga label hilang di situ
   saja. Yang diperbaiki bukan cuma opasitasnya -- gradiennya dipadatkan jadi
   dua henti yang keduanya pekat.

   Versi kedua (7 Sep 2026) menaikkannya lagi, kali ini untuk yang MELEBAR,
   dengan alasan foto berkontras tinggi akan menelan labelnya. Itu keliru,
   dan pemiliknya yang menunjukkan buktinya: kartu KPI tembus di atas foto
   yang SAMA -- rgba(28,28,30,.58) + blur 6px -- dan angkanya terbaca
   sempurna. Kalau kartu bisa, rail juga bisa; yang membuat versi pertama
   gagal adalah henti 17% itu, bukan ketembusannya.

   Sekarang keduanya disetel ke tingkat KARTU, karena itu memang tolok ukur
   yang benar: rail dan kartu sama-sama melayang di atas foto yang sama, dan
   tidak ada alasan yang satu terlihat kaca sementara yang lain terlihat
   papan. Diuji langsung di atas wallpaper, gelap dan terang.

   Mode terang ditahan sedikit lebih pekat (.66/.70 vs .55/.60 di gelap) dan
   itu bukan selera: label rail memakai --muted yang sengaja lembut, dan di
   atas foto GELAP lapisan putih tipis tidak cukup mengangkat kontrasnya. */
html[data-latar="1"] {
    --d-rail-kaca: linear-gradient(180deg, rgba(255,255,255,.72), rgba(255,255,255,.66));
    --d-rail-kaca-buka: linear-gradient(180deg, rgba(255,255,255,.76), rgba(255,255,255,.7));
}
html[data-theme="dark"][data-latar="1"] {
    --d-rail-kaca: linear-gradient(180deg, rgba(28,28,32,.76), rgba(24,24,28,.7));
    --d-rail-kaca-buka: linear-gradient(180deg, rgba(28,28,32,.8), rgba(24,24,28,.74));
}

/* KACA SUNGGUHAN, dan syaratnya dibuktikan bukan diasumsikan.

   [data-latar-serasi] dipasang JS hanya setelah foto latarnya diukur dan
   terang rata-ratanya terbukti searah dengan tema (lihat pasangLatarSerasi()
   di app.js). Selama itu belum terbukti, yang berlaku nilai pekat di atas.

   Di sini isiannya turun sampai .20 -- rail praktis cuma pembias, dan foto
   di baliknya lewat nyaris utuh. Itu baru aman justru karena keserasian tadi
   sudah dipastikan: label terang di atas foto gelap, atau label gelap di
   atas foto terang. */
html[data-latar="1"][data-latar-serasi] {
    --d-rail-kaca: linear-gradient(180deg, rgba(255,255,255,.24), rgba(255,255,255,.18));
    --d-rail-kaca-buka: linear-gradient(180deg, rgba(255,255,255,.3), rgba(255,255,255,.24));
}
html[data-theme="dark"][data-latar="1"][data-latar-serasi] {
    --d-rail-kaca: linear-gradient(180deg, rgba(28,28,32,.2), rgba(24,24,28,.15));
    --d-rail-kaca-buka: linear-gradient(180deg, rgba(28,28,32,.26), rgba(24,24,28,.2));
}

html.compact {
    --d-pad-card: 17px;
    --d-pad-row: 8px 14px;
}

/* -------------------------------------------------------------------------
   2. DASAR
   ------------------------------------------------------------------------- */
* { box-sizing: border-box; }

/* [hidden] harus benar-benar menyembunyikan. Aturan bawaan browser untuk
   atribut ini cuma `display:none` biasa, jadi KALAH oleh display apa pun
   yang ditulis penulis stylesheet -- .d-actionbar{display:flex} membuat
   bar aksi massal tetap terlihat walau ditandai hidden. Ini satu-satunya
   !important di berkas ini, dan memang idiom standar untuk kasus ini. */
[hidden] { display: none !important; }

html, body { margin: 0; padding: 0; height: 100%; }

body {
    background: var(--bg);
    color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", "Helvetica Neue", system-ui, sans-serif;
    -webkit-font-smoothing: antialiased;
    letter-spacing: -.011em;
}

a { color: var(--primary); text-decoration: none; }
/* TIDAK ADA garis bawah saat hover, di mana pun. (Keputusan pemilik,
   9 Sep 2026.)

   Di aplikasi ini hampir setiap <a> sebenarnya sebuah PERMUKAAN, bukan
   tautan di dalam kalimat: petak freezer, kartu outlet, baris tabel,
   tingkat rak, pil seksi. Garis bawah pada permukaan seperti itu tidak
   menambah keterangan apa pun -- yang sudah menandainya sebagai bisa
   diketuk adalah bentuk, latar yang berubah, dan kursor. Yang ia lakukan
   cuma membuat kartu terlihat rusak sesaat.

   Penanda hover-nya sudah ada di masing-masing komponen (latar berubah,
   border menyala, sedikit terangkat). Kalau suatu saat ada tautan yang
   memang duduk di tengah kalimat dan butuh garis bawah, berikan kelasnya
   sendiri -- jangan hidupkan lagi aturan menyeluruh di sini. */
a:hover { text-decoration: none; }

input, button, select, textarea { font-family: inherit; }
input:focus, textarea:focus { outline: none; }

/* Angka SELALU tabular supaya kolom Rupiah di tabel lurus ke bawah dan
   tidak "bergoyang" saat nilainya berubah lewat AJAX. */
.d-num, .d-kpi-nilai, .d-stepper-nilai { font-variant-numeric: tabular-nums; }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
    background: rgba(140,140,150,.35);
    border-radius: 8px;
    border: 3px solid transparent;
    background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: rgba(140,140,150,.55); background-clip: content-box; }

/* -------------------------------------------------------------------------
   3. KEYFRAME
   Sembilan animasi dari handoff. Yang memakainya WAJIB menutup dengan
   `backwards`, jangan `both` -- lihat jebakan #1 di kepala file.
   ------------------------------------------------------------------------- */
@keyframes d-fadeUp     { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes d-screenIn   { from { opacity: 0; transform: translateY(18px) scale(.992); } to { opacity: 1; transform: none; } }
@keyframes d-sheetIn    { from { opacity: 0; transform: translateY(40px) scale(.94); } to { opacity: 1; transform: none; } }
@keyframes d-backdropIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes d-rowIn      { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: none; } }
@keyframes d-toastIn    { from { opacity: 0; transform: translateY(22px) scale(.9); } to { opacity: 1; transform: none; } }
@keyframes d-dash       { from { stroke-dashoffset: 2400; } to { stroke-dashoffset: 0; } }
@keyframes d-grow       { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes d-growX      { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes d-popIn      { 0% { transform: scale(.7); opacity: 0; } 60% { transform: scale(1.08); } 100% { transform: scale(1); opacity: 1; } }
@keyframes d-spin       { to { transform: rotate(360deg); } }
@keyframes d-pulseRing  {
    0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--primary) 32%, transparent); }
    70%  { box-shadow: 0 0 0 16px color-mix(in srgb, var(--primary) 0%, transparent); }
    100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--primary) 0%, transparent); }
}
@keyframes d-floatA     { 0%,100% { transform: translate(0,0) scale(1); } 50% { transform: translate(40px,-30px) scale(1.12); } }
@keyframes d-floatB     { 0%,100% { transform: translate(0,0) scale(1); } 50% { transform: translate(-50px,25px) scale(1.08); } }

/* Hormati preferensi sistem "kurangi gerak". Bukan basa-basi aksesibilitas:
   animasi dash chart 1,9 detik + 30-an baris tabel yang masuk bergantian
   memang bisa memicu mual buat sebagian orang. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
    }
}

/* =========================================================================
   MULAI DARI SINI: HANYA DESKTOP (>=769px)
   ========================================================================= */
@media (min-width: 769px) {

/* -------------------------------------------------------------------------
   4. LATAR AMBIENT
   Lima bidang cahaya yang mengambang pelan + lapisan wallpaper opsional.
   pointer-events:none supaya tidak pernah menghalangi klik.

   JANGAN DIHILANGKAN SEBAGAI HIASAN. Kaca beku cuma terbaca kalau ada
   sesuatu BERWARNA di belakangnya untuk diburamkan; lapisan inilah yang
   menyediakannya. Tanpa ia, setiap kartu di aplikasi ini jadi putih pucat
   dan seluruh biaya backdrop-filter terbayar tanpa ada yang terlihat
   menembusnya.

   Sebelum 5 Sep 2026 di sini cuma ada DUA bercak, keduanya warna aksen yang
   sama, keduanya di sudut yang berlawanan. Akibatnya seluruh bagian TENGAH
   layar -- tempat semua kartu sebenarnya duduk -- berdiri di atas abu-abu
   rata, dan kartunya memang tampil putih pucat.

   Tiga bidang baru dan letaknya dipilih dari ISI HALAMAN, bukan dari
   komposisi: (c) di belakang baris kartu KPI, (d) di belakang tabel
   panjang, (e) di pinggang kiri yang selama ini paling kosong. Dua di
   antaranya BUKAN warna aksen dengan sengaja -- kalau kelimanya burgundy,
   yang bertambah cuma pekatnya satu warna, bukan sesuatu untuk dibiaskan.
   ------------------------------------------------------------------------- */
.d-ambient {
    position: fixed;
    inset: 0;
    z-index: 0;
    overflow: hidden;
    pointer-events: none;
    background: linear-gradient(160deg, var(--bg), var(--bg2));
}
/*
   RIWAYAT `filter: blur()` DI BIDANG INI -- dicabut, lalu dikembalikan.

   Versi pertama memakai blur(34px) dan DICABUT karena ia justru MENAMBAH
   tepi: batas lingkaran bidang di sudut kanan bawah terlihat sebagai busur
   bergaris, karena filter membuat lapisan komposit sendiri dan hasilnya
   terpotong di batas elemen -- gradiennya waktu itu masih berwarna sampai
   tepi.

   Dikembalikan sebagai blur(24px) sesudah gradiennya dibuat bening di 88%
   (lihat resep di .d-blob). Ruang 12% di tepi itulah syaratnya: blur
   menyebar ke dalam ruang kosong dan tidak pernah sampai ke batas yang
   dipotong. Busurnya tidak kembali.

   ONGKOSNYA TERUKUR, DAN TERNYATA TIDAK ADA. Waktu mencabutnya dulu, salah
   satu alasan yang saya tulis adalah beban konvolusi per bingkai -- dan
   catatan itu sendiri menyebut bahwa angkanya belum pernah diukur. Sekarang
   sudah, di halaman Stok dengan tabel ratusan baris (6 Sep 2026, jendela
   1536px):

       diam, blur menyala   108 fps
       diam, blur dimatikan 106 fps
       sambil menggulir      89 fps

   Selisih dua bingkai itu ada di dalam derau pengukuran. Jadi alasan
   performa yang dulu saya sebut TIDAK terbukti, dan tidak boleh dipakai
   sebagai dasar mencabutnya lagi. Yang dulu benar cuma alasan busurnya --
   dan itu pun sudah ditangani oleh ruang 12% tadi, bukan oleh mencabut
   blur.
*/
.d-blob {
    position: absolute;
    border-radius: 50%;
    /*
      SATU RESEP UNTUK KELIMANYA. Tiap bidang cuma menyetel --blob (warna)
      dan --blob-op (kepekatan); bentuk pudarnya ditulis sekali di sini.

      LIMA HENTI, BUKAN DUA. Versi sebelumnya cuma "warna -> transparent
      66%", dan gradien dua henti memudar LURUS. Mata sangat peka pada
      perubahan mendadak di kemiringan gradien -- di titik warna mulai
      memudar dan di titik ia berhenti, mata membaca tepi yang sebenarnya
      tidak ada (efek Mach). Itu yang membuat bidang di kanan bawah terbaca
      sebagai lingkaran bergaris, bukan sebagai cahaya.

      Kelima henti di bawah mengikuti kurva yang melandai -- cepat di tengah,
      makin pelan menjelang tepi -- sehingga tidak ada satu titik pun di mana
      kemiringannya berubah mendadak.

      `closest-side`, BUKAN bawaan `farthest-corner`. Bawaannya mengukur ray
      sampai SUDUT kotak, padahal elemennya bulat (border-radius: 50%) --
      artinya gradien masih berwarna saat border-radius sudah memotongnya,
      dan potongan itu tepi keras. Dengan closest-side, ray persis
      sepanjang jari-jari lingkarannya.
    */
    background: radial-gradient(circle closest-side,
        var(--blob) 0%,
        color-mix(in srgb, var(--blob) 80%, transparent) 26%,
        color-mix(in srgb, var(--blob) 46%, transparent) 48%,
        color-mix(in srgb, var(--blob) 18%, transparent) 66%,
        color-mix(in srgb, var(--blob) 5%, transparent) 79%,
        transparent 88%);
    /*
      BLUR BOLEH DIPASANG LAGI, dan syaratnya ada di angka 88% di atas.

      Percobaan 5 Sep 2026 membuang blur karena ia justru MEMUNCULKAN busur
      bergaris di sudut kanan bawah. Sebabnya bukan blur-nya: waktu itu
      gradien masih berwarna sampai tepi elemen, jadi blur menyeret warna
      keluar lingkaran lalu border-radius memotongnya -- dan potongan hasil
      blur itulah busurnya.

      Sekarang gradien sudah bening di 88%, menyisakan 12% jari-jari (sekitar
      33px pada bidang 560px) sebagai ruang kosong di tepi. Blur 24px
      menyebar ke dalam ruang itu dan tidak pernah sampai ke tepi yang
      dipotong. Kalau suatu saat blur dinaikkan melewati sisa ruang itu,
      busurnya akan kembali -- naikkan 88% turun lebih dulu.
    */
    filter: blur(24px);
    /* --blob-redup dipakai keadaan wallpaper untuk memudarkan semuanya
       sekaligus tanpa perlu tahu kepekatan masing-masing bidang. */
    opacity: calc(var(--blob-op, .2) * var(--blob-redup, 1));
    transition: opacity .5s;
}
.d-blob-a {
    top: -180px; left: -120px; width: 620px; height: 620px;
    --blob: var(--primary); --blob-op: .30;
    animation: d-floatA 22s ease-in-out infinite;
}
.d-blob-b {
    bottom: -220px; right: -140px; width: 680px; height: 680px;
    --blob: var(--primary); --blob-op: .22;
    animation: d-floatB 26s ease-in-out infinite;
}
.d-blob-c {
    top: -200px; left: 30%; width: 720px; height: 720px;
    --blob: var(--teal); --blob-op: .26;
    animation: d-floatB 31s ease-in-out 3s infinite;
}
.d-blob-d {
    bottom: -240px; left: 14%; width: 680px; height: 680px;
    --blob: var(--purple); --blob-op: .20;
    animation: d-floatA 35s ease-in-out 6s infinite;
}
/* Paling redup dari kelimanya: ia satu-satunya yang jatuh di ruang kosong
   sebelah kanan konten, tanpa kartu apa pun di atasnya untuk meredamnya --
   jadi ia yang paling mudah terbaca sebagai "ada lingkaran di sana". */
.d-blob-e {
    top: 30%; right: -160px; width: 620px; height: 620px;
    --blob: var(--teal); --blob-op: .14;
    animation: d-floatA 39s ease-in-out 9s infinite;
}
/* KHUSUS UNTUK RAIL, dan satu-satunya yang letaknya dipilih dari BENDA di
   atasnya, bukan dari isi halaman.

   Kelima bidang di atas menyebar mengikuti isi halaman, dan akibatnya cuma
   .d-blob-a yang kebetulan lewat di belakang rail -- di kiri ATAS. Separuh
   bawah rail berdiri di atas gradien datar tanpa warna apa pun, dan
   backdrop-filter tidak bisa melumat sesuatu yang tidak ada di sana: rail
   jadi berwarna di atas lalu memucat rata ke bawah.

   Bidang ini menutup separuh yang kosong itu. Warnanya SENGAJA bukan
   --primary: yang dituju perpindahan warna dari atas ke bawah, dan kalau
   keduanya burgundy yang bertambah cuma pekatnya satu warna -- persis
   kekeliruan yang sudah dicatat di kepala bagian ini soal dua bidang lama.

   Lebih redup (.16) daripada .d-blob-a (.30) dengan sengaja. Ia jatuh di
   bagian rail yang KOSONG -- di bawah butir menu terakhir -- tanpa ikon
   atau label apa pun di atasnya untuk meredamnya. */
.d-blob-f {
    bottom: -260px; left: -220px; width: 700px; height: 700px;
    --blob: var(--purple); --blob-op: .16;
    animation: d-floatB 33s ease-in-out 4s infinite;
}
/* Wallpaper menyala -> bidang cahaya dipudarkan supaya fotonya tidak
   terlihat "kotor" tertimpa lima bercak warna. */
html[data-latar="1"] .d-blob { --blob-redup: .22; }
/* Wallpaper HARUS di atas kelima bidang. Ia ditulis paling akhir di
   header.php sehingga urutan lukis sudah benar; z-index dipasang supaya
   tetap benar kalau suatu hari ada bidang yang ditambahkan sesudahnya. */
.d-wallpaper {
    position: absolute;
    inset: 0;
    z-index: 1;
    background-size: cover;
    background-position: center;
}

/* -------------------------------------------------------------------------
   5. SHELL + RAIL NAVIGASI

   RAIL YANG MELEBAR SAAT DISENTUH (5 Sep 2026).

   Bentuk sebelumnya: kolom 92px berisi ikon dengan label 9,5px DI BAWAHNYA,
   permanen. Dua hal salah di situ. Pertama, 9,5px adalah ukuran huruf yang
   dipilih bukan karena itu ukuran yang benar melainkan karena "Transaksi"
   harus muat di bawah ikon selebar 22px -- teksnya ada, tapi tidak sungguh-
   sungguh dibaca siapa pun; yang dipakai orang tetap bentuk ikonnya. Kedua,
   tiga belas item x (ikon + label + jarak) adalah kolom yang tingginya
   selalu berkelahi dengan tinggi jendela, dan enam @media (max-height) di
   bawah ini adalah bekas perkelahian itu.

   Bentuk baru: 80px berisi ikon saja, melebar jadi 248px saat disentuh dan
   labelnya muncul di SAMPING ikon dengan ukuran 13,5px -- ukuran yang memang
   bisa dibaca. Yang berdiri diam jadi lebih sepi, dan yang dibaca jadi
   terbaca.

   RAIL TIDAK IKUT ALIRAN FLEX. Ruangnya dipesan .d-shell::before selebar
   80px; rail sendiri position:fixed di atasnya. Kalau rail ikut aliran,
   melebar jadi 248px berarti .d-viewport menyusut 168px SETIAP KALI kursor
   lewat -- dan setiap tabel di sebelahnya menghitung ulang lebar kolomnya,
   teksnya melompat, baris yang sedang dibaca berpindah tempat. Dengan fixed
   ia cuma menutupi, tidak mendorong.
   ------------------------------------------------------------------------- */
.d-shell {
    position: relative;
    z-index: 1;
    display: flex;
    height: 100vh;
    overflow: hidden;
}

/* Pemesan ruang, dan hanya itu. Sengaja pseudo-elemen, bukan <div> di
   header.php: ia murni urusan tata letak desktop, dan menaruhnya di markup
   berarti ada satu elemen kosong yang ikut terkirim ke HP -- tempat rail
   ini tidak dipakai sama sekali. */
.d-shell::before {
    content: '';
    flex: 0 0 80px;
}

.d-rail {
    position: fixed;
    left: 0;
    top: 0;
    bottom: 0;
    z-index: 40;
    width: 80px;
    /* WAJIB. Tanpa border-box, padding 12px membuat lebar terpakainya 104px
       dan rail menimpa konten dalam keadaan DIAM, bukan cuma saat melebar. */
    box-sizing: border-box;
    padding: 18px 12px;
    display: flex;
    flex-direction: column;
    gap: 3px;
    overflow: hidden;
    background: var(--d-rail-kaca);
    backdrop-filter: var(--d-kaca-rail);
    -webkit-backdrop-filter: var(--d-kaca-rail);
    box-shadow: var(--d-rail-cincin);
    transition:
        width .5s var(--d-ease-rail),
        background .4s var(--d-ease-rail),
        box-shadow .4s var(--d-ease-rail);
}

/* FOKUS PAPAN TIK IKUT MEMBUKA RAIL, TAPI HANYA FOKUS PAPAN TIK.

   Yang berpindah dengan Tab menyusuri tiga belas pintu yang labelnya tidak
   terbaca kalau rail cuma melebar untuk tetikus. Jadi fokus harus ikut
   disebut.

   Yang TIDAK boleh ikut adalah fokus yang datang dari klik. Menekan butir
   rail memindahkan halaman, dan tautan yang barusan diklik tetap memegang
   fokus di halaman baru. Dengan :focus-within rail itu bertahan terbuka
   meski kursor sudah lama pergi -- sampai orangnya menekan sesuatu yang
   lain untuk melepaskannya. :has(:focus-visible) memisahkan keduanya:
   peramban cuma memasang :focus-visible pada fokus dari papan tik. */
/* .d-rail-tahan: MELEBAR TANPA KURSOR DI ATASNYA, dipasang JS.

   Menekan butir rail memindahkan halaman, dan halaman baru dimuat dengan
   rail dalam keadaan menyempit -- peramban belum menilai :hover sekali pun
   sebelum tetikus bergerak lagi. Yang terlihat: rail mengatup sendiri tepat
   di saat orangnya masih menunjuk ke sana, lalu membuka lagi begitu tetikus
   digerakkan 1px. Kelas ini menutup jeda itu; pelepasannya di pasangRail()
   (app.js) menunggu gerakan tetikus berikutnya, karena baru saat itulah
   peramban tahu kursornya sungguh-sungguh di mana. */
/* MELEBAR SAAT DISOROT HANYA KALAU ADA YANG BISA MENYOROT.

   Layar sentuh memasang :hover pada apa pun yang disentuh. Di tablet
   (rail masih tampil di atas 768px) menyentuh butir rail memulai animasi
   melebar 0,5 detik, lalu halaman berpindah di tengah animasi itu -- yang
   terlihat rail membuka separuh lalu hilang. Dilaporkan pemilik dari iPad
   sebagai "kesannya seperti glitch", dan memang begitu: melebar itu
   pratinjau untuk yang sedang MENGARAHKAN kursor, dan di layar sentuh
   tidak ada yang mengarahkan -- sentuhan langsung berarti berpindah.

   .d-rail-tahan dan :has(:focus-visible) sengaja TETAP di luar media query:
   yang pertama dipasang JS dan hanya hidup di jalur bertetikus, yang kedua
   milik pengguna papan tik yang menyusuri rail dengan Tab dan justru butuh
   labelnya terbaca. */
@media (hover: hover) {
    .d-rail:hover { width: 248px; background: var(--d-rail-kaca-buka); }
}
.d-rail.d-rail-tahan,
.d-rail:has(:focus-visible) {
    width: 248px;
    background: var(--d-rail-kaca-buka);
}

/* ---- kepala: merek ---- */
.d-rail-kepala {
    display: flex;
    align-items: center;
    gap: 12px;
    height: 42px;
    /* Padding kiri 12px BUKAN angka bebas: rail selebar 80px punya padding
       12px, dan lambang 32px berpusat di 12 + 12 + 16 = 40 -- tepat di
       tengah rail yang menyempit. Item di bawah dan avatar di kaki dihitung
       dengan cara yang sama, jadi ketiga lingkaran itu berdiri di SATU
       sumbu tegak. Meleset 2px saja sudah terlihat sebagai rail yang
       goyang. */
    padding: 0 12px;
    margin-bottom: 12px;
    flex: 0 0 auto;
    color: var(--text);
    text-decoration: none;
    /* Nama tempat, bukan salah satu tujuan -- karena itu lebih besar dan
       lebih tebal daripada label butir di bawahnya. */
    font-size: 14px;
    font-weight: 800;
    letter-spacing: -.02em;
}
.d-rail-kepala:hover { text-decoration: none; color: var(--text); }
.d-rail-lambang {
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: linear-gradient(150deg, color-mix(in srgb, var(--primary) 78%, #fff 22%), var(--primary));
    color: #fff;
    font-size: 14px;
    font-weight: 800;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 8px 18px -10px color-mix(in srgb, var(--primary) 85%, transparent);
}
/* Kata kedua merek diredupkan, bukan dihilangkan: "Diamondo" saja tidak
   memberi tahu aplikasi apa ini, dan dua kata setebal sama saling
   berkelahi. */
.d-rail-kepala .d-rail-teks span { color: var(--muted); font-weight: 700; }

/* ---- daftar butir ---- */
/* Rail bisa lebih panjang daripada layar pendek. Yang menggulir DAFTARNYA,
   bukan seluruh rail -- merek di atas dan avatar di bawah harus tetap di
   tempatnya. Itu juga yang membuat enam @media (max-height) versi lama
   tidak diperlukan lagi: sisa item tidak hilang, ia tinggal digulir. */
.d-rail-daftar {
    /* Patokan .d-rail-sorot. Daftar ini juga wadah gulir, jadi pil-nya ikut
       tergulir bersama butirnya -- yang memang harus. */
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 3px;
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: none;
    min-height: 0;
    flex: 1 1 auto;
}
.d-rail-daftar::-webkit-scrollbar { display: none; }

.d-rail-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: 13px;
    height: var(--d-rail-item-h, 46px);
    /* 17px: ikon 22px berpusat di 12 + 17 + 11 = 40. Lihat catatan sumbu di
       .d-rail-kepala. */
    padding: 0 17px;
    flex: 0 0 auto;
    border: none;
    background: none;
    cursor: pointer;
    border-radius: 999px;
    color: var(--muted);
    text-decoration: none;
    font-size: 13.5px;
    font-weight: 700;
    letter-spacing: -.015em;
    transition:
        background .34s var(--d-ease-rail),
        box-shadow .34s var(--d-ease-rail),
        transform .34s var(--d-ease-rail),
        color .34s var(--d-ease-rail);
}
/* Ikut dipagari dengan alasan yang sama: di layar sentuh butir yang
   barusan disentuh tertinggal menyala dan bergeser 2px sampai ada yang
   disentuh lagi -- sorotan atas halaman yang sudah ditinggalkan. */
@media (hover: hover) {
    .d-rail-item:hover {
        background: var(--d-rail-butir-hover);
        box-shadow: var(--d-rail-butir-cincin);
        transform: translateX(2px);
        color: var(--text);
        text-decoration: none;
    }
}
/* Keadaan aktif: sekeping kaca yang TERANGKAT dari rail. Sengaja BUKAN
   kotak tint aksen + garis 3px seperti dulu, dan sejak 6 Sep 2026 juga
   bukan tulisan beraksen -- alasannya sama untuk keduanya: warna aksen di
   chrome terbaca sebagai peringatan, bukan sebagai penunjuk tempat.

   Jadi seluruh beban menyatakan "halaman ini" dipikul BENTUKNYA, bukan
   warnanya. Supaya sanggup memikul itu, kepingnya harus benar-benar
   terbaca punya tebal: gradien permukaan + empat lapis bayangan (lihat
   --d-rail-butir-timbul) + pembias sendiri di atas kaca rail. */
/* Butir aktif TIDAK menggambar pil-nya sendiri lagi -- itu tugas
   .d-rail-sorot di bawah. Yang tertinggal di sini hanya yang memang milik
   butirnya: warna dan bobot hurufnya. Pil yang dimiliki butir tidak bisa
   berpindah ke butir lain; satu pil bersama bisa. */
.d-rail-item.aktif {
    color: var(--d-rail-aktif-teks);
    font-weight: 800;
}
/* Yang aktif tidak ikut bergeser 2px saat disentuh: ia sudah terangkat,
   dan benda yang sudah terangkat tidak bergeser lagi hanya karena
   dilewati kursor. Latar hover-nya juga ditahan -- kalau ikut, ia
   menumpuk di atas pil dan butir aktif jadi lebih terang daripada
   seharusnya justru saat ditunjuk. */
.d-rail-item.aktif:hover { transform: none; background: none; }

/* ---- Pil aktif: satu untuk seluruh rail, BERPINDAH ----

   Dua lapis, dan pembagiannya bukan gaya-gayaan:

     .d-rail-sorot   memegang POSISI (transform: translateY + height)
     ::before        memegang RUPA (gradien, timbul, pembias) + lentingan

   Harus dipisah karena properti `scale` dan `transform` tidak berdiri
   sendiri-sendiri: peramban menerapkannya sebagai translate * rotate *
   scale * transform, sehingga scaleY ikut MENGALIKAN translateY. Pil di
   offset 300px yang dilentingkan 1.08 akan melompat 24px ke bawah di
   tengah perjalanannya. Dengan lentingan dipasang di ::before -- kotak
   sendiri, titik pusat sendiri, tanpa translate apa pun -- keduanya tidak
   pernah bertemu.

   left/right 0 (bukan lebar tetap) supaya pil ikut melar sendiri saat rail
   membuka dari 80px ke 248px: lebarnya dihitung ulang tiap frame oleh
   transisi width milik rail, jadi tidak perlu dianimasikan terpisah dan
   tidak akan pernah meleset dari lebar butirnya. */
.d-rail-sorot {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: var(--d-rail-item-h, 46px);
    opacity: 0;
    pointer-events: none;
    transition:
        transform .46s var(--d-ease-rail),
        height .46s var(--d-ease-rail),
        opacity .22s linear;
}
.d-rail-sorot::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 999px;
    background: var(--d-rail-butir-aktif);
    box-shadow: var(--d-rail-butir-timbul);
    backdrop-filter: var(--d-rail-butir-bias);
    -webkit-backdrop-filter: var(--d-rail-butir-bias);
}
.d-rail-sorot.tampil { opacity: 1; }
/* Dipakai saat pil ditaruh pertama kali (dan saat jendela diubah ukurannya):
   di situ ia harus MUNCUL di tempatnya, bukan meluncur dari sudut kiri
   atas. */
.d-rail-sorot.diam { transition: none; }

/* Lentingan cairnya. Bukan sekadar hiasan: benda yang berpindah tanpa
   berubah bentuk terbaca sebagai gambar yang digeser, sedangkan yang
   memanjang searah jalannya lalu memantul pendek terbaca sebagai sesuatu
   yang punya massa. Memanjang di sumbu Y (arah jalannya) sambil menyempit
   sedikit di X, lalu satu pantulan balik yang lebih kecil sebelum diam --
   perbandingan 1.09 : 0.97 itu yang membuatnya berhenti terasa mengendap,
   bukan mengunci. */
@keyframes d-sorot-lentur {
    0%   { transform: scale(1, 1); }
    38%  { transform: scale(.94, 1.09); }
    70%  { transform: scale(1.03, .97); }
    100% { transform: scale(1, 1); }
}
.d-rail-sorot.melenting::before { animation: d-sorot-lentur .46s var(--d-ease-rail); }

/* Yang minta gerakan dikurangi tetap dapat penanda tempatnya -- yang
   dibuang cuma perjalanannya. */
@media (prefers-reduced-motion: reduce) {
    .d-rail-sorot { transition: opacity .22s linear; }
    .d-rail-sorot.melenting::before { animation: none; }
}

/* Pembungkus ikon. Ada supaya titik notifikasi punya patokan yang tidak
   bergerak: kalau titiknya diposisikan terhadap BUTIRNYA, ia ikut melompat
   ke ujung kanan begitu butir melebar dari 56px jadi 224px -- menjauh dari
   ikon yang seharusnya ia tandai. */
.d-rail-ikon {
    position: relative;
    flex: 0 0 auto;
    display: flex;
}
.d-rail-item svg { width: var(--d-rail-ikon, 22px); height: var(--d-rail-ikon, 22px); display: block; }


/* Label muncul BERURUTAN saat rail melebar -- jeda tiap butir sedikit lebih
   lama daripada butir di atasnya, jadi terbaca sebagai satu gerakan
   menyapu ke bawah, bukan sebagai tiga belas kata yang menyala serentak.
   --i diisi PHP (lihat d_rail() di includes/ui.php).

   Saat rail MENYEMPIT jedanya nol lagi supaya semuanya pudar bersamaan:
   gerakan berurutan saat menutup terbaca sebagai lambat, bukan halus. */
.d-rail-label {
    opacity: 0;
    white-space: nowrap;
    transition: opacity .34s var(--d-ease-out);
    transition-delay: 0s;
}
@media (hover: hover) {
    .d-rail:hover .d-rail-label {
        opacity: 1;
        transition-delay: calc(.05s + var(--i, 0) * .012s);
    }
}
.d-rail.d-rail-tahan .d-rail-label,
.d-rail:has(:focus-visible) .d-rail-label {
    opacity: 1;
    transition-delay: calc(.05s + var(--i, 0) * .012s);
}

/* Titik merah notifikasi (stok di bawah minimum / resi menunggu). Ia SENGAJA
   bukan bagian dari .d-rail-label yang memudar: justru saat rail menyempit
   dan labelnya tidak terlihat, titik inilah satu-satunya yang memberi tahu
   ada sesuatu yang menunggu. Cincin --card-solid memisahkannya dari ikon di
   belakangnya, baik mode terang maupun gelap. */
.d-rail-dot {
    position: absolute;
    top: -2px;
    right: -3px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--red);
    box-shadow: 0 0 0 2px var(--card-solid);
}

.d-rail-pisah {
    width: auto;
    height: 1px;
    background: var(--border);
    margin: 7px 17px;
    flex: 0 0 auto;
}
/* Sisa dari rail lama: pendorong yang menjaga avatar tetap di dasar. Kini
   .d-rail-daftar yang flex:1, jadi elemen ini tidak punya tugas lagi.
   Aturannya ditinggal (bukan dihapus) supaya halaman yang menyusun
   navigasinya sendiri tidak tiba-tiba menyisakan kotak kosong. */
.d-rail-isi { display: none; }

/* ---- kaki: avatar + nama ---- */
.d-rail-kaki {
    display: flex;
    align-items: center;
    gap: 12px;
    height: 48px;
    /* 11px: avatar 34px berpusat di 12 + 11 + 17 = 40. Lihat catatan sumbu
       di .d-rail-kepala. */
    padding: 0 11px;
    margin-top: 8px;
    box-sizing: border-box;
    flex: 0 0 auto;
    border-radius: 999px;
    color: var(--muted);
    text-decoration: none;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: normal;
    transition: background .3s var(--d-ease-rail), box-shadow .3s var(--d-ease-rail);
}
/* Kaki punya keadaan hover yang sama dengan butir: ia juga sebuah tujuan
   (halaman Pengaturan), bukan sekadar keterangan siapa yang sedang masuk.
   Karena itu ia ikut dipagari @media (hover: hover) seperti butir rail --
   waktu tiga aturan rail dipagari, yang ini terlewat, dan gejala "menyala
   lalu halaman pindah di tengah animasi" yang dilaporkan dari iPad masih
   tersisa persis di satu elemen ini. */
@media (hover: hover) {
    .d-rail-kaki:hover {
        background: var(--d-rail-butir-hover);
        box-shadow: var(--d-rail-butir-cincin);
        text-decoration: none;
        color: var(--text);
    }
    .d-rail-kaki:hover .d-rail-avatar { transform: scale(1.06); }
}
/* Nama panjang dipotong, tidak membungkus: rail 248px punya ruang untuk
   satu baris, dan nama yang pecah dua baris membuat kaki lebih tinggi
   daripada tempat yang disediakan untuknya. */
.d-rail-kaki .d-rail-label {
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}

.d-rail-avatar {
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: none;
    cursor: pointer;
    background: linear-gradient(150deg, #a82440, #96371f);
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    text-decoration: none;
    transition: transform .3s var(--d-ease-pop);
}
.d-rail-avatar img { width: 100%; height: 100%; object-fit: cover; }

/* -------------------------------------------------------------------------
   Saklar terang/gelap (d_tema_tombol di ui.php, Uwms.gantiTema di app.js)
   Permintaan pemilik (gladi 11 Sep): kecil, di atas avatar; matahari saat
   terang, bulan sabit saat gelap; berganti dengan gerak mengikuti busur.

   KEDUA IKON BERPUTAR MENGELILINGI TITIK DI BAWAH TOMBOL (transform-origin
   50% 185%), jadi rotasi = bergerak di sepanjang busur langit: yang lama
   terbenam ke kanan-bawah, yang baru terbit dari kiri-bawah -- selalu ke
   arah yang sama, seperti langit sungguhan. overflow:hidden memotongnya
   di tepi lingkaran tombol.
   ------------------------------------------------------------------------- */
/* padding-left 13px: pusat tombol 30px jatuh di x=40, segaris dengan pusat
   avatar (diukur: 25px membuatnya bergeser 12px ke kanan). */
.d-rail-tema { display: flex; padding-left: 13px; margin: 4px 0 2px; }
.d-tema-tombol {
    position: relative;
    flex: 0 0 auto;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    overflow: hidden;
    transition: background .2s, transform .2s;
}
.d-tema-tombol:hover { background: var(--d-rail-butir-hover, var(--field)); transform: scale(1.08); }
.d-tema-tombol:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.d-tema-tombol svg {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 18px;
    height: 18px;
    margin: -9px 0 0 -9px;
    transform-origin: 50% 185%;
}
.d-tema-matahari { color: #f4a526; }
.d-tema-bulan    { color: #b9c2f2; }
/* Keadaan diam: yang berlaku di puncak busur, yang lain di bawah cakrawala kiri. */
.d-tema-bulan { transform: rotate(-80deg); opacity: 0; }
html[data-theme="dark"] .d-tema-matahari { transform: rotate(-80deg); opacity: 0; }
html[data-theme="dark"] .d-tema-bulan    { transform: none; opacity: 1; }
/* Sinar matahari berputar pelan saat disorot -- satu-satunya gerak diam. */
.d-tema-sinar { transform-origin: 12px 12px; transition: transform .6s cubic-bezier(.2,.8,.2,1); }
.d-tema-tombol:hover .d-tema-sinar { transform: rotate(45deg); }

@keyframes d-tema-terbenam { from { transform: none; opacity: 1; }
                             to   { transform: rotate(80deg); opacity: 0; } }
@keyframes d-tema-terbit   { from { transform: rotate(-80deg); opacity: 0; }
                             to   { transform: none; opacity: 1; } }
/* .berganti dipasang JS sesaat setelah tema diganti: yang BARU terbit, yang
   LAMA terbenam. Mana yang baru dibaca dari tema yang sedang berlaku. */
/* Keduanya: jangka waktu yang sama, laju yang sama, mulai dan selesai di
   detik yang sama dengan sapuan lingkarannya. linear, bukan kurva melenting:
   yang satu terbenam persis secepat yang lain terbit, jadi yang terlihat
   satu benda menyusuri busur -- bukan dua benda dengan rencana sendiri. */
html[data-theme="dark"] .d-tema-tombol.berganti .d-tema-bulan,
html:not([data-theme="dark"]) .d-tema-tombol.berganti .d-tema-matahari {
    animation: d-tema-terbit var(--d-tema-durasi) linear both;
}
html[data-theme="dark"] .d-tema-tombol.berganti .d-tema-matahari,
html:not([data-theme="dark"]) .d-tema-tombol.berganti .d-tema-bulan {
    animation: d-tema-terbenam var(--d-tema-durasi) linear both;
}
@media (prefers-reduced-motion: reduce) {
    .d-tema-tombol.berganti svg { animation-duration: .01s !important; }
}
/* Pergantian warna halaman: lingkaran tema baru menyebar dari tombol
   (View Transitions, dijalankan app.js). Animasi bawaan -- silang-pudar --
   dimatikan supaya yang terlihat hanya lingkarannya. */
::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; }

/* HP: tombol di header, sejajar dengan tombol pindai & avatar. */
.m-tema { width: 34px; height: 34px; border: 1px solid var(--border); background: var(--card-solid); }
.m-tema svg { width: 19px; height: 19px; margin: -9.5px 0 0 -9.5px; }

/* Rail menyesuaikan TINGGI JENDELA.
   ------------------------------------------------------------------
   Jauh lebih longgar daripada versi ikon-di-atas-label, karena butir
   horizontal tidak perlu menampung dua baris. Rail terpenuh (pemilik:
   super admin + fitur reseller) berisi 13 butir + 2 pemisah:

       13 x (H + 3)  +  2 x 15 pemisah  +  54 kepala + 56 kaki + 36 padding

   Dengan H bawaan 46 itu 813px. Di jendela laptop ~770px daftarnya sudah
   menggulir -- dan menggulir memang jalan keluar yang benar di sini
   (.d-rail-daftar punya overflow-y:auto), tapi butir yang sedikit lebih
   pendek lebih baik daripada butir yang harus dicari dengan menggulir.
   Dua ambang sudah cukup; di bawah 640px sisanya diserahkan ke gulir. */
@media (max-height: 820px) {
    .d-rail { --d-rail-item-h: 42px; }
}
@media (max-height: 700px) {
    .d-rail { --d-rail-item-h: 38px; --d-rail-ikon: 21px; }
    .d-rail-kepala { height: 38px; margin-bottom: 8px; }
    .d-rail-kaki { height: 42px; }
}
/* Di bawah 600px daftarnya tetap akan menggulir walau butirnya dipendekkan
   -- tiga belas butir memang tidak muat. Yang dikejar ambang ini bukan
   "semuanya terlihat" melainkan "yang tersembunyi tinggal satu-dua", karena
   batang gulir daftarnya sengaja tak terlihat: makin banyak yang perlu
   digulir, makin besar peluang orang tidak tahu ada yang di bawah sana. */
@media (max-height: 600px) {
    .d-rail { --d-rail-item-h: 34px; --d-rail-ikon: 20px; }
    .d-rail-pisah { margin: 4px 17px; }
    .d-rail-kaki { height: 38px; margin-top: 4px; }
}

/* -------------------------------------------------------------------------
   6. AREA KONTEN (kerangka -- desktop saja)
   ------------------------------------------------------------------------- */
.d-viewport { flex: 1; min-width: 0; height: 100%; overflow-y: auto; overflow-x: hidden; }
.d-page {
    max-width: 1180px;
    margin: 0 auto;
    padding: 30px 40px 80px;
    animation: d-screenIn .5s var(--d-ease-out) backwards;
}

} /* ==== akhir kerangka desktop-only ==== */

/* =========================================================================
   KOMPONEN -- BERLAKU DI SEMUA LEBAR
   =========================================================================
   Semua di bawah ini SENGAJA di luar @media (min-width:769px).

   Alasannya: dari 20-an halaman app, cuma 6 yang punya markup mobile
   sendiri (blok .m-only). Sisanya -- Pencarian, Barang Keluar, Transaksi,
   Biaya, Laporan, Pengaturan, Tenant, Scan -- memakai markup yang SAMA di
   HP maupun desktop, dan selama ini mobile.css yang meneman ulang markup
   itu (.card, .responsive-table, dst). Begitu halaman-halaman itu pindah
   ke class .d-*, mobile.css tidak lagi mengenalinya; kalau gaya .d-* juga
   terkunci di min-width:769px, hasilnya halaman tanpa gaya sama sekali di
   HP -- bukan "mobile tidak tersentuh", tapi mobile rusak.

   Yang tetap desktop-only hanyalah KERANGKA (rail, viewport, padding
   halaman, latar ambient) -- itu memang digantikan topbar + tab bar milik
   mobile.css. Komponen isinya dipakai bersama, dengan penyesuaian sempit
   di blok @media (max-width:768px) paling bawah.
   ========================================================================= */

.d-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 20px;
    flex-wrap: wrap;
    margin-bottom: 22px;
}
.d-head h1 { margin: 0; font-size: 32px; font-weight: 700; letter-spacing: -.035em; }
.d-head p  { margin: 5px 0 0; font-size: 14px; color: var(--muted); }
.d-head-aksi { display: flex; gap: 9px; align-items: center; }

/* RITME: jarak ANTAR kelompok jauh lebih besar daripada jarak DI DALAM
   kelompok. Dulu keduanya sama-sama 14-20px, dan jarak yang seragam
   membuat sepuluh benda terbaca sebagai satu daftar panjang tanpa
   pengelompokan -- persis kesan "ditambahkan satu per satu".

   34px di atas, 14px di bawah: judul seksi menempel pada isinya sendiri
   dan menjauh dari kelompok sebelumnya. Itu yang membuat mata memotong
   halaman jadi beberapa bagian tanpa perlu garis pembatas. */
.d-seksi {
    display: flex;
    align-items: baseline;
    gap: 12px;
    margin: 34px 0 14px;
}
/* Kelompok pertama tidak punya apa-apa di atasnya untuk dijauhi. */
.d-page > .d-seksi:first-child,
.d-filter + .d-seksi { margin-top: 4px; }
/* Di dalam .d-tumpuk (panel tab di Scan & Biaya) jaraknya sudah ditanggung
   `gap: 14px` milik flex, dan margin flex item DITAMBAHKAN ke gap, bukan
   dilebur seperti margin biasa. Tanpa aturan ini jarak di atas label seksi
   jadi 34+14 = 48px -- bukan lapang, melainkan bolong. */
.d-tumpuk > .d-seksi { margin: 20px 0 0; }
/* Label kelompok: huruf kecil berspasi lebar. Ia KETERANGAN, bukan judul --
   yang berhak tampil sebagai judul cuma satu di tiap halaman (.d-head h1),
   dan label seksi yang setebal judul akan bersaing dengannya. */
.d-seksi-teks {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--faint);
    flex: 0 0 auto;
}
.d-seksi::after { content: ""; flex: 1; height: 1px; background: var(--hair); }

/* ---- Teks telanjang di atas wallpaper foto ----
   Judul halaman, subjudul, dan label seksi tidak punya kartu di belakangnya.
   Di atas gradien turunan tema itu aman; di atas foto arbitrer tidak --
   teks #1d1d1f mode terang di atas foto malam praktis hilang.

   Yang ditimpa HANYA elemen telanjang itu. Sengaja bukan --text di level
   .d-page: token itu diwarisi juga oleh isi kartu, yang punya latar sendiri
   dan kontrasnya sudah benar -- menimpanya di sana justru merusak.

   Kombinator KETURUNAN (spasi), bukan anak-langsung (>). Terlihat seperti
   bisa dirapikan jadi '>' tapi JANGAN: badan halaman masih dibungkus
   .m-analitik > .m-analitik-body (panel geser Analitik milik UI mobile,
   lihat index.php), jadi .d-head bukan anak langsung .d-page dan seluruh
   aturan ini diam-diam tidak pernah cocok. */
.d-atas-foto-gelap .d-head h1,
.d-atas-foto-gelap .d-seksi-teks { color: #f5f5f7; text-shadow: 0 1px 4px rgba(0,0,0,.45); }
.d-atas-foto-gelap .d-head p { color: rgba(255,255,255,.78); text-shadow: 0 1px 4px rgba(0,0,0,.45); }
.d-atas-foto-gelap .d-seksi::after { background: rgba(255,255,255,.22); }

.d-atas-foto-terang .d-head h1,
.d-atas-foto-terang .d-seksi-teks { color: #1d1d1f; text-shadow: 0 1px 4px rgba(255,255,255,.5); }
.d-atas-foto-terang .d-head p { color: rgba(0,0,0,.68); text-shadow: 0 1px 4px rgba(255,255,255,.5); }
.d-atas-foto-terang .d-seksi::after { background: rgba(0,0,0,.18); }

/* Tombol ghost di kepala halaman juga duduk telanjang di atas foto, dan
   latarnya (--field) cuma 9% opak -- di atas foto ia hilang sama sekali.
   Diberi permukaan padat khusus saat wallpaper menyala. */
.d-atas-foto-gelap .d-head .d-btn-ghost,
.d-atas-foto-terang .d-head .d-btn-ghost {
    background: var(--card-solid);
    border: 1px solid var(--border);
}

/* -------------------------------------------------------------------------
   7. KARTU
   ------------------------------------------------------------------------- */
.d-card {
    background: var(--card);
    backdrop-filter: var(--d-kaca-card);
    -webkit-backdrop-filter: var(--d-kaca-card);
    /* Tanpa border. Tepinya digambar cincin inset di dalam --d-shadow-card
       -- alasannya di blok token, dan yang paling menentukan: border
       menambah 1px ke kotak, dan baris tabel di sini memakai grid yang
       jalurnya harus identik dengan judul kolomnya. */
    border-radius: var(--d-r-card);
    box-shadow: var(--d-shadow-card);
    padding: var(--d-pad-card);
    animation: d-fadeUp .55s var(--d-ease-out) backwards;
}
/* Dua kartu yang bersaudara langsung tetap berjarak 14px (handoff: "gap antar
   kartu 14px"). .d-card sendiri tidak punya margin, jadi kartu yang ditulis
   berurutan TANPA pembungkus .d-grid/.d-tumpuk akan benar-benar bersentuhan --
   terukur gap 0 di Performa Reseller & Integrasi. Di dalam pembungkus yang
   sudah punya gap, marginnya dinolkan lagi supaya tidak dobel. */
/* Pemilih SAUDARA UMUM (~), bukan saudara langsung (+): di antara dua kartu
   sering ada blok tersembunyi sumber-isi-sheet (mis. #blokGantiReseller),
   dan satu elemen di tengah sudah cukup membuat "+" tidak cocok -- kedua
   kartunya lalu menempel tanpa jarak sama sekali. */
.d-card ~ .d-card { margin-top: 14px; }
.d-grid > .d-card ~ .d-card,
.d-tumpuk > .d-card ~ .d-card,
.d-stack > .d-card ~ .d-card,
/* .scan-konsol WAJIB ikut di sini. Kedua kolomnya .d-card bersaudara, jadi
   yang kanan kena margin-top 14px dan berdiri 14px lebih pendek daripada
   kiri -- terukur, bukan dugaan. Konsol yang kedua kolomnya tidak sejajar
   justru membatalkan hal yang membuatnya terbaca sebagai satu perkakas. */
.scan-konsol > .d-card ~ .d-card { margin-top: 0; }
/* KARTU DI DALAM PEMBUNGKUS keluar dari jangkauan "~" di atas: kartu di dalam
   <form> dan kartu sesudah form bukan saudara, jadi keduanya menempel 0px.
   Terukur waktu gladi bersih 11 Sep: Pindah Petak, Terima Kiriman Besar,
   Pengaturan (form berisi kartu lalu kartu riwayat), dan layar konfirmasi
   terima outlet (form lalu kartu "Ada selisih"). Dua arah ditangani:
   kartu SESUDAH pembungkus-berkartu, dan kartu pertama di pembungkus yang
   datang SESUDAH kartu. Hanya anak langsung (> .d-card) -- pembungkus
   sheet tersembunyi yang kartunya lebih dalam tidak ikut menggeser apa pun.
   "Kartu pertama" ditulis :not(.d-card ~ .d-card), BUKAN :first-child: form
   di sini selalu diawali <input> CSRF tersembunyi, jadi kartunya tidak
   pernah jadi anak pertama. */
.d-page > :not(.d-card):has(> .d-card) ~ .d-card,
.d-page > .d-card ~ :not(.d-card) > .d-card:not(.d-card ~ .d-card),
.d-page > :not(.d-card):has(> .d-card) ~ :not(.d-card) > .d-card:not(.d-card ~ .d-card) { margin-top: 14px; }

.d-card-h2 {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    margin: 0 0 16px;
}
.d-card-h2 > :first-child { font-size: 15px; font-weight: 700; letter-spacing: -.015em; }
/* 15px, turun dari 17px. Di halaman ini sudah ada DUA tingkat judul di
   atasnya -- judul halaman 32px dan label seksi 11px berspasi -- dan judul
   kartu 17px tebal berdiri terlalu dekat dengan judul halaman untuk
   terbaca sebagai tingkat ketiga. Yang dibaca mata jadi "banyak judul
   sederajat", dan itu bagian dari kesan tidak tertata. */

.d-grid { display: grid; gap: 14px; }

/* =========================================================================
   BENTANG ANGKA -- bukan deretan kartu
   =========================================================================
   Sebelum 5 Sep 2026 baris ini adalah EMPAT KARTU berdiri sendiri, dan di
   bawahnya ENAM kartu rasio yang juga berdiri sendiri. Sepuluh kotak dengan
   bobot visual yang persis sama, bertumpuk. Yang dibaca mata bukan "empat
   angka penting lalu enam angka pendukung", melainkan "sepuluh benda", dan
   dasbor yang terbaca sebagai timbunan benda memang terlihat seperti disusun
   satu per satu tanpa rencana.

   Yang diperbaiki BUKAN rupa tiap kartu, melainkan JUMLAH PERMUKAANNYA.
   Empat kartu KPI sekarang menjadi SATU bentang dengan empat sel di
   dalamnya; enam kartu rasio menjadi SATU pita telanjang tanpa permukaan
   sama sekali. Sepuluh kotak jadi satu permukaan + satu pita.

   Hierarkinya lalu datang dari BAHAN, bukan dari ukuran huruf: yang utama
   punya permukaan kaca, yang pendukung berdiri langsung di atas latar, yang
   berisi daftar tetap berupa kartu. Tiga bahan untuk tiga derajat
   kepentingan -- dan itu yang membuat urutannya terbaca tanpa perlu
   dijelaskan.

   SENGAJA DIKERJAKAN DI CSS, bukan dengan menyunting markup tiap halaman.
   .d-grid-kpi dipakai Dasbor, Biaya, dan Laporan; .d-grid-rasio dipakai
   Dasbor, Keuangan, dan Reseller. Mengubahnya di satu tempat membuat
   keenam halaman itu berpindah bersama-sama, dan tidak ada halaman yang
   tertinggal memakai bentuk lama. */
.d-grid-kpi {
    grid-template-columns: repeat(auto-fit, minmax(238px, 1fr));
    /* Nol -- selnya harus BERSENTUHAN supaya terbaca sebagai satu benda
       berpetak. Jarak sekecil apa pun di antaranya mengembalikan bacaan
       "beberapa benda berdampingan". */
    gap: 0;
    background: var(--card);
    backdrop-filter: var(--d-kaca-card);
    -webkit-backdrop-filter: var(--d-kaca-card);
    border-radius: var(--d-r-card);
    box-shadow: var(--d-shadow-card);
    /* `hidden`, DAN INI YANG MENJAGA BENTUKNYA.

       Sempat `visible` waktu sel yang disentuh berubah jadi kartu bersudut
       bulat yang terangkat keluar dari bentangnya -- dengan `hidden` kartu
       itu terpotong rata dan terbaca terpenggal. Niatnya dibalik 6 Sep 2026
       atas permintaan pemilik: sel TIDAK BOLEH lagi membulat saat disentuh,
       ia harus mempertahankan bentuk petaknya. Yang tersisa cuma zoom-nya.

       Begitu selnya tidak membulat, memotong di wadah justru jadi jawaban
       yang paling benar -- dan benar SENDIRINYA. Sel tengah terpotong lurus
       di atas dan bawah, jadi tetap siku. Sel ujung terpotong mengikuti
       sudut bentang, jadi sisi luarnya membulat persis seperti bentangnya
       sementara sisi dalamnya tetap siku. Tidak ada satu pun aturan sudut
       per-sel yang perlu ditulis, dan yang lebih penting: ia tetap benar
       berapa pun kolom yang terbentuk saat gridnya membungkus. Aturan
       :first-child/:last-child akan salah begitu selnya turun baris. */
    overflow: hidden;
    animation: d-fadeUp .55s var(--d-ease-out) backwards;
}

/* =========================================================================
   BENTANG GABUNGAN -- angka + grafiknya jadi satu benda
   =========================================================================
   Grafik Tren Harian menggambarkan PERSIS empat angka yang ada di bentang
   KPI: Omzet, Laba Kotor, Laba Bersih, Transaksi. Selama ia berdiri sebagai
   kartu tersendiri di bawah sana, dasbor menambah satu kotak untuk data yang
   sudah ditampilkan -- dan memutus hubungan antara angka dengan bentuk
   geraknya, padahal justru hubungan itu yang dicari orang saat membuka
   dasbor.

   .d-bentang membungkus keduanya jadi satu permukaan. Permukaannya PINDAH
   ke pembungkus ini, dan .d-grid-kpi di dalamnya dinolkan -- bukan
   sebaliknya (menaruh grafik sebagai sel grid), karena grafik harus
   membentang selebar keempat sel sekaligus, bukan menempati satu jalur.

   .d-grid-kpi di LUAR .d-bentang tetap punya permukaannya sendiri. Biaya
   dan Laporan memakainya tanpa grafik, dan keduanya tidak boleh ikut
   kehilangan permukaan hanya karena Dasbor menggabungkannya dengan tren. */
.d-bentang {
    background: var(--card);
    backdrop-filter: var(--d-kaca-card);
    -webkit-backdrop-filter: var(--d-kaca-card);
    border-radius: var(--d-r-card);
    box-shadow: var(--d-shadow-card);
    overflow: hidden;
    animation: d-fadeUp .55s var(--d-ease-out) backwards;
}
.d-bentang > .d-grid-kpi {
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-radius: 0;
    box-shadow: none;
    overflow: visible;
    animation: none;
}
/* Bagian grafik. Garis rambut di punggungnya adalah satu-satunya yang
   memisahkannya dari deretan angka -- pemisah yang sama persis dengan yang
   dipakai antar sel, jadi grafik terbaca sebagai baris kedua dari benda
   yang sama, bukan sebagai lampiran. */
.d-bentang-tren {
    padding: 18px 22px 20px;
    box-shadow: 0 -1px 0 var(--hair);
}
/* Judul bagian ini SEKECIL label seksi, bukan sebesar judul kartu: ia
   keterangan untuk sebagian dari satu benda, bukan nama benda baru. */
.d-bentang-tren-judul {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--faint);
    margin: 0 0 10px;
}

/* BENTANG ISI BEBAS. Mekanismenya sama persis dengan bentang KPI -- satu
   permukaan, sel dipisah garis rambut outset yang terpotong di tepi -- tapi
   isinya tidak harus .d-kpi. Dipakai ringkasan "Per Kategori / Per
   Ekspedisi" di halaman Scan: dua panel yang menjawab satu pertanyaan ("apa
   saja yang sudah discan hari ini"), jadi dua permukaan untuk itu adalah
   satu permukaan kelebihan. */
.d-bentang-2 {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0;
    background: var(--card);
    backdrop-filter: var(--d-kaca-card);
    -webkit-backdrop-filter: var(--d-kaca-card);
    border-radius: var(--d-r-card);
    box-shadow: var(--d-shadow-card);
    overflow: hidden;
    animation: d-fadeUp .55s var(--d-ease-out) backwards;
}
.d-bentang-2 > * {
    padding: 20px 22px;
    box-shadow: -1px 0 0 var(--hair), 0 -1px 0 var(--hair);
    min-width: 0;
}

/* Pita rasio: SATU bentang juga, sama seperti KPI di atas.

   Versi pertama membuatnya telanjang tanpa permukaan, dengan tiap sel
   punya sudut membulat sendiri supaya sorotan hover-nya tidak bersudut
   tajam. Salah: sudut itu justru mengembalikan bacaan "enam kartu
   berdampingan" -- persis yang sedang dihilangkan. Sudut adalah tanda
   BATAS BENDA, dan benda yang batasnya kelihatan enam kali tetap terbaca
   sebagai enam benda, sekalipun tidak punya latar.

   Sekarang selnya bersudut nol dan menyatu, dan permukaannya yang
   membedakannya dari bentang KPI: cincin kaca saja, TANPA bayangan jatuh.
   Ia rata dengan halaman sementara bentang KPI mengambang di atasnya --
   hierarki yang tetap terbaca, tanpa satu pun sudut di tengah pita. */
.d-grid-rasio {
    grid-template-columns: repeat(auto-fit, minmax(196px, 1fr));
    gap: 0;
    background: color-mix(in srgb, var(--card) 55%, transparent);
    backdrop-filter: var(--d-kaca-card);
    -webkit-backdrop-filter: var(--d-kaca-card);
    border-radius: var(--d-r-card);
    box-shadow: var(--d-kaca-cincin);
    /* Sama seperti bentang KPI: `hidden`, supaya sudut sel mengikuti sudut
       bentangnya tanpa satu pun aturan per-sel. Lihat catatan panjang di
       .d-grid-kpi. */
    overflow: hidden;
}
/* Lebih rapat daripada sel bentang: angka pendukung tidak perlu selapang
   angka utama, dan perbedaan kepadatan itu ikut menyatakan derajatnya. */
.d-grid-rasio > .d-kpi {
    padding: 16px 18px;
    border-radius: 0;
}
/* Dasbor: kartu Rasio & Modal jadi ENAM sejak Total HPP dan Nilai Stok Gudang
   dipisah (15 Agu 2026). Dengan lantai 196px, lebar desktop biasa cuma muat 5
   jalur -- kartu ke-6 turun sendirian dan barisnya timpang. Lantainya
   diturunkan supaya keenamnya duduk satu baris.
   Di-scope ke body dasbor: .d-grid-rasio juga dipakai keuangan_view.php dan
   data_reseller.php dengan jumlah kartu yang lain, dan tidak ada alasan
   mengubah kepadatan di sana. */
body.dasbor-ringkas .d-grid-rasio,
body.dasbor-analitik .d-grid-rasio { grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); }
/* Enam kolom berarti tiap kartu ~146px ruang isi. Angka 20px membuat
   "Rp 198.611.295" (~145px) pas di ambang dan pecah jadi "Rp" / angka --
   satuan yang menggantung sendirian di satu baris. 18px menuntaskannya
   (~130px) tanpa mengubah kartu rasio di halaman lain. */
body.dasbor-ringkas .d-grid-rasio .d-kpi-nilai,
body.dasbor-analitik .d-grid-rasio .d-kpi-nilai { font-size: 18px; }
.d-grid-2     { grid-template-columns: 2fr 1fr; }
.d-grid-11    { grid-template-columns: 1fr 1fr; }
/* Dua kolom Pengaturan mulai menumpuk lebih awal daripada grid lain:
   kartunya berisi teks penjelas panjang, jadi di bawah ~1100px dua
   kolom sudah terlalu sempit untuk dibaca. */
@media (max-width: 1100px) { .d-atur-kolom { grid-template-columns: 1fr; } }
.d-grid-15    { grid-template-columns: 1.5fr 1fr; }

/* Kartu masuk bergantian, 45ms per kartu. Ditulis sebagai nth-child supaya
   halaman tidak perlu menghitung delay satu-satu di inline style. */
.d-grid > *:nth-child(1) { animation-delay: 0ms; }
.d-grid > *:nth-child(2) { animation-delay: 45ms; }
.d-grid > *:nth-child(3) { animation-delay: 90ms; }
.d-grid > *:nth-child(4) { animation-delay: 135ms; }
.d-grid > *:nth-child(5) { animation-delay: 180ms; }
.d-grid > *:nth-child(n+6) { animation-delay: 225ms; }

/* -------------------------------------------------------------------------
   8. KARTU KPI
   Tanpa border (border-color transparan), sparkline full-bleed menempel
   dasar kartu lewat margin negatif sebesar padding kartu.
   ------------------------------------------------------------------------- */
.d-kpi {
    position: relative;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    /* SEL, bukan kartu. Permukaannya milik bentang di atasnya; yang
       tertinggal di sini cuma isinya + satu garis pemisah.

       Pemisahnya box-shadow OUTSET, dan itu tetap dipertahankan meski
       bentangnya tidak lagi memotong. Outset menggambar garis DI LUAR kotak
       sel, jadi milik sel terkiri jatuh di luar bentang -- dulu dipotong
       `overflow: hidden`, sekarang cuma menggantung 1px di sana. Pada
       --hair (5% hitam) di atas latar gradien, satu piksel itu tidak
       terlihat; diperiksa berdampingan.

       Yang didapat dengan mempertahankannya jauh lebih besar daripada satu
       piksel itu: ia tetap benar saat sel MEMBUNGKUS ke baris berikutnya
       (Laporan memakai lima), karena sel pertama baris kedua juga berada di
       tepi kiri dan garisnya ikut jatuh ke luar. Versi inset + :first-child
       gagal di situ -- :first-child cuma mengenali anak pertama, bukan anak
       pertama TIAP baris, dan jumlah kolom di sini ditentukan auto-fit
       sehingga tidak bisa dihitung dari CSS.

       DUA ARAH: garis ATAS mengurus batas antar baris saat membungkus,
       dengan mekanisme yang sama -- milik baris pertama jatuh di luar. */
    background: none;
    border: 1px solid transparent;
    border-radius: 0;
    box-shadow: -1px 0 0 var(--hair), 0 -1px 0 var(--hair);
    padding: 20px 22px;
    text-decoration: none;
    color: inherit;
    transition:
        background .5s var(--d-ease-out),
        backdrop-filter .5s var(--d-ease-out),
        border-radius .36s var(--d-ease-out),
        transform .36s cubic-bezier(.34,1.2,.42,1),
        box-shadow .36s;
}

.d-kpi.d-kpi-spark { padding: 20px 22px 0; }

/* --d-kpi-warna di-set inline per kartu; bayangan hover ikut warnanya
   supaya tiap KPI terasa punya identitas sendiri, bukan drop shadow abu
   yang seragam. Tanpa outline, tanpa shade sudut (aturan handoff). */
/* DISENTUH, SEL TERANGKAT MENJADI KARTU.

   Sel dalam keadaan diam tidak punya permukaan sendiri -- ia cuma petak di
   dalam bentang. Saat disentuh ia MENGAMBIL permukaan: latar kaca, cincin,
   dan bayangan berwarna miliknya sendiri, lalu naik 4px dan memuai 3%.

   TAPI TIDAK MENGAMBIL SUDUT. Radius di sini berlaku untuk .d-kpi yang
   berdiri sendiri di luar bentang; begitu ia jadi sel bentang, radiusnya
   dinolkan lagi beberapa baris di bawah. Permintaan pemilik (6 Sep 2026):
   petak tengah harus tetap siku karena ia memang di tengah, dan petak ujung
   harus mempertahankan sisi mana yang membulat dan mana yang siku. Yang
   berubah saat disentuh cuma zoom-nya.

   z-index WAJIB. Sel berikutnya digambar sesudah sel ini, jadi tanpa
   dinaikkan, tepi kanan sel yang terangkat akan tertutup tetangganya --
   yang terlihat kartu terangkat yang terpotong lurus di satu sisi.

   Bayangannya memakai --d-kpi-warna, bukan abu netral: tiap angka di dasbor
   ini punya warnanya sendiri, dan bayangan berwarna itu yang membuat
   sentuhan terasa milik angka itu, bukan efek umum yang sama untuk semua. */
.d-kpi:hover {
    background: var(--card);
    backdrop-filter: var(--d-kaca-card);
    -webkit-backdrop-filter: var(--d-kaca-card);
    border-radius: var(--d-r-card);
    transform: translateY(-4px) scale(1.03);
    box-shadow: var(--d-kaca-cincin),
                0 30px 56px -26px color-mix(in srgb, var(--d-kpi-warna, var(--primary)) 55%, transparent);
    z-index: 3;
    text-decoration: none;
}
/* SEL BENTANG MEMPERTAHANKAN BENTUKNYA. Radius dinolkan supaya sudutnya
   ditentukan sepenuhnya oleh pemotongan di wadah (overflow: hidden pada
   bentangnya), bukan oleh selnya sendiri. Hasilnya: petak tengah siku di
   keempat sudut, petak ujung membulat hanya di sisi luarnya. */
.d-grid-kpi > .d-kpi:hover,
.d-grid-rasio > .d-kpi:hover { border-radius: 0; }

/* Sel rasio lebih rapat, jadi angkatnya lebih kecil -- naik 4px dan memuai
   3% pada petak setinggi 90px terasa melompat, bukan terangkat. */
.d-grid-rasio > .d-kpi:hover {
    transform: translateY(-3px) scale(1.02);
}
a.d-kpi { cursor: pointer; }

/* Sel yang sedang dipilih (mis. reseller aktif di tab Performa). Ditandai
   latar tint + garis aksen TEBAL di tepi kiri, bukan cincin mengelilingi:
   cincin mensyaratkan benda yang punya keempat sisinya sendiri, dan sel di
   dalam bentang tidak punya itu. */
.d-kpi-terpilih {
    background: color-mix(in srgb, var(--primary) 10%, transparent);
    box-shadow: inset 3px 0 0 var(--primary);
}
.d-kpi:hover .d-kpi-spark-wrap { opacity: 1; }

.d-kpi-label { font-size: 12.5px; font-weight: 600; color: var(--muted); line-height: 1.35; }
/* LABEL DIPATOK DUA BARIS DI DALAM BENTANG.

   "Total Omzet Setelah Potongan Marketplace" pecah dua baris sementara
   ketiga tetangganya satu baris, jadi angkanya mulai 17px lebih rendah --
   terukur di data sungguhan, tidak muncul di data contoh yang labelnya
   pendek.

   Waktu tiap KPI masih kartu terpisah, selisih itu nyaris tak terlihat:
   masing-masing punya batas sendiri dan mata tidak membandingkan garisnya.
   Di dalam satu bentang tanpa batas, empat angka yang tidak sebaris justru
   jadi hal pertama yang terlihat.

   min-height, bukan tinggi tetap: label tiga baris (mungkin di kolom sempit)
   tetap boleh tumbuh, dan yang satu baris tetap memesan ruang dua baris
   supaya angkanya sebaris dengan yang lain. 2.7em = 2 x line-height 1.35.

   Berlaku untuk KEDUA bentang. Pita rasio punya penyakit yang sama persis
   -- "ROAS (Omzet / Biaya Iklan)" dua baris di antara lima tetangga yang
   satu baris, jadi angkanya turun 17px sendirian. */
.d-grid-kpi > .d-kpi .d-kpi-label,
.d-grid-rasio > .d-kpi .d-kpi-label { min-height: 2.7em; }
.d-kpi-nilai {
    font-size: 26px;
    font-weight: 700;
    letter-spacing: -.03em;
    margin-top: 6px;
    color: var(--d-kpi-warna, var(--text));
}
.d-kpi-delta {
    display: flex;
    align-items: baseline;
    gap: 5px;
    font-size: 11.5px;
    margin-top: 5px;
    flex-wrap: wrap;
}
.d-kpi-delta b { font-weight: 700; }
.d-kpi-delta .d-naik  { color: var(--green); }
.d-kpi-delta .d-turun { color: var(--red); }
.d-kpi-delta em { font-style: normal; color: var(--faint); }
.d-kpi-sub { font-size: 12px; color: var(--faint); line-height: 1.45; margin-top: 6px; flex: 1 1 auto; }
.d-kpi-isi { flex: 1 1 auto; }
.d-kpi-spark-wrap {
    /* Margin negatifnya HARUS sebesar padding mendatar sel (22px), bukan
       angka bebas: sparkline ini full-bleed sampai tepi sel, dan selisih
       2px saja membuat garisnya berhenti menggantung sebelum pemisah. */
    margin: 14px -22px 0;
    padding-bottom: 2px;
    opacity: .85;
    transform-origin: bottom;
    transition: opacity .36s;
}
.d-kpi-spark-wrap svg { width: 100%; height: 42px; display: block; overflow: visible; }
/* Sparkline dulu selalu tampil samar (.85) dan menyala penuh saat kartunya
   disentuh. Di dalam bentang ia justru menjadi satu-satunya hal yang
   membedakan sel berisi tren dari sel biasa, jadi ia tampil penuh sejak
   awal -- yang samar di dalam permukaan bersama terbaca seperti gambar yang
   gagal dimuat, bukan seperti keterangan tambahan. */
.d-kpi-spark-wrap { opacity: 1; }
.d-spark-garis { stroke-dasharray: 2400; animation: d-dash 1.6s cubic-bezier(.2,.8,.3,1) backwards; }

/* Kartu rasio (baris "Rasio & Modal"): angkanya 20px, bukan 26px seperti
   KPI utama -- handoff memisahkan keduanya, dan pada kartu selebar 196px
   angka 26px seperti "Rp 198.611.295" pecah jadi dua baris. */
.d-kpi-rasio .d-kpi-nilai { font-size: 20px; letter-spacing: -.02em; }

/* Kartu rasio harus tetap PERSEGI PANJANG (lebih lebar daripada tinggi).
   Semua sel grid meregang setinggi sel tertinggi, jadi tinggi barisnya
   ditentukan oleh kartu terisi paling banyak -- yaitu kartu dua-nilai
   (Total HPP + Nilai Stok Gudang). Karena itu yang dipendekkan kartu
   ITU, bukan grid-nya dibuat align-items:start: kalau tiap kartu setinggi
   isinya sendiri, dasar barisnya jadi bergerigi. */
.d-kpi-rasio .d-kpi-sub { flex: 0 0 auto; }

.d-kpi-ganda .d-kpi-nilai { font-size: 17px; letter-spacing: -.015em; margin-top: 3px; }
.d-kpi-ganda .d-kpi-pisah { margin: 8px 0; }

/* Dua nilai dipisah garis rambut (dipakai Total HPP / Nilai Stok Gudang). */
.d-kpi-pisah { height: 1px; background: var(--hair); margin: 10px 0; }

/* -------------------------------------------------------------------------
   9. TOMBOL
   ------------------------------------------------------------------------- */
.d-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    border-radius: 12px;
    cursor: pointer;
    font-weight: 600;
    white-space: nowrap;
    padding: 11px 20px;
    font-size: 14px;
    text-decoration: none;
    transition: all .28s cubic-bezier(.34,1.3,.42,1);
}
.d-btn:hover { transform: translateY(-2px); text-decoration: none; }
.d-btn:disabled { cursor: default; transform: none; }
.d-btn-kecil { padding: 8px 14px; font-size: 13px; border-radius: 10px; }
/* Ikon di dalam tombol. Tanpa ukuran eksplisit sebuah <svg> tanpa atribut
   width/height jatuh ke 0x0 di flex container -- jadi SELURUH ikon tombol
   (Export, Rentang tanggal, dst) tidak pernah terlihat sama sekali. */
.d-btn svg, .d-btn-ikon {
    width: 16px;
    height: 16px;
    flex: 0 0 16px;
}
.d-btn-kecil svg { width: 15px; height: 15px; flex: 0 0 15px; }

.d-btn-primary {
    border: none;
    background: var(--primary);
    color: #fff;
    box-shadow: 0 10px 24px -12px color-mix(in srgb, var(--primary) 90%, transparent);
}
.d-btn-primary:hover { color: #fff; }
.d-btn-outline { border: 1px solid var(--border); background: var(--card-solid); color: var(--text); }
.d-btn-outline:hover { color: var(--text); }
.d-btn-ghost   { border: none; background: var(--field); color: var(--text); }
.d-btn-ghost:hover { color: var(--text); }

/* Tombol destruktif: teks merah, latar tint 12%, border 30%. Merah terang
   HANYA di sini & status buruk -- lihat aturan #8 handoff. */
.d-btn-bahaya {
    border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
    background: color-mix(in srgb, var(--red) 12%, transparent);
    color: var(--merah-teks);   /* bukan --red: di mode terang cuma 2,55:1 */
}
.d-btn-bahaya:hover { color: var(--merah-teks); }
/* Varian tenang: garis & teks tetap merah, tapi latarnya kartu -- bukan
   tint 12%. Dipakai tombol Logout penuh-lebar di Profil: blok merah
   selebar layar terbaca seperti peringatan, padahal logout itu tindakan
   biasa. Di tombol berukuran normal tint-nya tetap dipakai. */
.d-btn-bahaya.d-btn-tenang { background: var(--card-solid); }
.d-btn-bahaya.d-btn-tenang:hover { background: color-mix(in srgb, var(--red) 8%, var(--card-solid)); }

/* Pasangan HIJAU dari .d-btn-bahaya, pola yang sama persis (garis & teks
   berwarna di atas latar tint) -- untuk tindakan yang MENYELESAIKAN, bukan
   yang membatalkan. Dipakai berpasangan dengan .d-btn-bahaya supaya sepasang
   tombol terbaca sebagai "ya / tidak" cuma dari warnanya. */
.d-btn-oke {
    border: 1px solid color-mix(in srgb, var(--green) 32%, transparent);
    background: color-mix(in srgb, var(--green) 12%, transparent);
    color: var(--green);
}
.d-btn-oke:hover { color: var(--green); }

/* Pasangan KUNING dari .d-btn-bahaya/.d-btn-oke, pola yang sama persis.
   Untuk tindakan yang perlu MENONJOL tanpa berteriak bahaya: bukan merusak
   (jadi bukan merah), tapi juga bukan langkah biasa yang boleh ditekan
   tanpa pikir. Sejauh ini dipakai "Koreksi Angka" di sheet Detail
   Transaksi -- satu-satunya tombol di sana yang mengubah angka finansial.
   Warnanya dari token --warning (oranye), bukan kuning murni: kuning murni
   di atas latar terang nyaris tidak terbaca. */
.d-btn-kuning {
    border: 1px solid color-mix(in srgb, var(--warning) 32%, transparent);
    background: color-mix(in srgb, var(--warning) 14%, transparent);
    color: var(--warning);
}
.d-btn-kuning:hover { color: var(--warning); }

/* Tombol aksi yang isinya LAMBANG saja. Kolom Aksi di tabel sempit tidak muat
   dua tombol berlabel teks; lihat catatan di
   includes/belum_checkout_helper.php. Ukurannya dipatok (bukan lewat padding)
   supaya kedua tombol persis sama besar walau lambangnya beda bentuk. */
.d-btn.d-btn-aksi-ikon {
    width: 38px;
    height: 36px;
    padding: 0;
    gap: 0;
    flex: 0 0 auto;
    border-radius: 10px;
}
/* Garis lambang ditebalkan -- stroke bawaan d_svg() 1.7 terlalu tipis untuk
   dibaca sekilas sambil packing. Aturan CSS mengalahkan atribut stroke-width
   di <svg>, jadi d_svg() tidak perlu diubah & ikon lain tidak ikut menebal. */
.d-btn.d-btn-aksi-ikon svg {
    width: 20px;
    height: 20px;
    flex: 0 0 20px;
    stroke-width: 3;
}

/* Tombol simpan: label -> "Menyimpan..." + spinner -> "Tersimpan" hijau.
   Statusnya ditaruh di data-status oleh assets/app.js. */
.d-save[data-status="jalan"] { opacity: .75; cursor: default; }
.d-save[data-status="ok"]    { background: var(--green); }
.d-save-spinner {
    width: 13px;
    height: 13px;
    border-radius: 50%;
    border: 2px solid rgba(255,255,255,.45);
    border-top-color: #fff;
    animation: d-spin .7s linear infinite;
    flex: 0 0 13px;
}

/* -------------------------------------------------------------------------
   10. PILL, SEGMENTED, CHIP
   ------------------------------------------------------------------------- */
.d-pills { display: flex; flex-wrap: wrap; gap: 8px; }
.d-pill {
    padding: 9px 16px;
    border-radius: 999px;
    font-size: 13.5px;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    flex: 0 0 auto;
    line-height: 1.2;
    border: 1px solid var(--border);
    background: var(--card-solid);
    color: var(--muted);
    text-decoration: none;
    transition: all .32s cubic-bezier(.34,1.3,.42,1);
}
.d-pill:hover { color: var(--text); text-decoration: none; }
.d-pill.aktif {
    border-color: transparent;
    background: var(--primary);
    color: #fff;
    box-shadow: 0 8px 20px -8px color-mix(in srgb, var(--primary) 70%, transparent);
    transform: translateY(-1px);
}

/* JEBAKAN #3: grid, bukan flex. Kolom harus sama lebar atau thumb-nya
   tidak lurus dengan tombolnya. --d-seg-n = jumlah item, di-set inline. */
/* Kepala kartu Riwayat Transaksi: tab status di kiri, kotak cari mengisi
   sisa ruang di kanan. Sebelumnya keduanya bar filter tersendiri di atas
   kartu, yang memakan satu baris penuh sebelum tabelnya mulai. */
.d-trx-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 16px;
}
.d-trx-bar .d-seg { flex: 0 0 auto; }
.d-trx-bar .d-search { flex: 1; min-width: 0; }
.d-trx-status { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* Kolom "Omzet bersih" di halaman Transaksi memajang omzet SETELAH potongan
   marketplace, dan angka itu punya dua asal-usul yang jauh berbeda mutunya:
   Income Dilepas sungguhan dari file Rekonsiliasi Profit Riil, atau taksiran
   rumus. Yang riil ditandai hijau + titik supaya bisa dipindai satu kolom,
   tanpa membuka sheet Detail satu per satu.

   Titiknya sengaja ada di samping warnanya: warna sendirian tidak terbaca oleh
   mata yang sulit membedakan merah-hijau, dan di kolom angka rata-kanan
   perbedaan warna saja gampang terlewat. */
.d-omzet-riil { color: var(--hijau-teks); }
.d-omzet-riil::after {
    content: '';
    display: inline-block;
    width: 5px;
    height: 5px;
    margin-left: 5px;
    border-radius: 50%;
    background: currentColor;
    vertical-align: middle;
    /* Titiknya ikut turun sedikit supaya duduk di tengah tinggi angka, bukan
       di garis dasar huruf. */
    transform: translateY(-1px);
}
/* Angka yang sudah ditarik Otomasi Shopee tapi dananya BELUM dilepas.
   Titiknya PENUH, sama bentuknya dengan yang hijau -- keduanya dibedakan
   warna saja (permintaan pemilik, 6 Sep 2026). Versi pertama memakai cincin
   supaya tetap terbaca oleh mata yang sulit membedakan warna; kalau suatu
   hari itu jadi masalah, di sinilah tempat mengembalikannya. */
.d-omzet-estimasi { color: var(--kuning-teks); }
.d-omzet-estimasi::after {
    content: '';
    display: inline-block;
    width: 5px;
    height: 5px;
    margin-left: 5px;
    border-radius: 50%;
    background: currentColor;
    vertical-align: middle;
    transform: translateY(-1px);
}

/* Keterangan kecil di kaki kartu Riwayat -- tanpa ini titik hijaunya cuma
   jadi teka-teki warna. */
.d-trx-legenda {
    display: flex;
    align-items: center;
    gap: 7px;
    margin-top: 12px;
    font-size: 12px;
    color: var(--muted);
}
.d-trx-legenda i {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--hijau-teks);
    flex: 0 0 auto;
}
/* Contoh titik kuning di legenda -- bentuknya harus SAMA dengan yang di
   kolom, kalau tidak legendanya justru menyesatkan. */
.d-trx-legenda i.kuning { background: var(--kuning-teks); }

/* Kartu kepala Laporan: pintasan periode di kiri, pemilih tab menempel di
   kanan. Keduanya menyaring isi yang sama, jadi duduk di satu baris. */
.d-lap-bar {
    /* Menempel saat digulir, sama seperti bar filter di Dasbor: pilihan
       periode & pemilih tab dipakai bolak-balik sambil membaca tabel di
       bawahnya, jadi harus tetap terjangkau tanpa menggulir balik ke atas. */
    position: sticky;
    top: 0;
    z-index: 12;
    /* Resep BAR MENEMPEL, sama dengan .d-filter di Dasbor: isi tabel lewat
       di baliknya saat digulir, jadi butuh kepekatan + blur sekaligus supaya
       angkanya tidak terbaca menembus bar. */
    background: var(--d-lengket-latar);
    backdrop-filter: var(--d-lengket-bias);
    -webkit-backdrop-filter: var(--d-lengket-bias);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    /* Padding kartu filter memang lebih rapat dari kartu isi (24px) --
       prototipe handoff menimpanya jadi 18px 22px untuk kartu ini. */
    padding: 18px 22px;
    /* margin-bottom WAJIB: .d-card tidak punya margin sendiri, dan panel tab
       di bawahnya bermargin 0, jadi tanpa ini kedua kartu benar-benar
       bersentuhan (terukur gap 0). Handoff: jarak antar kartu 14px. */
    margin: 14px 0;
}
.d-lap-bar .d-seg { flex: 0 0 auto; }
/* Isyarat "sedang mengambil data" saat periode diganti tanpa muat ulang --
   tanpa kedip halaman, tidak ada tanda lain bahwa sesuatu sedang berjalan. */
.d-lap-sibuk { opacity: .55; pointer-events: none; transition: opacity .2s; }

/* SKU panjang di baris kedua No. Resi (mis. PORT96-P1-KANGSA-SILVER2-ABU)
   dulu menaikkan min-content kolom kiri sampai 790px, sehingga kartu
   Ringkasan di kanan cuma kebagian sisanya dan judulnya patah dua baris.
   Boleh dipenggal di mana saja; lebar kolomnya sendiri diatur lewat minmax
   di transaksi.php, jangan pakai min-width:0 di selnya -- kolom ber-fr
   (Jam, Penerima) akan runtuh sampai nol. */
.d-grid-trx .d-table-row .d-sub { overflow-wrap: anywhere; }
/* minmax(0,...) supaya perbandingan 2:1 benar-benar dipakai. Dengan `2fr 1fr`
   polos, lantai bawaan fr adalah min-content: kolom kiri mengembang ikut isi
   tabel (829px terukur) dan kartu Ringkasan kebagian sisanya -- judulnya
   patah dua baris. Lebar minimum tiap kolom tabel sudah dijaga lewat minmax
   di transaksi.php, jadi kolom kiri tetap tidak bisa gepeng. */
.d-grid-trx { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }

/* Bar kepala halaman Stok: kotak cari memanjang di kiri, pemilih tab
   menempel di kanan. Satu baris untuk dua kontrol yang dulu memakan dua. */
.d-stok-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 14px;
    /* PERMUKAAN, sama seperti .d-filter di Dasbor.

       Dulu bar ini tidak punya latar sama sekali: kotak cari dan strip tab
       berdiri langsung di atas halaman. Itu masih terbaca waktu isi di
       bawahnya juga kartu-kartu terpisah. Begitu isi halaman menjadi
       permukaan besar yang rapi, bar tanpa permukaan terbaca sebagai dua
       kontrol yang kebetulan diletakkan berdampingan, bukan sebagai satu
       bilah kendali -- dan Stok jadi satu-satunya halaman yang kepalanya
       berbeda dari Dasbor tanpa alasan. */
    padding: 12px 14px;
    border-radius: 22px;
    background: var(--card);
    backdrop-filter: var(--d-kaca-bar);
    -webkit-backdrop-filter: var(--d-kaca-bar);
    box-shadow: var(--d-shadow-card);
}
/* Tempat aslinya: anak pertama kartu tabel. Di dalam kartu, permukaannya
   DICABUT lagi -- kartu sudah jadi permukaannya, dan bar berpermukaan di
   dalam kartu berpermukaan adalah kaca di atas kaca tanpa alasan. */
.d-card > .d-stok-bar {
    margin: 0 0 16px;
    padding: 0;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    box-shadow: none;
    border-radius: 0;
}
/* Kartu tabel bisa menggulir mendatar; tanpa ini barnya ikut hanyut
   ke kiri saat tabel lebar digulir. */
.d-card-scroll > .d-stok-bar { position: sticky; left: 0; }
/* Penyaring berupa <form> di dalam bar -- dipakai halaman Barang Keluar,
   yang kotak carinya mengirim GET sungguhan (bukan penyaring JS seperti di
   Stok) sehingga kotak, tombol rentang tanggal, dan Reset harus terbungkus
   satu <form>. Tanpa aturan ini form itu jadi satu anak flex setinggi isinya
   yang tidak mau memanjang, dan pil tab terdorong menempel ke kotak cari. */
.d-stok-bar > form {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 1;
    min-width: 0;
}
.d-stok-bar .d-search { flex: 1; min-width: 0; }
.d-stok-bar .d-seg { flex: 0 0 auto; }
.d-stok-bar input:disabled { cursor: not-allowed; }

/* Bar sub-tab halaman Rincian Reseller: kapsul 3 tab + kotak cari.
   Bedanya dengan .d-stok-bar -- di sana kotak carinya `flex:1` sampai mentok
   ke kanan karena ia satu-satunya isi kartu; di sini kapsul yang jadi tokoh
   utamanya, jadi kotaknya dipatok ~300px dan boleh menyusut, tidak memanjang.
   flex-wrap: di layar sempit kotaknya turun ke baris kedua, bukan menghimpit
   kapsul sampai labelnya terpotong. */
.d-subtoko-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 16px;
}
.d-subtoko-bar .d-seg { flex: 0 0 auto; }
.d-subtoko-bar .d-search { flex: 0 1 300px; min-width: 180px; }
/* Di HP kapsul 3 tab sudah memakan selebar layar, jadi kotak carinya turun ke
   baris sendiri (flex-wrap di atas). Di baris itu ia MEMENUHI lebar -- kotak
   300px yang menyisakan ruang kosong di kanannya terlihat seperti belum jadi. */
@media (max-width: 768px) {
    .d-subtoko-bar .d-search { flex: 1 1 100%; }
}

.d-seg {
    position: relative;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    padding: 3px;
    /* Alurnya ikut membulat penuh karena thumb-nya membulat penuh. Kapsul
       di dalam kotak-bersudut menyisakan celah berbentuk bulan sabit di
       keempat pojoknya -- terbaca sebagai dua bentuk yang tidak sengaja
       dipasangkan, bukan satu kendali. */
    border-radius: 999px;
    background: var(--field);
    width: max-content;
    max-width: 100%;
}
/* Penanda tab aktif. Materialnya Liquid Glass -- lihat catatan panjang di
   --d-seg-kaca (blok token di atas) untuk dari mana bentuknya diambil.

   Pembiasnya menyampel ulang tiap frame selagi pil ini meluncur, jadi
   perpindahannya sendiri yang menghidupkan kacanya: latar di belakangnya
   ikut bergeser di dalam lensa.

   TIMINGNYA MILIK SIDEBAR, DISALIN APA ADANYA: .46s var(--d-ease-rail),
   sama dengan .d-rail-sorot. Permintaan pemilik 6 Sep 2026 -- "buat
   animasinya ketika bubble nya berpindah itu sangat sangat persis dengan
   yang ada di side bar".

   Riwayat singkatnya, supaya tidak ada yang mengembalikannya tanpa sadar:
   versi WMS memakai pegas cubic-bezier(.34,1.35,.42,1) yang menembak jauh
   melewati tujuannya -- terukur, pada 190ms dari 420ms pil sudah di 222px
   sementara tujuannya 217px, jadi seluruh perjalanan habis di sepertiga
   pertama dan sisanya cuma pantulan di tempat. Lalu sempat .55s dengan
   kurva melambat panjang, lalu .42s var(--d-ease-out) supaya serentak
   dengan animasi masuk baris tabel.

   Yang terakhir itu satu-satunya yang dikorbankan sekarang: baris tabel
   masuk dalam .42s var(--d-ease-out), pil berpindah dalam .46s
   var(--d-ease-rail). Selisihnya 40ms dan kurvanya beda tipis -- harga
   yang dibayar supaya pil benar-benar bergerak seperti sorotan sidebar,
   bukan seperti tiruannya.

   TETAP DI BAWAH LABELNYA (.d-seg-item punya z-index 1). Menaruhnya di atas
   memang akan membuat teks tab aktif ikut terbias -- persis seperti bar biru
   yang membelok di dalam lensa pada rekaman rujukan -- tapi yang terbias itu
   satu-satunya kata yang harus paling terbaca di kendali ini. Segmented
   control Apple sendiri menaruh thumb-nya di belakang label. */
.d-seg-thumb {
    position: absolute;
    top: 3px;
    bottom: 3px;
    left: 3px;
    width: calc((100% - 6px) / var(--d-seg-n, 2));
    /* 999px, sama dengan .d-rail-item / .d-rail-sorot -- permintaan pemilik
       6 Sep 2026 agar bentuknya persis sorotan sidebar. */
    border-radius: 999px;
    pointer-events: none;
    /* .46s var(--d-ease-rail) -- ANGKA MILIK SOROTAN SIDEBAR, disalin apa
       adanya. Lihat .d-rail-sorot. */
    transition: transform .46s var(--d-ease-rail);
}
/* KACANYA DI ::before, BUKAN DI PILNYA SENDIRI -- susunan yang sama persis
   dengan .d-rail-sorot, dan alasannya juga sama.

   Pil memegang POSISI (transform: translateX), lapisan ini memegang RUPA
   dan lentingannya. Harus dipisah karena `scale` dan `transform` tidak
   berdiri sendiri-sendiri: peramban menerapkannya sebagai translate *
   rotate * scale * transform, sehingga scale ikut MENGALIKAN translate. Pil
   di offset 300px yang dilentingkan 1.09 akan melompat 27px ke samping di
   tengah perjalanannya. Dengan lentingan di ::before -- kotak sendiri,
   titik pusat sendiri, tanpa translate apa pun -- keduanya tidak pernah
   bertemu. */
.d-seg-thumb::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: var(--d-seg-kaca);
    backdrop-filter: var(--d-seg-bias);
    -webkit-backdrop-filter: var(--d-seg-bias);
    box-shadow: var(--d-seg-rim);
}
/* PENGURAIAN WARNA DI RIM BAWAH: DIBUANG.

   Dulu ada .d-seg-thumb::after berisi gradien jingga-hijau-biru bermask,
   di-blend `screen` di pita 22% paling bawah -- meniru tepi tebal lensa yang
   memecah cahaya pada rekaman WWDC. Di rujukannya efek itu setipis rambut.
   Di pil setinggi 35px ia tidak pernah setipis itu, dan yang terbaca orang
   adalah "ada bayangan hijau dan merah di bawah bubble" -- noda warna, bukan
   cahaya yang terurai. Dibuang atas laporan pemilik (6 Sep 2026). */

/* LENTINGAN SAAT BERPINDAH -- kembaran d-sorot-lentur milik sidebar.

   Angka-angkanya sama, SUMBUNYA yang ditukar, dan itu justru yang membuatnya
   terbaca "persis". Sidebar berjalan tegak: ia memanjang di Y (1.09) sambil
   menyempit di X (.94) -- memanjang SEARAH JALANNYA. Pil segmented berjalan
   mendatar, jadi arah yang sama berarti 1.09 di X dan .94 di Y. Menyalin
   angkanya mentah-mentah tanpa menukar sumbu akan menghasilkan pil yang
   memipih tegak lurus terhadap arah geraknya -- kebalikan dari benda yang
   ditarik, dan tidak akan pernah terlihat seperti sidebar.

   Pantulan baliknya (.97/1.03 di 70%) lebih kecil daripada tarikan awalnya,
   sama seperti di sidebar: itu yang membuat berhentinya terasa mengendap,
   bukan mengunci. */
@keyframes d-seg-lentur {
    0%   { transform: scale(1, 1); }
    38%  { transform: scale(1.09, .94); }
    70%  { transform: scale(.97, 1.03); }
    100% { transform: scale(1, 1); }
}
/* Cuma ::before yang perlu dilentingkan sekarang -- ::after sudah tidak ada
   lagi sejak semburat pelanginya dibuang. */
.d-seg-thumb.melenting::before { animation: d-seg-lentur .46s var(--d-ease-rail); }

@media (prefers-reduced-motion: reduce) {
    .d-seg-thumb.melenting::before { animation: none; }
}

.d-seg-item {
    position: relative;
    z-index: 1;
    padding: 9px 14px;
    border: none;
    background: none;
    cursor: pointer;
    font-size: 13.5px;
    font-weight: 600;
    white-space: nowrap;
    line-height: 1.3;
    text-align: center;
    color: var(--muted);
    text-decoration: none;
    transition: color .3s;
}
.d-seg-item:hover { text-decoration: none; color: var(--text); }
/* Ditulis SESUDAH :hover dengan sengaja. Kekhususan keduanya sama, jadi
   urutan inilah yang membuat tab aktif tetap memakai warna penuhnya saat
   kursor melewatinya, bukan ikut berubah seperti tab biasa.

   Tebal hurufnya TIDAK ikut berubah, dan itu bukan kelalaian: huruf yang
   menebal melebarkan tabnya, tab yang melebar menggeser tetangganya, dan
   thumb yang lebarnya dihitung dari kolom grid berhenti tidak lagi pas. */
.d-seg-item.aktif { color: var(--text); }

/* Panel tab yang ditukar di sisi klien. Nama classnya SENGAJA tetap
   .tab-panel (tanpa awalan d-): halaman Stok sudah memakainya di markup dan
   di JS gantiTab(), dan menamainya ulang berarti menyentuh keduanya tanpa
   alasan. Gayanya sendiri sebelumnya ada di style.css lama -- tanpa aturan
   di bawah, SEMUA panel tampil bertumpuk sekaligus. */
.tab-panel { display: none; }

/* Segmented penukar panel -- d_segmen() di ui.php, penangan klik di app.js.
   Dipakai Stok, Permintaan, Surat Jalan. */
.d-segmen { margin-bottom: 16px; }
.d-segmen-panel { display: none; }
.d-segmen-panel.aktif { display: block; }

/* Tumpukan kartu berjarak, seperti pembungkus isi layar di prototipe.
   Dipakai panel tab halaman Scan, yang menaruh kartunya sebagai saudara
   langsung (bukan di dalam .d-grid).
   Kedua aturan .tab-panel di bawah ditulis dengan DUA class supaya menang
   atas .d-tumpuk polos -- kalau tidak, panel yang sedang tidak aktif ikut
   tampil begitu .d-tumpuk kebetulan berada di urutan CSS setelahnya. */
.d-tumpuk { display: flex; flex-direction: column; gap: 14px; }
.tab-panel.d-tumpuk:not(.active) { display: none; }
.tab-panel.d-tumpuk.active { display: flex; }
.tab-panel.active { display: block; animation: d-fadeUp .35s var(--d-ease-out) backwards; }

.d-chips { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.d-chip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 8px 13px;
    border-radius: 999px;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    flex: 0 0 auto;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--muted);
    text-decoration: none;
    transition: all .3s cubic-bezier(.34,1.3,.42,1);
}
.d-chip:hover { text-decoration: none; }
.d-chip.aktif {
    border-color: transparent;
    background: color-mix(in srgb, var(--primary) 13%, transparent);
    color: var(--primary);
}
.d-chip-tanda { font-size: 13px; opacity: .75; }
.d-chip:not(.aktif) .d-chip-tanda { font-size: 14px; opacity: .6; }
.d-chips-ket { font-size: 12px; color: var(--faint); margin-left: 2px; }

/* -------------------------------------------------------------------------
   11. BAR FILTER STICKY
   z-index 12 + blur 40px. Menempel di atas viewport konten saat digulir.
   ------------------------------------------------------------------------- */
.d-filter {
    position: sticky;
    top: 0;
    z-index: 12;
    /* 14px, sama dengan jarak antar kartu di handoff. Dulu 4px, jadi bar
       filter terlihat menempel ke kartu pertama di bawahnya. */
    margin-bottom: 14px;
    padding: 14px 18px;
    border-radius: 22px;
    /* --d-lengket-*, BUKAN --card + --d-kaca-bar. Bar ini menempel di puncak
       layar dan seluruh isi halaman lewat di baliknya; dengan --card di .46
       dan tanpa blur, angka KPI serta garis sparkline terbaca menumpuk
       dengan pil-pilnya. Lihat catatan di --d-lengket-latar. */
    background: var(--d-lengket-latar);
    backdrop-filter: var(--d-lengket-bias);
    -webkit-backdrop-filter: var(--d-lengket-bias);
    box-shadow: var(--d-shadow-card);
}
.d-filter-baris { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
/* Sepasang tombol di ujung baris filter (Filter + Reset di Arus Kas). */
.d-filter-aksi { display: flex; align-items: center; gap: 9px; flex: 0 0 auto; }
.d-filter-pisah { height: 1px; background: var(--hair); margin: 12px 0; }

/* Bagian yang sedang diambil ulang lewat Diamondo.ajaxGanti(). Sengaja
   cuma diredupkan, BUKAN diganti spinner: isinya masih angka yang sah
   (periode sebelumnya), jadi lebih baik tetap terbaca sepersekian detik
   daripada berkedip jadi kosong. */
.d-memuat { opacity: .45; pointer-events: none; transition: opacity .15s; }

/* -------------------------------------------------------------------------
   12. TABEL
   Grid, bukan <table>: kolomnya perlu fr/minmax dan tiap baris perlu jadi
   kartu sendiri dengan radius + hover geser. --d-kolom di-set inline.
   ------------------------------------------------------------------------- */
.d-table-head,
.d-table-row {
    display: grid;
    grid-template-columns: var(--d-kolom);
    gap: 14px;
    align-items: center;
}
/* min-width:0 DI SEMUA LEBAR, bukan cuma di HP.

   Bawaan grid untuk jalur fr adalah `minmax(auto, 1fr)`, dan `auto` di sana
   berarti "sekurang-kurangnya selebar min-content isinya". Jadi selama
   anaknya belum diberi min-width:0, jalur fr TIDAK MAU menyusut: barisnya
   melar melewati kartunya dan menyembul keluar. Aturan ini dulu cuma ada di
   bagian HP; akibatnya tabel SKU Belum Terdaftar menuntut 847px di kartu
   selebar 730px pada viewport 900px -- dan karena kartunya `overflow
   visible`, luberannya tidak digulir melainkan DIPOTONG DIAM-DIAM oleh
   .d-viewport. Tidak terlihat, tidak terjangkau.

   Di layar lapang aturan ini tidak mengubah apa pun: jalur fr baru menyusut
   kalau memang kekurangan tempat. */
.d-table-head > *,
.d-table-row > * { min-width: 0; }
.d-table-head {
    padding: 0 14px 10px;
    font-size: 11.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--faint);
}
/* Judul kolom yang bisa diklik untuk mengurutkan. Panahnya muncul hanya
   pada kolom yang sedang aktif -- panah pucat di semua kolom sekaligus
   membuat header jadi ramai tanpa menambah informasi. */
.d-th-sort { cursor: pointer; user-select: none; transition: color .2s; }
.d-th-sort:hover { color: var(--text); }
.d-th-sort.sort-asc::after  { content: " \2191"; color: var(--primary); }
.d-th-sort.sort-desc::after { content: " \2193"; color: var(--primary); }

/* Bar paginasi. Markup-nya dibuat buatPaginasi() di assets/table-utils.js
   yang dipakai bersama halaman UI lama, jadi class-nya masih .btn/.small/
   .pagination-info. Ditema ulang di sini secara terkurung dalam
   .d-paginasi, supaya table-utils.js tidak perlu dicabang dua. */
.d-paginasi {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    margin-top: 16px;
}
.d-paginasi:empty { display: none; }
.d-paginasi .pagination-info { font-size: 12.5px; color: var(--muted); }
.d-paginasi .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 34px;
    padding: 7px 12px;
    border-radius: 10px;
    border: 1px solid var(--border);
    background: var(--card-solid);
    color: var(--text);
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: border-color .2s, opacity .2s, transform .28s cubic-bezier(.34,1.3,.42,1);
}
/* Angkat 2px seperti semua tombol lain (handoff). Dulu cuma border yang
   berubah, jadi tombol paginasi terasa mati dibanding tombol di sekitarnya. */
.d-paginasi .btn:hover:not(:disabled) { border-color: var(--primary); transform: translateY(-2px); }
.d-paginasi .btn:disabled { opacity: .4; cursor: default; }

/* ---- Varian untuk <table> sungguhan ----
   d-table (grid) dipakai tabel yang barisnya dirender PHP dan tidak
   disentuh JS. Sebagian halaman lama punya tabel yang barisnya DIBANGUN
   ULANG oleh JS setelah aksi AJAX (mis. Daftar Produk: satu <tr> disusun
   ulang sel demi sel setelah Edit tersimpan). Mengubah tabel itu jadi
   <div> berarti ikut menulis ulang JS-nya -- pekerjaan berisiko yang tidak
   menghasilkan perbedaan apa pun di layar. Jadi tabel semacam itu tetap
   <table>, dan aturan di bawah membuatnya tampil sama dengan .d-table. */
.d-tabel {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0 4px;
    font-size: 13.5px;
}
.d-tabel .rt-head th,
.d-tabel thead th {
    padding: 0 14px 10px;
    font-size: 11.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--faint);
    text-align: left;
    /* Judul kolom ikut boleh membungkus, dengan alasan yang sama seperti
       <td> di atas. "Harga Jual" dan "Jumlah Masuk" adalah dua kata, dan
       selama ia dipaksa satu baris ia memasang lantai lebar yang tidak
       bisa ditawar untuk seluruh kolomnya. */
    white-space: normal;
    overflow-wrap: break-word;
}
/* Judul kolom ikut perataan selnya. Tanpa ini `text-align:left` di atas
   (spesifisitas 0,2,1) mengalahkan .d-kanan (0,1,0), sehingga SETIAP judul
   kolom angka berdiri di kiri sementara angkanya rata kanan -- matanya
   kehilangan garis bantu dan kolomnya jadi sulit dibaca. */
.d-tabel .rt-head th.d-kanan,
.d-tabel thead th.d-kanan { text-align: right; }
.d-tabel .rt-head th.d-tengah,
.d-tabel thead th.d-tengah { text-align: center; }
.d-tabel td {
    padding: var(--d-pad-row);
    /* BARIS BUKAN KARTU KECIL.

       Sebelumnya tiap baris punya latar sendiri, garis atas-bawah, dan
       sudut membulat di kedua ujungnya -- yaitu sebuah kartu, di dalam
       kartu. Tabel berisi 30 baris jadi 30 kotak bertumpuk di dalam satu
       kotak besar, dan itu penyumbang terbesar kesan "dasbor ini cuma
       tumpukan kartu".

       Sekarang barisnya TELANJANG: kaca kartunya sendiri yang jadi
       latarnya, dan yang memisahkan cuma satu garis rambut di bawah. Yang
       hilang bukan keterbacaannya -- kolom tetap lurus, pemisah tetap ada
       -- melainkan satu tingkat kotak yang memang tidak pernah dibutuhkan. */
    background: none;
    border-top: 0;
    border-bottom: 1px solid var(--hair);
    vertical-align: middle;
    /* SEL TEKS BOLEH MEMBUNGKUS, SEL ANGKA TIDAK -- dan ini membalik
       keputusan lama.

       Dulu SEMUA <td> di-`nowrap` dengan alasan "angka yang terbelah lebih
       sulit dibaca daripada tabel yang harus digulir sedikit", dan tabel
       yang kelebaran diserahkan ke .d-card-scroll. Pemilik membatalkan
       pertukaran itu 8 Sep 2026: "buat agar tidak ada yang scroll samping
       semua ya, jadi tidak ganggu saat mengedit data di tabelnya."

       Alasannya masuk akal begitu dilihat: kolom Aksi selalu yang paling
       kanan, jadi kolom yang tergulir keluar layar justru kolom yang
       dipakai bekerja. Terukur di Data Reseller @1024px -- tabel menuntut
       1.001px di kartu selebar 854px, dan tombol Nonaktifkan tidak
       terlihat sama sekali sampai kartunya digeser.

       Yang membungkus cuma sel TEKS. Sel angka tetap `nowrap` lewat
       daftar pengecualian di bawah, jadi "Rp 611.500" tetap utuh -- yang
       dikorbankan cuma nama toko/produk yang kini boleh turun ke baris
       kedua. Tinggi baris bertambah, lebarnya tidak pernah.

       break-word, BUKAN anywhere: teks pecah di SPASI lebih dulu, dan cuma
       memenggal di tengah kata kalau satu kata sendirian memang lebih
       lebar dari kolomnya. */
    white-space: normal;
    overflow-wrap: break-word;
}
/* Kolom yang isinya angka atau tombol -- tidak boleh pecah di mana pun.
   .d-num dan .d-kanan menandai kolom angka (47 + 101 pemakaian di halaman),
   .d-keep tombol aksi, .d-check kotak centang. Kalau suatu saat ada kolom
   angka BARU yang lupa diberi salah satu kelas ini, gejalanya angka pecah
   dua baris di layar sempit -- bukan tabel yang menggulir lagi. */
.d-tabel td.d-num,
.d-tabel td.d-kanan,
.d-tabel td.d-tengah,
.d-tabel td.d-keep,
.d-tabel td.d-check { white-space: nowrap; }
/* Dipertahankan walau kini sama dengan bawaan: 20 halaman masih menulisnya,
   dan ia menyatakan maksud ("kolom ini memang kalimat") pada pembacanya. */
.d-tabel td.d-bungkus { white-space: normal; }

/* Baris pemisah di dalam tabel: bukan data, melainkan judul kelompok
   (dipakai sheet Rincian Batch Stok untuk memisahkan batch yang masih ada
   isinya dari riwayat yang sudah habis). Sengaja di-scope ke class sendiri
   supaya tidak menumpang aturan <td> biasa -- latar kartu, border, dan
   radius sudut dibatalkan semua, jadi ia terbaca sebagai label, bukan baris
   tabel yang kebetulan kosong. */
.d-tabel tr.batch-pemisah td {
    background: none;
    border: 0;
    border-radius: 0;
    padding: 18px 7px 6px;
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--faint);
}
/* JUDUL kolomnya juga. Aturan `white-space: nowrap` di atas berlaku pada
   .d-tabel .rt-head th / thead th (spesifisitas 0,2,1), jadi menempelkan
   .d-bungkus di <th> saja dulu tidak berpengaruh apa-apa -- judul dua kata
   seperti "Stok Sebelum" atau "Jumlah Masuk" tetap memaksa satu baris dan
   ikut mendorong tabel melewati lebar kartu. Selektornya disamakan panjang
   supaya menang, dan break-word (bukan anywhere) supaya judul pecah di
   SPASI lebih dulu, tidak di tengah kata. */
.d-tabel .rt-head th.d-bungkus,
.d-tabel thead th.d-bungkus { white-space: normal; overflow-wrap: break-word; }

/* =========================================================================
   BAND TENGAH (<=1100px): TIDAK ADA TABEL YANG MENGGULIR MENYAMPING
   =========================================================================

   Permintaan pemilik 8 Sep 2026: "untuk tabel-tabel yang ada di seluruh
   halaman ini, tolong buat agar tidak ada yang scroll samping semua ya,
   jadi tidak ganggu saat mengedit data di tabelnya."

   KENAPA JUSTRU DI BAND INI, DAN CUMA DI SINI
   -------------------------------------------
   .d-page dibatasi max-width 1180px, jadi di layar selebar apa pun kolom
   isinya berhenti di 1.100px -- dan di lebar itu semua tabel muat. Di
   <=768px kolom yang tidak krusial sudah disembunyikan sejak 15 Agu 2026,
   jadi di HP pun muat. Yang tidak pernah diurus siapa pun adalah lebar di
   ANTARA keduanya: kolomnya masih lengkap seperti desktop, tempatnya sudah
   sesempit tablet. Terukur 8 Sep 2026 di lima halaman:

       Data Reseller  @1024  kartu 854px, tabel 1.001px  (+147)
       Stok · Batch   @900   kartu 582px, tabel   769px  (+187)
       Stok · Paket   @900   kartu 730px, tabel   837px  (+107)
       Laporan        @800   kartu 630px, tabel   741px  (+111)
       Biaya/Keuangan @800   kartu 582px, tabel   645px   (+63)

   Jendela yang tidak dilebarkan penuh mendarat persis di sini, dan begitu
   juga layar 1536px yang diperbesar 133% -- yang terakhir ini kemungkinan
   besar yang dilihat pemilik.

   YANG DIPAKAI DI SINI SET KOLOM YANG SAMA DENGAN HP, DAN ITU DISENGAJA.
   Set itu sudah terbukti muat di 390px; dipakai di ruang 769-1100px ia
   pasti muat juga -- kolomnya sama, tempatnya jauh lebih lega. Jadi tidak
   ada keputusan baru "kolom mana yang boleh hilang" yang perlu diambil dan
   dijaga sinkron; yang sudah dikurasi 15 Agu dipakai ulang apa adanya.

   SATU AMBANG SAJA, 1100px. Bukan dua ambang yang saling melengkapi --
   berkas ini sudah pernah kena jebakan itu (lihat .scan-konsol): dua ambang
   berbeda menyisakan celah beberapa piksel yang cuma muncul di satu lebar
   tertentu, dan bug seperti itu tidak akan pernah dilaporkan siapa pun
   dengan benar. Terukur: dengan ambang 1023px, tabel Transaksi masih
   menyembul 3px di jendela 1024-1049px. Dinaikkan ke 1100px, celahnya
   tertutup.

   Diverifikasi 19 halaman x 11 lebar (390-1280px), nol luberan -- baik yang
   digulir maupun yang dipotong diam-diam.
   ========================================================================= */
@media (max-width: 1100px) {
    .d-tabel .hide-mobile { display: none; }
    .d-table-head > .d-hp-off,
    .d-table-row > .d-hp-off { display: none; }
    /* Kolom yang disembunyikan harus ikut hilang dari daftar jalur grid.
       Kalau cuma sel-nya yang display:none, grid tetap memesan jalurnya dan
       kepala tabel meleset dari isinya. --d-kolom-hp disiapkan d_table(). */
    .d-table-head,
    .d-table-row { grid-template-columns: var(--d-kolom-hp, var(--d-kolom)); }
}

/* JARING PENGAMAN, bukan lagi cara kerja utama. Sesudah aturan di atas tidak
   ada tabel yang perlu digulir; ini disisakan supaya tabel BARU yang kelak
   kelebaran tetap bisa dijangkau (digulir) alih-alih terpotong diam-diam.
   Kalau batang gulir ini sampai muncul, itu tanda ada tabel baru yang belum
   ditandai .hide-mobile / 'hp' => false -- bukan tanda aturan ini bekerja
   sebagaimana mestinya. */
.d-card-scroll { overflow-x: auto; }

/* Kolom Aksi di Kelola Tenant: dua tombol berdampingan memaksa kolomnya
   322 px, dan tabelnya jadi 121 px lebih lebar dari kartunya -- halaman
   bisa digulir menyamping. .d-card-scroll memang sengaja mengizinkan
   gulir, tapi itu untuk tabel yang benar-benar butuh; di sini cuma dua
   tombol yang tidak mau mengalah. Boleh membungkus ke baris kedua saat
   sempit, bukan dipaksa satu baris.

   gap 9px = jarak baku sekelompok tombol di sistem ini (.d-head-aksi,
   .d-filter-aksi, .scan-aksi, .d-sheet-aksi semuanya 9px). Percobaan
   pertama memakai line-height + margin-right 4px dan hasilnya tombolnya
   terlihat menempel, tidak selaras dengan kelompok tombol mana pun.

   Flex-nya di DIV pembungkus, BUKAN di <td>: display:flex pada sel
   mencabutnya dari tata letak tabel (bukan lagi table-cell) dan selnya
   bisa berhenti lurus dengan judul kolomnya. Form-nya jadi anak flex --
   itu sebabnya style="display:inline" sebarisnya dibuang di
   daftar_tenant.php; style sebaris akan mengalahkan aturan ini. */
.d-aksi-tenant-grup {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 9px;
}
.d-aksi-tenant-grup > form { display: inline-flex; }
/* Di layar sempit selnya menciut sampai lebih kecil dari tombolnya
   sendiri ("Buat Ulang Link" 123 px di dalam sel 105 px), dan tombolnya
   menyembul keluar kartu. Labelnya boleh patah dua baris di sini SAJA --
   di desktop tetap satu baris. */
/* Lebar TENGAH (769-1150 px): kolom hide-mobile baru bersembunyi di
   bawah 768 px, jadi di band ini ketujuh kolom tampil sementara kartunya
   sudah sempit -- tabelnya lebih 200 px dan kartunya bisa digulir lagi.
   Dua kolom paling sedikit isinya (No. WA yang hampir selalu "-", dan
   tanggal Daftar) turun lebih awal, dan label tombolnya boleh patah.
   Email & Status TETAP tampil: itu yang dipakai mengenali barisnya. */
@media (max-width: 1150px) {
    .d-tabel .d-kolom-sedang { display: none; }
    .d-tabel td.d-aksi-tenant .d-btn { white-space: normal; text-align: center; }
}
/* Satu tingkat lagi di 769-900 px: Email + Aktivasi bersama-sama masih
   melebihi kartunya (terukur lebih 47 px di 800 px). Aktivasi yang turun,
   bukan Email -- keadaan aktivasi masih terbaca dari ada/tidaknya tombol
   "Buat Ulang Link" di kolom Aksi, sedangkan email tidak punya pengganti. */
@media (max-width: 900px) {
    .d-tabel .d-kolom-sempit { display: none; }
}
/* Sudut & garis tepi baris ikut dibuang bersama latarnya: keduanya cuma
   masuk akal untuk baris yang berupa kartu. Radius disisakan pada SOROTAN
   hover di bawah, supaya yang membulat adalah sorotannya, bukan barisnya. */
.d-tabel tr:not(.rt-head):last-child td { border-bottom: 0; }
.d-tabel tbody tr,
.d-tabel tr { transition: transform .26s, box-shadow .26s; }
/* Baris masuk bergiliran (handoff: rowIn .42s, jeda 34ms/baris). Dulu cuma
   .d-table-row versi grid yang dapat; tabel <table> -- yang dipakai sebagian
   besar halaman -- muncul serentak tanpa animasi. */
.d-tabel tr:not(.rt-head) { animation: d-rowIn .42s var(--d-ease-out) backwards; }
.d-tabel tr:not(.rt-head):nth-child(1)  { animation-delay: 0ms; }
.d-tabel tr:not(.rt-head):nth-child(2)  { animation-delay: 34ms; }
.d-tabel tr:not(.rt-head):nth-child(3)  { animation-delay: 68ms; }
.d-tabel tr:not(.rt-head):nth-child(4)  { animation-delay: 102ms; }
.d-tabel tr:not(.rt-head):nth-child(5)  { animation-delay: 136ms; }
.d-tabel tr:not(.rt-head):nth-child(6)  { animation-delay: 170ms; }
.d-tabel tr:not(.rt-head):nth-child(7)  { animation-delay: 204ms; }
.d-tabel tr:not(.rt-head):nth-child(8)  { animation-delay: 238ms; }
.d-tabel tr:not(.rt-head):nth-child(9)  { animation-delay: 272ms; }
.d-tabel tr:not(.rt-head):nth-child(10) { animation-delay: 306ms; }
/* Lewat baris ke-11 delaynya dirata: lebih dari itu terasa seperti halaman
   lambat, bukan animasi -- alasan yang sama seperti .d-table-row. */
.d-tabel tr:not(.rt-head):nth-child(n+11) { animation-delay: 340ms; }

/* Disentuh, baris MENYALA DAN MENGGESER DIRI 4px.

   Geseran ini sempat saya buang dengan alasan kolomnya jadi tidak lurus
   dengan judulnya selama kursor lewat. Dikembalikan atas permintaan pemilik
   (6 Sep 2026): baris yang bergeser + berbayang adalah cara halaman ini
   menyatakan "baris ini yang sedang kamu tunjuk", dan di tabel selebar
   layar penuh itu justru penanda yang paling dibutuhkan. Ketidaklurusan
   sesaat pada satu baris ternyata harga yang jauh lebih murah daripada
   kehilangan penandanya. */
/* SOROTANNYA LAPISAN SENDIRI (::after), BUKAN LATAR <tr>.

   Dua kendala yang harus dipenuhi sekaligus:

   1. Latar TIDAK BOLEH di <td>. Waktu tiap sel memegang latarnya sendiri,
      batas antar sel jatuh di posisi sub-pixel (lebar kolom di sini
      pecahan) dan dua latar SEMI-TRANSPARAN saling menumpuk selebar satu
      piksel. Di area itu alphanya berlipat, dan yang terlihat GARIS
      VERTIKAL di setiap batas kolom.

   2. Latar di <tr> menyembuhkan itu -- satu bidang utuh, tidak ada batas
      untuk ditumpuk -- tapi Chrome MENGABAIKAN border-radius pada
      `display: table-row`. Diuji langsung 6 Sep 2026: baris ber-radius
      14px di <tr> tetap bersudut siku, sementara lapisan overlay di
      dalamnya membulat. Deklarasinya LOLOS ke getComputedStyle (terbaca
      "12px" walau yang tergambar siku), jadi mengukur saja tidak cukup
      untuk menangkap ini -- harus dilihat.

   ::after memenuhi keduanya: kotak biasa (bukan table-row) sehingga
   radiusnya dihormati, sekaligus satu bidang utuh selebar baris sehingga
   tidak ada batas sel untuk ditumpuk.

   Bonusnya kotak biasa juga menerima backdrop-filter -- yang dulu terpaksa
   dilepas karena <tr> tidak bisa memakainya dan <td> memunculkan garis yang
   sama. Jadi barisnya sekarang betul-betul membiaskan apa yang ada di
   belakangnya, bukan sekadar diberi rona tembus.

   z-index -1 menaruh lapisannya di BELAKANG isi sel; `z-index: 0` di
   <tr>-nya yang mengurung lapisan itu di dalam barisnya sendiri, supaya ia
   tidak jatuh sampai ke belakang latar kartu. */
.d-tabel tr:not(.rt-head) { position: relative; z-index: 0; }
.d-tabel tr:not(.rt-head)::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: 12px;
    background: var(--d-baris-hover);
    opacity: 0;
    transition: opacity .2s var(--d-ease-out);
    pointer-events: none;
}
/* backdrop-filter HANYA di keadaan hover, bukan di lapisan dasarnya: satu
   tabel di sini bisa berisi 97 baris, dan 97 lapisan pembias -- walau
   kebetulan tembus pandang -- tetap harus dikomposit browser. Yang menyala
   selalu cuma satu, jadi cuma satu yang perlu membias. */
.d-tabel tr:not(.rt-head):hover::after {
    opacity: 1;
    backdrop-filter: var(--d-baris-bias);
    -webkit-backdrop-filter: var(--d-baris-bias);
    box-shadow: 0 10px 26px -18px rgba(0, 0, 0, .4);
}
.d-tabel tr:not(.rt-head):hover {
    transform: translateX(4px);
}
.d-tabel .empty-state,
.d-tabel td.empty-state {
    padding: 34px;
    text-align: center;
    color: var(--faint);
    background: transparent;
    border: 0;
}
/* Beberapa badge berdampingan dalam satu sel, bukan bertumpuk lewat <br>.
   Dipakai kolom Status di tabel Produk supaya barisnya setinggi satu baris
   teks saja. flex-wrap dipertahankan supaya di kolom sempit badge-nya turun
   sendiri, bukan menjebol lebar tabel. */
.d-badge-baris {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}
.d-badge-baris > .hide-mobile {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

/* Label yang diawali kotak centang (mis. pilihan metrik grafik Laporan). */
.d-cek-label {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    font-size: 13.5px;
}

/* -------------------------------------------------------------------------
   TABEL LABA RUGI (tab Laporan > Laba Rugi)
   Baris pengurang ditulis menjorok & diredupkan; dua baris jumlah diberi
   tint (hijau untuk Laba Kotor, burgundy untuk Laba Bersih) seperti handoff.
   ------------------------------------------------------------------------- */
.d-tabel-labarugi .baris-lr-kurang td:first-child { padding-left: 30px; color: var(--muted); }
.d-tabel-labarugi .baris-lr-total-hijau td {
    background: color-mix(in srgb, var(--green) 11%, var(--card-solid));
    font-weight: 700;
}
.d-tabel-labarugi .baris-lr-total-brand td {
    background: color-mix(in srgb, var(--primary) 11%, var(--card-solid));
    font-weight: 700;
}
.d-tabel-labarugi .baris-lr-total-hijau:hover td,
.d-tabel-labarugi .baris-lr-total-brand:hover td { background: inherit; }

/* Daftar "label ... nilai" dua kolom di dalam kartu (kartu Titik Impas). */
.d-rincian { display: flex; flex-direction: column; gap: 2px; }
.d-rincian-baris {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 14px;
    padding: 9px 0;
    font-size: 13.5px;
    border-bottom: 1px solid var(--hair);
}
.d-rincian-baris:last-child { border-bottom: 0; }
.d-rincian-baris > :first-child { color: var(--muted); }
.d-rincian-total {
    margin-top: 4px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
    border-bottom: 0;
}

/* Sheet Detail Transaksi punya 11 baris rincian PLUS deretan tombol aksi,
   sementara .d-sheet memotong di 84vh lalu menggulir. Waktu baris "No.
   pesanan" ditambahkan (30 Agu 2026), padatnya diturunkan 9px -> 7px KHUSUS
   sheet ini: penghematan 4px x 11 baris (44px) lebih besar daripada tinggi
   baris barunya (~35px), jadi sheetnya justru sedikit lebih pendek daripada
   sebelum baris itu ada. Sengaja discope ke #blokDetailTrx -- .d-rincian
   dipakai sheet lain yang barisnya sedikit dan tidak perlu dipadatkan. */
#blokDetailTrx .d-rincian-baris { padding: 7px 0; }

/* Nilai yang bisa diklik untuk disalin (No. pesanan di sheet Detail: dipakai
   mencari resi ini di file Rekonsiliasi Profit Riil, jadi hampir selalu
   berakhir di clipboard). Garis putus-putus tipis di bawah angka = satu-
   satunya petunjuk bahwa ia bisa diklik; tanpa itu ia terlihat sama persis
   dengan nilai baris lain yang mati. */
.d-nilai-salin {
    cursor: pointer;
    border-bottom: 1px dashed var(--faint);
    transition: color .15s ease, border-color .15s ease;
}
.d-nilai-salin:hover { color: var(--primary); border-bottom-color: var(--primary); }

/* Baris produk nonaktif: diredupkan, bukan disembunyikan. */
.d-tabel tr.tr-nonaktif td { opacity: .55; }

/* Baris yang membuka sheet kalau diklik (Produk & Paket di halaman Stok).
   Handoff memindahkan Edit dari tombol per baris ke barisnya sendiri, jadi
   barisnya harus KELIHATAN bisa diklik. Yang tersisa di sini cuma
   kursornya: geseran 4px + bayangannya sekarang dipunyai SETIAP baris
   .d-tabel (lihat sorotan ::after di atas), jadi aturan khusus di sini
   tinggal menggambar bayangan kedua yang bersudut siku di atas bayangan
   pertama yang sudah membulat. */
.d-tabel tr.d-baris-klik { cursor: pointer; }
.d-tabel tr.d-baris-klik:focus-visible {
    outline: none;
    transform: translateX(4px);
}
.d-tabel tr.d-baris-klik:focus-visible td {
    background: color-mix(in srgb, var(--primary) 7%, var(--card-solid));
}
.d-tabel tr.d-baris-klik:focus-visible td:first-child { box-shadow: inset 3px 0 0 var(--primary); }

/* Tabel pratinjau impor -- padat, dalam kotak bergulir. */
.d-tabel-pratinjau { width: 100%; border-collapse: collapse; font-size: 12px; }
.d-tabel-pratinjau th,
.d-tabel-pratinjau td {
    padding: 6px 10px;
    border-bottom: 1px solid var(--hair);
    text-align: left;
    white-space: nowrap;
}
.d-tabel-pratinjau th {
    position: sticky;
    top: 0;
    background: var(--field);
    font-weight: 700;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--faint);
}

.d-table-body { display: flex; flex-direction: column; gap: 0; }
.d-table-row {
    padding: var(--d-pad-row);
    border-radius: 12px;
    /* Telanjang, sama seperti <td> di atas -- alasannya di sana. */
    background: none;
    border: 0;
    box-shadow: 0 1px 0 var(--hair);
    font-size: 13.5px;
    color: inherit;
    text-decoration: none;
    animation: d-rowIn .42s var(--d-ease-out) backwards;
    transition: transform .26s, box-shadow .26s;
}
.d-table-row:hover {
    background: var(--d-baris-hover);
    backdrop-filter: var(--d-baris-bias);
    -webkit-backdrop-filter: var(--d-baris-bias);
    transform: translateX(4px);
    box-shadow: 0 10px 26px -18px rgba(0,0,0,.4);
    text-decoration: none;
}
a.d-table-row, .d-table-row[data-klik] { cursor: pointer; }
/* Sel dipotong ellipsis KECUALI kolom beraksi (.d-keep) -- tanpa ini
   tombol di kolom Aksi ikut terpotong begitu tabelnya sempit. */
.d-table-row > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.d-table-row > .d-keep { overflow: visible; }
.d-kanan  { text-align: right; }
.d-tengah { text-align: center; }
.d-table-kosong { padding: 34px; text-align: center; color: var(--faint); font-size: 13.5px; }

/* Delay masuk per baris 34ms. Dibatasi 12 baris lalu rata: lebih dari itu
   delay-nya jadi lebih dari 0,4 detik dan mulai terasa seperti halaman
   lambat, bukan animasi. Baris ke-25 ke atas langsung tampil tanpa animasi
   sama sekali -- tabel panjang tidak boleh menunda pembacaan. */
.d-table-body > .d-table-row:nth-child(1)  { animation-delay: 0ms; }
.d-table-body > .d-table-row:nth-child(2)  { animation-delay: 34ms; }
.d-table-body > .d-table-row:nth-child(3)  { animation-delay: 68ms; }
.d-table-body > .d-table-row:nth-child(4)  { animation-delay: 102ms; }
.d-table-body > .d-table-row:nth-child(5)  { animation-delay: 136ms; }
.d-table-body > .d-table-row:nth-child(6)  { animation-delay: 170ms; }
.d-table-body > .d-table-row:nth-child(7)  { animation-delay: 204ms; }
.d-table-body > .d-table-row:nth-child(8)  { animation-delay: 238ms; }
.d-table-body > .d-table-row:nth-child(9)  { animation-delay: 272ms; }
.d-table-body > .d-table-row:nth-child(10) { animation-delay: 306ms; }
.d-table-body > .d-table-row:nth-child(11) { animation-delay: 340ms; }
.d-table-body > .d-table-row:nth-child(n+12) { animation-delay: 374ms; }
.d-table-body > .d-table-row:nth-child(n+25) { animation: none; }

/* Tabel yang badannya dirender ulang berkala (Barang Keluar: polling 3 detik
   supaya terasa realtime saat resi discan). Di sana animasi masuk justru
   merugikan -- tiap muat ulang mengganti SELURUH isi tbody, jadi d-rowIn main
   lagi dari awal dan penyegaran yang seharusnya tak terasa malah jadi kedipan
   berjenjang tiap 3 detik.
   Berlaku di HP maupun desktop: aturannya tidak dibungkus media query.
   Ditaruh SESUDAH blok nth-child di atas -- kekhususannya seri (0,3,0), jadi
   yang menang adalah yang belakangan. */
.d-tanpa-animasi .d-table-body > .d-table-row,
.d-tanpa-animasi .d-table-row * { animation: none; }

/* Padanan untuk keluarga <table class="d-tabel">.
   Kenapa baru ada sekarang: saklar .d-tanpa-animasi dibuat 17 Agu 2026 untuk
   Barang Keluar, dan halaman itu memakai grid d_table() -- jadi aturannya
   ditulis hanya untuk .d-table-row. Animasi baris pada <table> sendiri sudah
   ada sejak 14 Agu (fondasi UI desktop). Halaman Scan punya polling 3 detik
   yang persis sama dengan Barang Keluar, tapi tabelnya <table>, jadi ia tidak
   pernah kebagian saklarnya: tiap penyegaran mengganti seluruh <tbody> dan
   d-rowIn main lagi dari awal, berjenjang, tiap 3 detik.

   Ditulis :not(.rt-head) juga -- kekhususannya jadi (0,3,1), sedikit di atas
   aturan animasi yang dilawan. Tanpa itu keduanya seri dan yang menang cuma
   ditentukan urutan baris di berkas ini; seri semacam itu pecah diam-diam
   begitu ada yang merapikan urutan blok CSS. */
.d-tabel.d-tanpa-animasi tr:not(.rt-head),
.d-tabel.d-tanpa-animasi tr:not(.rt-head) * { animation: none; }

/* PASANGAN .d-tanpa-animasi: panel tab yang barisnya sengaja dibisukan.

   Halaman Stok jadi patokan pergantian tab di seluruh aplikasi, dan di sana
   yang bergerak saat tab diganti BUKAN panelnya melainkan BARIS TABELNYA --
   d-fadeUp milik panel justru dituntaskan seketika, karena panel itu ikut
   membawa pil tab yang barusan ditekan (lihat tahanBarTetapDiam). Tombol
   yang melompat sesudah ditekan selalu terasa seperti halaman dimuat ulang.

   Pola itu runtuh di Barang Keluar, dan tidak kelihatan sampai dicoba:
   halaman itu memuat ulang badan tabelnya tiap 3 detik, jadi barisnya
   memakai .d-tanpa-animasi di atas. Begitu d-fadeUp panel ikut dituntaskan,
   yang tersisa NOL gerakan -- isi tab berganti seketika, dan pergantian yang
   tidak bergerak sama sekali persis terbaca sebagai halaman yang di-refresh.
   Dua mekanisme yang masing-masing benar, saling meniadakan.

   Gerakannya dikembalikan satu tingkat lebih dalam: bukan panelnya, bukan
   barisnya, melainkan ISI KARTU. Ketiganya -- judul, "pilih semua", dan
   tabelnya -- tidak memuat pil tab, jadi tahanBarTetapDiam membiarkannya
   berjalan; dan tidak satu pun diganti oleh polling 3 detik, jadi kedipan
   yang dihindari .d-tanpa-animasi tidak kembali.

   Berjenjang lewat KELAS, bukan :nth-child. Bar tab diselipkan JS sebagai
   anak pertama kartu, jadi nomor urut tiap anak bergeser satu begitu bar itu
   masuk -- dan bergeser lagi di kartu yang punya paragraf keterangan.
   Jenjang yang dihitung dari nomor urut akan diam-diam salah di salah satu
   dari keduanya.

   BARIS KEDUA SELEKTOR BUKAN KELEBIHAN. Kedua tab halaman ini menyusun
   kartunya dengan cara berbeda: tab Menunggu Checkout membungkus SELURUH
   isinya dalam satu <form> aksi massal, tab Pesanan Pending tidak. Dengan
   satu selektor saja, yang pertama cuma punya satu anak untuk dianimasikan
   (form-nya) sehingga isinya masuk sebagai satu blok, sementara yang kedua
   berjenjang -- dua tab bersebelahan dengan gerakan berbeda, tanpa alasan
   yang bisa dilihat siapa pun. :not(form) mencegah keduanya beranimasi
   sekaligus, yang akan menumpuk pudarnya jadi dua kali. */
/* LONGHAND, BUKAN SHORTHAND `animation`, dan ini bukan selera.

   Shorthand menyetel ulang SEMUA sub-properti yang tidak disebut, termasuk
   animation-delay. Selektor di sini berkekuatan (0,4,1) -- tiga kelas,
   ditambah satu kelas dan satu elemen dari kedua :not() -- sementara aturan
   jenjang di bawahnya cuma (0,3,0). Jadi shorthand-nya MENANG dan mengembali-
   kan delay ke 0 di mana pun ia ditulis; menaruhnya lebih dulu tidak
   menolong, karena urutan baru menentukan saat kekhususannya seri. Terukur:
   getComputedStyle(.d-pilih-semua).animationDelay mengembalikan "0s" padahal
   aturan 55ms-nya cocok.

   Dengan longhand tidak ada satu pun yang menyentuh animation-delay selain
   aturan jenjangnya sendiri, jadi soal kekhususan itu hilang sama sekali
   alih-alih dimenangkan dengan selektor yang lebih panjang. */
.d-tab-isi-masuk.active > .d-card > *:not(.d-stok-bar):not(form),
.d-tab-isi-masuk.active > .d-card > form > * {
    animation-name: d-fadeUp;
    animation-duration: .38s;
    animation-timing-function: var(--d-ease-out);
    animation-fill-mode: backwards;
}
/* Selektor keturunan, bukan anak langsung: keduanya harus berlaku baik di
   kartu yang isinya terbungkus <form> maupun yang tidak. */
.d-tab-isi-masuk.active .d-pilih-semua { animation-delay: 55ms; }
.d-tab-isi-masuk.active .d-table       { animation-delay: 95ms; }

@media (prefers-reduced-motion: reduce) {
    .d-tab-isi-masuk.active > .d-card > *,
    .d-tab-isi-masuk.active > .d-card > form > * { animation-name: none; }
}

/* SKU pada Barang Keluar dicetak tebal (permintaan pemilik 17 Agu 2026).
   Di HP ia menggantikan kolom Penerima, di desktop ia satu dari tujuh
   kolom -- di kedua-duanya dialah yang dibaca petugas saat mencocokkan
   barang, jadi ia yang perlu paling cepat ketemu mata. Tidak dibungkus
   media query: berlaku di HP maupun desktop.
   Di-scope ke .baris-belum-checkout, BUKAN ke [data-label="SKU"] umum:
   data-label dipasang otomatis oleh d_table() dari judul kolom, jadi
   selektor tanpa scope akan ikut menebalkan kolom SKU di halaman lain
   yang tidak meminta apa-apa.
   .detail-more ("+2 lainnya") sengaja dikembalikan ke berat biasa -- itu
   keterangan bahwa masih ada sisa, bukan bagian dari SKU-nya. */
.baris-belum-checkout > [data-label="SKU"] { font-weight: 700; }
.baris-belum-checkout > [data-label="SKU"] .detail-more { font-weight: 400; }

/* -------------------------------------------------------------------------
   13. BADGE & DELTA
   ------------------------------------------------------------------------- */
.d-badge {
    display: inline-block;
    animation: d-popIn .28s var(--d-ease-pop) backwards;
    padding: 4px 10px;
    border-radius: 999px;
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .01em;
    background: color-mix(in srgb, var(--d-badge-warna, var(--muted)) 14%, transparent);
    color: var(--d-badge-warna, var(--muted));
}
/* Badge yang sekaligus tautan (mis. "n SKU tertahan" di Data Reseller):
   tetap terlihat sebagai badge, bukan teks bergaris bawah. */
a.d-badge { text-decoration: none; cursor: pointer; }
.d-badge-ok    { --d-badge-warna: var(--green); }
.d-badge-warn  { --d-badge-warna: var(--orange); }
.d-badge-buruk { --d-badge-warna: var(--red); }
.d-badge-info  { --d-badge-warna: var(--teal); }
/* Kelabu: keadaan yang bukan kabar baik maupun buruk, cuma "tidak berjalan"
   (tenant Nonaktif & Internal di handoff). --d-badge-info sudah dipakai teal,
   dan teal terbaca seperti informasi penting. */
.d-badge-netral { --d-badge-warna: var(--muted); }
.d-badge-brand { --d-badge-warna: var(--primary); }
/* Warna merek marketplace. Ditaruh sebagai token supaya tema gelap bisa
   menaikkan terangnya tanpa mengulang aturan badge-nya. Nilai mentah merek
   itu dirancang untuk latar putih; di atas #0b0b0d hijau Tokopedia mulai
   redup, jadi versi gelapnya dinaikkan. */
.d-badge-shopee { --d-badge-warna: var(--merek-shopee); }
.d-badge-tiktok { --d-badge-warna: var(--merek-tiktok); }

.d-delta {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 9px;
    border-radius: 999px;
    font-size: 11.5px;
    font-weight: 700;
    line-height: 1;
    letter-spacing: -.01em;
    background: color-mix(in srgb, var(--d-delta-warna) 14%, transparent);
    color: var(--d-delta-warna);
}
.d-delta-naik  { --d-delta-warna: var(--green); }
.d-delta-turun { --d-delta-warna: var(--red); }
.d-delta i { font-style: normal; font-size: 9px; }

/* -------------------------------------------------------------------------
   14. BAR HORIZONTAL & PROGRESS
   ------------------------------------------------------------------------- */
.d-bars { display: flex; flex-direction: column; gap: 13px; }
.d-bar-label { display: flex; justify-content: space-between; font-size: 13.5px; margin-bottom: 6px; }
.d-bar-label > :first-child { font-weight: 600; }
.d-bar-label > :last-child { color: var(--muted); font-variant-numeric: tabular-nums; }
.d-bar-track { height: 8px; border-radius: 6px; background: var(--field); overflow: hidden; }
.d-bar-isi {
    height: 100%;
    border-radius: 6px;
    background: var(--d-bar-warna, var(--primary));
    transform-origin: left;
    animation: d-growX .8s var(--d-ease-out) backwards;
    transition: width .6s;
}
/* ---------------------------------------------------------------------
   Grafik batang berkelompok (d_chart_batang) -- dua batang per kelompok.
   Tingginya persentase dari puncak, jadi tidak perlu diukur JS.
   --------------------------------------------------------------------- */
/* ---------------------------------------------------------------------
   Slot suara scan (sheet Unggah Suara Scan). Satu baris per kejadian, dan
   barisnya SEKALIGUS zona lepas berkas -- garis putus-putus bernada sesuai
   kejadiannya (hijau berhasil, merah gagal, kelabu selesai).
   --------------------------------------------------------------------- */
.d-slot-suara {
    --d-slot-warna: var(--muted);
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 14px 16px;
    border-radius: 16px;
    border: 1.5px dashed color-mix(in srgb, var(--d-slot-warna) 40%, transparent);
    background: color-mix(in srgb, var(--d-slot-warna) 5%, transparent);
    transition: border-color .22s, background .22s, transform .22s;
}
.d-slot-ok     { --d-slot-warna: var(--green); }
.d-slot-buruk  { --d-slot-warna: var(--red); }
.d-slot-netral { --d-slot-warna: var(--muted); }
.d-slot-suara.d-slot-aktif {
    border-color: var(--d-slot-warna);
    background: color-mix(in srgb, var(--d-slot-warna) 12%, transparent);
    transform: scale(1.01);
}
.d-slot-ikon {
    flex: 0 0 38px;
    width: 38px;
    height: 38px;
    border-radius: 11px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--d-slot-warna) 16%, transparent);
    color: var(--d-slot-warna);
}
.d-slot-ikon svg { width: 18px; height: 18px; }
.d-slot-teks { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.d-slot-judul { font-size: 14px; font-weight: 700; letter-spacing: -.01em; }
.d-slot-ket   { font-size: 12px; color: var(--muted); }
.d-slot-berkas {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11.5px;
    color: var(--faint);
    overflow-wrap: anywhere;
}
.d-slot-aksi { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }

/* Kepala sheet Edit Profil: avatar + tombol ganti foto berdampingan. */
.d-profil-atas { display: flex; align-items: center; gap: 16px; }
.d-profil-atas .user-avatar { flex: 0 0 62px; width: 62px; height: 62px; font-size: 22px; }

/* Indikator kekuatan password. */
.d-pw-kuat { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
.d-pw-track { flex: 1; height: 5px; border-radius: 999px; background: var(--field); overflow: hidden; }
.d-pw-isi { display: block; height: 100%; width: 0; border-radius: 999px; transition: width .3s var(--d-ease-out), background .3s; }
.d-pw-label { font-size: 12px; font-weight: 700; flex: 0 0 auto; }

@media (max-width: 560px) {
    .d-slot-suara { flex-wrap: wrap; }
    .d-slot-aksi { width: 100%; justify-content: flex-end; }
}

/* Sheet dua-rentang: pasangan tanggal KIRI lawan KANAN, dipisah kolom
   "dibandingkan dengan" bergaris tegak. Bertumpuk atas-bawah membuat keempat
   kotaknya terbaca seperti satu daftar berurutan, bukan dua sisi. */
.d-banding-duo {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    gap: 12px;
    align-items: center;
}
/* Tombol tanggal mengisi penuh kolomnya -- kalau dibiarkan selebar isinya,
   sisa ruang di kanan tiap tombol jadi lubang kosong yang membuat sheet
   terasa jauh lebih lebar dari isinya. */
.d-banding-sisi .d-kal-tombol { width: 100%; justify-content: flex-start; }
.d-banding-sisi .d-field + .d-field { margin-top: 12px !important; }
.d-banding-vs {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    width: 82px;
    font-size: 12px;
    color: var(--muted);
    text-align: center;
    line-height: 1.3;
}
.d-banding-vs::before,
.d-banding-vs::after {
    content: "";
    flex: 1;
    width: 1px;
    background: var(--hair);
}
/* Di HP sheet-nya selebar layar -- tiga kolom jadi terlalu sempit, jadi
   dilipat: pasangan atas, pemisah mendatar, pasangan bawah. */
@media (max-width: 640px) {
    .d-banding-duo { grid-template-columns: 1fr; }
    .d-banding-vs {
        flex-direction: row;
        max-width: none;
        width: 100%;
        gap: 12px;
    }
    .d-banding-vs::before,
    .d-banding-vs::after { width: auto; height: 1px; }
}

.d-batang { --d-batang-tinggi: 220px; }
.d-batang-plot {
    display: flex;
    align-items: flex-end;
    gap: 6px;
    height: var(--d-batang-tinggi);
    margin-top: 8px;
}
.d-batang-grup {
    flex: 1 1 0;
    min-width: 0;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    gap: 9px;
}
.d-batang-pasangan {
    flex: 1;
    width: 100%;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    gap: 7px;
}
.d-batang-bar {
    position: relative;
    width: 26px;
    max-width: 22%;
    min-height: 3px;
    border-radius: 7px 7px 0 0;
    transform-origin: bottom;
    animation: d-grow .75s var(--d-ease-out) backwards;
    transition: filter .25s;
}
.d-batang-grup:nth-child(1) .d-batang-bar { animation-delay: 0ms; }
.d-batang-grup:nth-child(2) .d-batang-bar { animation-delay: 60ms; }
.d-batang-grup:nth-child(3) .d-batang-bar { animation-delay: 120ms; }
.d-batang-grup:nth-child(4) .d-batang-bar { animation-delay: 180ms; }
.d-batang-grup:nth-child(5) .d-batang-bar { animation-delay: 240ms; }
.d-batang-grup:nth-child(6) .d-batang-bar { animation-delay: 300ms; }
.d-batang-grup:nth-child(n+7) .d-batang-bar { animation-delay: 360ms; }
.d-batang-bar:hover { filter: brightness(1.12); }
.d-batang-nama {
    font-size: 12px;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}
/* Tooltip kustom -- handoff melarang title= bawaan browser. */
.d-batang-tip {
    position: absolute;
    bottom: calc(100% + 9px);
    left: 50%;
    transform: translateX(-50%) translateY(4px);
    z-index: 5;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 7px 11px;
    border-radius: 11px;
    background: var(--card-solid);
    border: 1px solid var(--border);
    box-shadow: 0 18px 34px -18px rgba(0,0,0,.45);
    font-size: 11.5px;
    color: var(--muted);
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity .22s, transform .22s;
}
.d-batang-tip b { font-size: 13px; color: var(--text); font-variant-numeric: tabular-nums; }
.d-batang-bar:hover .d-batang-tip { opacity: 1; transform: translateX(-50%) translateY(0); }
.d-batang-legenda {
    display: flex;
    gap: 18px;
    margin-top: 16px;
    font-size: 12.5px;
    color: var(--muted);
}
.d-batang-legenda span { display: inline-flex; align-items: center; gap: 7px; }
.d-batang-legenda i { width: 16px; height: 4px; border-radius: 999px; }

.d-bars > *:nth-child(1) .d-bar-isi { animation-delay: 0ms; }
.d-bars > *:nth-child(2) .d-bar-isi { animation-delay: 70ms; }
.d-bars > *:nth-child(3) .d-bar-isi { animation-delay: 140ms; }
.d-bars > *:nth-child(4) .d-bar-isi { animation-delay: 210ms; }
.d-bars > *:nth-child(5) .d-bar-isi { animation-delay: 280ms; }
.d-bars > *:nth-child(n+6) .d-bar-isi { animation-delay: 350ms; }

/* Bar mini di kolom "Pangsa" tabel produk terbaik: 46x6px.
   display:block WAJIB di track & isi -- keduanya <span> (harus inline agar
   boleh duduk di dalam sel tabel bersama teks persennya), dan elemen inline
   mengabaikan width/height sepenuhnya, jadi tanpa ini barnya tidak pernah
   tergambar sama sekali. */
.d-pangsa { display: inline-flex; align-items: center; gap: 8px; justify-content: flex-end; }
.d-pangsa-track {
    display: block;
    width: 46px;
    height: 6px;
    border-radius: 4px;
    background: var(--field);
    overflow: hidden;
    flex: 0 0 46px;
}
.d-pangsa-isi {
    display: block;
    height: 100%;
    border-radius: 4px;
    background: var(--primary);
    transform-origin: left;
    animation: d-growX .8s var(--d-ease-out) backwards;
}

/* -------------------------------------------------------------------------
   15. ISIAN
   ------------------------------------------------------------------------- */
.d-field { display: block; }
.d-field-label { display: block; font-size: 12.5px; font-weight: 600; color: var(--muted); margin-bottom: 7px; }
.d-field-bantuan { display: block; font-size: 11.5px; line-height: 1.6; color: var(--muted); margin-top: 6px; }
/* JARAK FORMULIR BAKU (pemilik 16 Sep 2026: "semua spacing di halaman mana
   pun seperti Profil"): antar isian 12px, isian terakhir ke tombol 16px,
   label ke kotak 7px (di atas). Formulir bertumpuk memakai .d-form +
   .d-form-aksi -- jangan menulis kelas kembar per halaman lagi.
   Aturan bawaan di bawahnya dibungkus :where() (spesifisitas nol) supaya
   isian yang lupa dibungkus tetap berjarak, tapi baris isian berjejer
   (flex/grid milik halaman) yang menolkan margin selalu menang. */
:where(.d-field + .d-field) { margin-top: 12px; }
.d-field-baris > .d-field { margin-top: 0; }
.d-form { display: flex; flex-direction: column; gap: 12px; }
.d-form .d-field { margin: 0; }
.d-form-aksi { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 16px; }
.d-form > .d-form-aksi { margin-top: 4px; }
.d-input {
    width: 100%;
    padding: 11px 14px;
    border-radius: 12px;
    border: 1px solid var(--border);
    background: var(--card-solid);
    color: var(--text);
    font-size: 14px;
    transition: border-color .25s, box-shadow .25s;
}
.d-input:focus {
    border-color: var(--primary);
    box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--primary) 16%, transparent);
}
.d-input-angka { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; font-size: 13.5px; }

/* Panah naik-turun bawaan browser di <input type="number"> dibuang.
   Dua alasan: bentuknya digambar OS (handoff melarang kontrol bawaan
   browser), dan di kolom harga tombol itu lebih sering jadi sumber salah
   input daripada bantuan -- sekali serempet, HPP berubah tanpa disadari.
   Angka tetap diketik seperti biasa; tempat yang memang butuh naik-turun
   sudah punya .d-stepper sendiri.
   Pasangannya di assets/app.js: roda mouse & tombol panah/PageUp/PageDown
   juga tidak lagi mengubah nilainya. */
.d-input[type="number"]::-webkit-outer-spin-button,
.d-input[type="number"]::-webkit-inner-spin-button,
.qty-field::-webkit-outer-spin-button,
.qty-field::-webkit-inner-spin-button,
.inp-qty-selamat::-webkit-outer-spin-button,
.inp-qty-selamat::-webkit-inner-spin-button {
    -webkit-appearance: none;
    appearance: none;
    margin: 0;
}
.d-input[type="number"],
.qty-field,
.inp-qty-selamat {
    -moz-appearance: textfield;
    appearance: textfield;
}

/* Isian yang nilainya sudah diubah tapi BELUM disimpan (tabel HPP Reseller).
   Penanda ini yang membuat "n SKU diubah" di action bar bisa ditelusuri
   balik ke barisnya -- tanpa itu user harus mengingat sendiri mana yang
   tadi disentuh sebelum menekan Simpan. */
.d-input-berubah {
    border-color: var(--primary);
    background: color-mix(in srgb, var(--primary) 6%, var(--card-solid));
}

/* <select> yang TIDAK bisa dijadikan pill.
   Aturan #1 handoff melarang <select>, dan hampir semuanya sudah diganti
   d_pilih(). Yang tersisa adalah dropdown yang daftar pilihannya DIBANGUN
   ULANG oleh JS saat berjalan (mis. daftar supplier solasi yang menyesuaikan
   jenis kardus terpilih) dan bisa memuat puluhan baris -- deretan pill di
   situ jadi tidak terpakai, bukan lebih baik. Ditema seperti .d-input,
   dengan panah digambar sendiri supaya tidak memakai panah bawaan OS. */
.d-select {
    width: 100%;
    padding: 11px 38px 11px 14px;
    border-radius: 12px;
    border: 1px solid var(--border);
    background-color: var(--card-solid);
    color: var(--text);
    font-size: 14px;
    appearance: none;
    -webkit-appearance: none;
    cursor: pointer;
    /* PANAH SATU GAMBAR, BUKAN DUA SEGITIGA (15 Sep 2026).
       Dulu panahnya dua linear-gradient 5px yang diletakkan bersebelahan di
       posisi pecahan (calc(50% + 1px) pada tinggi ganjil). Di layar berskala
       125% kedua belahnya dibulatkan ke piksel yang BERBEDA, jadi panah
       terlihat seperti dua segitiga siku yang belum menyatu -- lalu "menyatu"
       begitu fokus menggambar ulang tombolnya. Keluhan pemilik di filter Jenis
       Buku Besar. Satu SVG tidak punya sambungan yang bisa meleset.
       Warnanya dipatok #8e8e93 (abu yang sama dengan sebelumnya di kedua
       tema); tombol pengganti buatan app.js menggambar panahnya sendiri lewat
       ::after supaya bisa ikut currentColor. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238e8e93' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-position: right 14px center;
    background-size: 10px 6px;
    background-repeat: no-repeat;
    transition: border-color .25s, box-shadow .25s;
}
.d-select:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--primary) 16%, transparent);
}

/* <select> aslinya TIDAK dibuang -- dia tetap sumber nilai & tetap ikut
   terkirim saat form disubmit; yang terlihat cuma tombol pengganti di
   sebelahnya (lihat pasangPilihKustom() di assets/app.js). Disembunyikan
   dengan cara yang tidak melepasnya dari form. */
/* !important DAN pointer-events:none, dua-duanya perlu.
   Halaman punya kebiasaan wajar menata <select>-nya sendiri (mis.
   `.pd-kolom select { width: 100% }` di pindah.php, `.ms-pilih select` di
   masuk.php). Selektor semacam itu sama kuatnya dengan selektor di sini
   dan berdiri lebih belakang, jadi ia MENANG: terukur di layar, <select>
   yang "tersembunyi" itu sebenarnya 1091x21px melayang di atas halaman --
   satu bahkan sampai keluar kartunya. Di Chrome clip-path membuatnya tidak
   bisa diketuk, jadi tidak ada yang sadar; di mesin lain hit-test pada
   clip-path tidak sekuat itu, dan yang terjadi: ketukan mendarat di
   <select> asli lalu browser membuka pemilih bawaannya sendiri di atas
   dropdown bertema kita. pointer-events:none menutup kemungkinan itu tanpa
   bergantung pada clip-path -- fokus lewat kode (validasi) tetap jalan. */
select.d-select-asli {
    position: absolute;
    width: 1px !important;
    height: 1px !important;
    min-width: 0 !important;
    padding: 0 !important;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
    pointer-events: none;
}
.d-select-tombol {
    display: block;
    position: relative;
    text-align: left;
    font-family: inherit;
    line-height: 1.35;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    background-image: none;
}
/* Panah tombol pengganti: SATU bentuk (sudut dua garis), ikut currentColor
   -- jadi di judul kolom tabel ia ikut abu judulnya, di isian ikut warna teks.
   Lihat catatan panah di .d-select di atas soal kenapa bukan dua gradien. */
.d-select-tombol::after {
    content: '';
    position: absolute;
    right: 16px;
    top: 50%;
    width: 6px;
    height: 6px;
    border-right: 1.6px solid currentColor;
    border-bottom: 1.6px solid currentColor;
    transform: translateY(-70%) rotate(45deg);
    opacity: .7;
    pointer-events: none;
}
.d-select-tombol.d-select-kosong { color: var(--faint); }
.d-select-tombol:disabled { opacity: .55; cursor: not-allowed; }

.d-pilih-panel {
    position: absolute;
    z-index: 90;
    max-width: min(420px, calc(100vw - 24px));
    padding: 6px;
    border-radius: 16px;
    background: var(--card-solid);
    border: 1px solid var(--border);
    box-shadow: 0 24px 50px -18px rgba(0, 0, 0, .4);
    animation: d-fadeUp .24s var(--d-ease-out) backwards;
}

/* PEMILIH DI HP: naik dari dasar layar, bukan panel melayang di sebelah
   tombol. Lihat layarSempit() di app.js.
   Kenapa: di HP tombolnya sering berada di dalam sheet yang sudah menempel di
   dasar layar, jadi tidak ada sisa ruang untuk panel yang dijangkarkan --
   ia berakhir tumpang tindih dengan sheet-nya atau di luar layar, dan yang
   menekan menyimpulkan dropdownnya mati.
   z-index 320: di atas .d-sheet-backdrop (70) dan .d-toast (400 tetap di
   atasnya, memang begitu -- pesan hasil harus terbaca di atas pemilih). */
@media (max-width: 768px) {
    .d-pilih-panel-hp {
        position: fixed;
        left: 0; right: 0; bottom: 0; top: auto;
        z-index: 320;
        min-width: 0;
        max-width: none;
        width: 100%;
        border-radius: 22px 22px 0 0;
        padding: 10px 10px calc(12px + env(safe-area-inset-bottom, 0px));
        box-shadow: 0 -18px 60px rgba(0,0,0,.45);
        animation: d-pilihNaik .24s cubic-bezier(.2,.95,.25,1.02);
    }
    /* Pegangan kecil di kepala, penanda bahwa ini panel yang bisa ditutup --
       kosakata yang sama dengan sheet di aplikasi ini. */
    .d-pilih-panel-hp::before {
        content: ''; display: block; width: 38px; height: 4px; margin: 2px auto 10px;
        border-radius: 99px; background: var(--border);
    }
    .d-pilih-panel-hp .d-pilih-daftar { max-height: 52vh; }
    .d-pilih-panel-hp .d-pilih-opsi   { padding: 13px 14px; font-size: 14.5px; }
    .d-pilih-panel-hp .d-pilih-cari   { font-size: 15px; padding: 11px 13px; }
}
@keyframes d-pilihNaik {
    from { transform: translateY(14px); opacity: 0; }
    to   { transform: translateY(0);    opacity: 1; }
}
.d-pilih-cari {
    width: 100%;
    margin-bottom: 6px;
    padding: 9px 12px;
    border-radius: 11px;
    border: 1px solid var(--border);
    background: var(--field);
    color: var(--text);
    font-size: 13.5px;
}
.d-pilih-cari:focus {
    outline: none;
    border-color: var(--primary);
}
.d-pilih-daftar { max-height: 300px; overflow-y: auto; }
.d-pilih-grup {
    padding: 8px 12px 4px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--faint);
}
.d-pilih-opsi {
    display: block;
    width: 100%;
    padding: 10px 12px;
    border: none;
    background: none;
    border-radius: 11px;
    cursor: pointer;
    text-align: left;
    color: var(--text);
    font-size: 13.5px;
    font-family: inherit;
    transition: background .18s;
}
.d-pilih-opsi:hover,
.d-pilih-opsi:focus-visible { background: var(--field); outline: none; }
.d-pilih-opsi.aktif { background: color-mix(in srgb, var(--primary) 12%, transparent); color: var(--primary); font-weight: 600; }
.d-pilih-opsi:disabled { opacity: .45; cursor: default; }
.d-pilih-kosong { padding: 22px; text-align: center; font-size: 13px; color: var(--faint); }
.d-field-baris { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
/* Tiga isian sebaris (HPP / Harga Jual / Harga Reseller di form Paket). */
.d-field-baris-3 { grid-template-columns: 1fr 1fr 1fr; }

/* Kotak centang berlabel yang berdiri sendiri (bukan di dalam tabel):
   seluruh barisnya jadi sasaran klik, bukan cuma kotak 18px-nya. */
.d-toggle-baris {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 11px 14px;
    border-radius: 12px;
    background: var(--field);
    font-size: 13.5px;
    cursor: pointer;
    transition: background .2s;
}
.d-toggle-baris:hover { background: color-mix(in srgb, var(--primary) 7%, var(--field)); }
.d-toggle-baris input { flex: 0 0 auto; }

/* -------------------------------------------------------------------------
   PEMILIH KOMPONEN PAKET (sheet Tambah / Edit Paket)
   Menggantikan deretan <select> bawaan browser: komponen terpilih tampil
   sebagai kartu (kode + modalnya + stepper qty + tombol buang), dan produk
   yang belum dipakai dipilih dari panel yang bisa dibuka-tutup.
   ------------------------------------------------------------------------- */
.komp-kepala {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 8px;
}
.komp-baris {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    margin-bottom: 6px;
    border-radius: 14px;
    background: var(--card-solid);
    border: 1px solid var(--hair);
    animation: d-fadeUp .32s var(--d-ease-out) backwards;
}
.komp-info { flex: 1; min-width: 0; }
.komp-kode {
    display: block;
    font-size: 13.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.komp-ket {
    display: block;
    margin-top: 2px;
    font-size: 11.5px;
    color: var(--faint);
}
.komp-buang {
    flex: 0 0 26px;
    width: 26px;
    height: 26px;
    border: none;
    border-radius: 50%;
    background: var(--field);
    color: var(--muted);
    font-size: 11px;
    line-height: 1;
    cursor: pointer;
    transition: background .22s, color .22s;
}
.komp-buang:hover {
    background: color-mix(in srgb, var(--red) 16%, transparent);
    color: var(--red);
}
.komp-kosong {
    padding: 20px;
    border-radius: 14px;
    background: var(--field);
    text-align: center;
    font-size: 13px;
    color: var(--faint);
}
.komp-tambah-wrap { margin-top: 10px; }
.komp-panel {
    margin-top: 8px;
    max-height: 220px;
    overflow: auto;
    padding: 6px;
    border-radius: 16px;
    background: var(--field);
    animation: d-fadeUp .3s var(--d-ease-out) backwards;
}
.komp-opsi {
    display: flex;
    width: 100%;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 12px;
    /* Border transparan, bukan `none`: hover mengubah WARNANYA, dan tanpa
       border yang sudah ada perubahan warna itu tidak terlihat sekaligus
       menggeser tata letak 1px saat muncul. */
    border: 1px solid transparent;
    background: none;
    border-radius: 11px;
    cursor: pointer;
    text-align: left;
    color: var(--text);
    transition: background .2s, transform .24s, border-color .24s;
}
/* Handoff: butir daftar di sheet -> translateX(4px) + border --primary. */
.komp-opsi:hover {
    background: var(--card-solid);
    transform: translateX(4px);
    border-color: var(--primary);
}
.komp-tambah-label {
    flex: 0 0 auto;
    font-size: 12.5px;
    font-weight: 700;
    color: var(--primary);
}

.d-stepper {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px;
    border-radius: 12px;
    background: var(--field);
}
.d-stepper button {
    width: 32px;
    height: 32px;
    border-radius: 9px;
    border: none;
    cursor: pointer;
    background: var(--card-solid);
    color: var(--text);
    font-size: 16px;
    font-weight: 600;
    line-height: 1;
    box-shadow: 0 1px 2px rgba(0,0,0,.12);
    transition: transform .22s;
}
.d-stepper button:active { transform: scale(.9); }
.d-stepper-nilai {
    min-width: 38px;
    width: 4.5em;
    text-align: center;
    font-size: 14.5px;
    font-weight: 700;
}
/* Angkanya bisa diketik langsung -- tombol saja tidak cukup untuk perubahan
   stok besar. Tampilannya tetap teks polos, bukan kotak input. */
input.d-stepper-nilai {
    border: none;
    background: none;
    color: inherit;
    font-family: inherit;
    padding: 0;
    border-radius: 8px;
}
input.d-stepper-nilai:focus { outline: 2px solid var(--primary); outline-offset: 2px; }

.d-toggle {
    position: relative;
    width: 50px;
    height: 30px;
    border-radius: 999px;
    border: none;
    cursor: pointer;
    background: var(--field);
    transition: background .32s;
    flex: 0 0 50px;
}
.d-toggle.aktif { background: var(--green); }
.d-toggle::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 3px rgba(0,0,0,.3);
    transition: transform .34s var(--d-ease-pop);
}
.d-toggle.aktif::after { transform: translateX(20px); }

/* Drop zone berkas. Aturan #1 handoff melarang elemen bawaan browser, dan
   <input type="file"> adalah pelanggar terakhir yang tersisa: tombol
   "Choose Files"-nya digambar OS, tidak bisa ditema, dan berbahasa Inggris
   walau seluruh UI berbahasa Indonesia. Input aslinya tetap ada (dibuat
   tak terlihat, BUKAN display:none supaya tetap bisa difokus keyboard dan
   tetap ikut ter-submit); yang terlihat adalah zona ini. */
.d-drop {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 22px 18px;
    border-radius: 16px;
    border: 1.5px dashed var(--border);
    background: var(--field);
    text-align: center;
    cursor: pointer;
    transition: border-color .25s, background .25s;
}
.d-drop:hover { border-color: var(--primary); }
.d-drop.seret {
    border-color: var(--primary);
    background: color-mix(in srgb, var(--primary) 8%, transparent);
}
.d-drop input[type="file"] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
}
.d-drop svg { width: 22px; height: 22px; color: var(--faint); }
.d-drop-judul { font-size: 13.5px; font-weight: 600; }
.d-drop-ket   { font-size: 12px; color: var(--faint); }
.d-drop-nama  { font-size: 12.5px; font-weight: 600; color: var(--primary); }
/* Jarak antar langkah di blok Rekonsiliasi (index.php). Dulu style inline;
   dijadikan kelas supaya versi ringkas di kartu HP bisa merapatkannya tanpa
   perlu !important. Nilainya sama persis dengan inline yang digantikan. */
.d-rekon-langkah { margin-top: 16px; }

/* Checkbox bulat. appearance:none lalu digambar sendiri -- kotak centang
   bawaan OS satu-satunya elemen di halaman ini yang tidak ikut tema, dan
   di mode gelap ia tampil sebagai kotak putih terang yang mencolok. */
.d-check {
    appearance: none;
    -webkit-appearance: none;
    position: relative;
    width: 21px;
    height: 21px;
    flex: 0 0 21px;
    border-radius: 7px;
    border: 1.5px solid var(--border);
    background: var(--card-solid);
    cursor: pointer;
    margin: 0;
    transition: background .24s var(--d-ease-pop), border-color .24s var(--d-ease-pop), transform .24s var(--d-ease-pop);
}
.d-check:hover { border-color: var(--primary); }
.d-check:checked {
    background: var(--primary);
    border-color: var(--primary);
    /* Sedikit membesar saat tercentang -- gerakan yang sama dengan
       prototipe, jadi perubahannya terasa, bukan cuma berganti warna. */
    transform: scale(1.06);
}
.d-check:checked::after {
    content: "";
    position: absolute;
    left: 6.5px;
    top: 3px;
    width: 5px;
    height: 10px;
    border: solid #fff;
    border-width: 0 2.2px 2.2px 0;
    transform: rotate(45deg);
}
/* Keadaan setengah (sebagian baris terpilih) di kotak "pilih semua". */
.d-check:indeterminate {
    background: var(--primary);
    border-color: var(--primary);
}
.d-check:indeterminate::after {
    content: "";
    position: absolute;
    left: 4px;
    top: 8.2px;
    width: 11px;
    height: 0;
    border-top: 2.2px solid #fff;
    border-radius: 2px;
}
/* Kolom yang gagal validasi. Ditandai app.js waktu peristiwa 'invalid'
   ditahan -- lihat pasangValidasiKustom(). Ini pengganti VISUAL dari
   gelembung bawaan browser, bukan tambahan di atasnya. */
.d-invalid,
.d-input.d-invalid,
.d-select.d-invalid {
    border-color: var(--red) !important;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 16%, transparent);
}
/* <select> yang sudah digantikan tombol kustom disembunyikan, jadi yang
   perlu disorot tombolnya -- bukan select-nya yang tak terlihat.

   PENTING soal arah selektor: pasangPilihKustom() di app.js memasang
   tombolnya dengan insertBefore(tombol, sel) -- jadi urutan DOM-nya
   TOMBOL dulu, baru <select>. Versi pertama aturan ini memakai
   `.d-invalid + .d-select-tombol` ("tombol SESUDAH select") dan karena itu
   tidak pernah cocok sama sekali. CSS tidak punya kombinator "saudara
   sebelumnya", jadi arahnya dibalik lewat :has(). */
.d-select-tombol:has(+ select.d-select-asli.d-invalid) {
    border-color: var(--red) !important;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 16%, transparent);
}

.d-check:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--primary) 16%, transparent);
}

/* Baris "Pilih semua N" di atas tabel bercentang. */
.d-pilih-semua {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 11px 14px;
    border-radius: 14px;
    background: var(--field);
    margin-bottom: 10px;
    font-size: 13.5px;
    font-weight: 600;
    cursor: pointer;
}

/* -------------------------------------------------------------------------
   KOTAK SCAN (halaman Scan)
   Nama class-nya SENGAJA dipertahankan (.scan-box, .scan-input, .scan-sisa)
   dan bukan diberi awalan d-. Dua alasan: seluruh JS halaman Scan menyasar
   kelas & id ini (fokus otomatis, polling, mode kamera), dan assets/mobile.css
   sudah punya versi mobilenya sendiri untuk .scan-box -- dimuat setelah
   berkas ini, jadi di HP tetap versi mobile yang menang tanpa perlu diubah.
   ------------------------------------------------------------------------- */
/* =========================================================================
   KONSOL SCAN -- halaman ini berhenti jadi dokumen, jadi tempat kerja
   =========================================================================
   Sebelumnya tab Sore adalah TUMPUKAN VERTIKAL: kotak scan, lalu ringkasan,
   lalu riwayat. Susunan itu masuk akal untuk halaman yang DIBACA dari atas ke
   bawah. Halaman ini tidak dibaca -- ia dipakai. Operator berdiri dengan
   scanner di tangan dan mengulang satu tindakan ratusan kali sepanjang sore.

   Akibat susunan lama, satu-satunya cara memastikan resi barusan benar-benar
   masuk adalah MENGGULIR ke bawah -- dengan tangan yang sedang memegang
   scanner dan paket. Lalu menggulir balik untuk menembak berikutnya. Ratusan
   kali.

   Konsol ini menaruh keduanya BERDAMPINGAN: yang dikerjakan di kiri, yang
   sudah masuk di kanan, sejajar mata, tanpa satu pun gulir. Konfirmasi
   muncul di ujung penglihatan pada saat yang sama dengan bunyi scanner.

   Kedua kolom sengaja SAMA TINGGI (stretch bawaan grid) -- itu yang membuat
   keduanya terbaca sebagai satu perkakas, bukan dua kartu yang kebetulan
   bersebelahan. Isi kolom kiri dipusatkan tegak supaya kotak input tetap di
   tengah walau riwayat di kanan sedang panjang.

   Ringkasan Per Kategori/Ekspedisi TIDAK ikut ke dalam konsol, dan itu
   disengaja: ia dibaca SESUDAH pekerjaan selesai, bukan sambil bekerja.
   Tempatnya di bawah, di luar area kerja. */
/* Panel ringkasan yang ditukar segmented di dalam .ringkas-scan.

   Namanya .ringkas-panel, BUKAN .tab-panel, dan itu wajib: gantiTab() di
   checkout.php menyasar SEMUA .tab-panel di halaman untuk menukar tab besar
   (Sore/Retur/Hitung Stok/COD). Panel bernama sama akan ikut disembunyikan
   tiap kali orang berpindah tab besar, lalu tidak pernah muncul lagi karena
   gantiTab tidak tahu harus menyalakannya kembali. */
.ringkas-panel { display: none; }
.ringkas-panel.aktif { display: block; }
.ringkas-meta { margin-bottom: 12px; }

/* TABEL RIWAYAT DIGULIR DI DALAM KARTUNYA, DI LEBAR BERAPA PUN.

   Bukan bagian dari konsol -- karena itu di luar media query di bawah. Ia
   memperbaiki hal yang sudah salah sejak sebelum konsol ada: di lebar
   sekitar 769-1000px, .hide-mobile belum menyala sehingga kelima kolom
   tampil, dan tabel yang menuntut 863px berdiri di kartu selebar ~600px.
   Karena .d-tabel memakai `white-space: nowrap`, tabelnya tidak mengalah --
   yang mengalah HALAMANNYA, dan seluruh layar bisa digeser menyamping.
   Terukur di 769px: document.scrollWidth melebihi clientWidth.

   Aturan keras di berkas ini menyebutnya sejak awal (lihat .d-card-scroll):
   yang boleh bergulir mendatar adalah KARTUNYA, halaman tidak boleh pernah
   bergeser. Ini menerapkannya pada kartu yang terlewat. */
.card-riwayat-terkonfirmasi > div:not(.d-card-h2) { overflow-x: auto; }

.scan-konsol {
    display: grid;
    /* DASARNYA SATU KOLOM; dua kolom dinyalakan media query di bawah.

       Arahnya sengaja begini -- bukan "dua kolom lalu dibatalkan di layar
       sempit". Versi pertama memakai dua ambang yang saling melengkapi
       (max-width:1000px untuk melebur kolom, min-width:1001px untuk
       menyembunyikan kolom tabel), dan di viewport tepat 1001px KEDUANYA
       padam: konsol masih dua kolom sementara kolom tabel tidak
       disembunyikan, jadi tabelnya menggulir menyamping. Celah satu piksel
       yang cuma muncul di satu lebar tertentu adalah bug yang tidak akan
       pernah dilaporkan siapa pun dengan benar.

       Dengan satu ambang saja, celah itu mustahil ada: yang di dalamnya
       menyala bersama, yang di luar padam bersama.

       `minmax(0, 1fr)`, BUKAN `1fr` -- dan bedanya bukan gaya penulisan.
       `1fr` sebenarnya `minmax(auto, 1fr)`, dan `auto` di sana berarti
       "sekurang-kurangnya selebar min-content isinya". Kartu riwayat berisi
       tabel ber-`white-space: nowrap` yang min-content-nya 869px, jadi di
       viewport 759px track-nya MELAR jadi 917px dan menggeser seluruh
       halaman menyamping -- terukur, bukan dugaan. Selama track ikut melar,
       `overflow-x` pada pembungkus daftar di atas tidak akan pernah kena
       giliran, karena tidak ada yang perlu digulir: kartunya sendiri yang
       kelebaran. minmax(0,...) memaksa minimumnya nol, kartunya kembali
       selebar kolom, dan gulirnya kembali ke tempat yang benar. */
    grid-template-columns: minmax(0, 1fr);
    gap: 14px;
    align-items: stretch;
}

@media (min-width: 1001px) {
    /* =====================================================================
       KONSOL SCAN -- halaman ini berhenti jadi dokumen, jadi tempat kerja
       =====================================================================
       Sebelumnya tab Sore adalah TUMPUKAN VERTIKAL: kotak scan, lalu
       ringkasan, lalu riwayat. Susunan itu masuk akal untuk halaman yang
       DIBACA dari atas ke bawah. Halaman ini tidak dibaca -- ia dipakai.
       Operator berdiri dengan scanner di tangan dan mengulang satu tindakan
       ratusan kali sepanjang sore.

       Akibat susunan lama, satu-satunya cara memastikan resi barusan
       benar-benar masuk adalah MENGGULIR ke bawah -- dengan tangan yang
       sedang memegang scanner dan paket -- lalu menggulir balik untuk
       menembak berikutnya. Ratusan kali.

       Konsol ini menaruh keduanya BERDAMPINGAN: yang dikerjakan di kiri,
       yang sudah masuk di kanan, sejajar mata, tanpa satu pun gulir.

       Kedua kolom sengaja SAMA TINGGI (stretch) -- itu yang membuat keduanya
       terbaca sebagai satu perkakas, bukan dua kartu yang kebetulan
       bersebelahan. Isi kolom kiri dipusatkan tegak supaya kotak input tetap
       di tengah walau riwayat di kanan sedang panjang.

       Ringkasan Per Kategori/Ekspedisi TIDAK ikut ke dalam konsol: ia dibaca
       SESUDAH pekerjaan selesai, bukan sambil bekerja. Tempatnya di bawah,
       di luar area kerja. */
    .scan-konsol { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
    .scan-konsol > .scan-box { justify-content: center; padding: 34px 24px; }
    /* Kolom kanan diisi ringkasan yang bisa ditukar (Per Kategori / Per
       Ekspedisi), bukan riwayat.

       Riwayat pernah ditaruh di sini dan tidak muat: kelima kolomnya
       menuntut 869px di ruang ~495px, sehingga dua kolom (SKU & Pengirim)
       harus dilepas supaya tabelnya tidak digulir menyamping. Kompromi itu
       hilang begitu yang menempati kolom kanan diganti benda yang memang
       ramping. Bar horizontal lebarnya ikut wadahnya -- ia tidak punya
       lebar minimum seperti tabel -- jadi ia muat di ruang mana pun, dan
       riwayat turun ke bawah dengan lebar penuh beserta kelima kolomnya.

       Ditukar, bukan berdampingan: keduanya menjawab pertanyaan yang sama
       dari sumbu berbeda dan tidak pernah dibandingkan bersamaan. */
    .scan-konsol > .ringkas-scan { display: flex; flex-direction: column; }
    /* Bar mengisi sisa tinggi kartu supaya dasarnya rata dengan kolom kiri,
       bukan menggantung di tengah dengan ruang kosong di bawahnya. */
    .scan-konsol > .ringkas-scan .ringkas-panel.aktif { display: flex; flex-direction: column; flex: 1 1 auto; }
    .scan-konsol > .ringkas-scan .d-bars { flex: 1 1 auto; }

    /* ANGKA SISA: DARI PIL KECIL JADI JANTUNG HALAMAN.

       Ia satu-satunya angka yang berarti selama bekerja, dan ia menghitung
       MUNDUR -- nol berarti pekerjaan hari ini selesai. Sebagai pil 20px di
       dalam kartu, ia harus dicari dulu. Sebesar ini ia terbaca dari jarak
       berdiri, tanpa menoleh dari paket yang sedang dipegang. */
    .scan-konsol .scan-sisa {
        flex-direction: column;
        align-items: center;
        gap: 2px;
        background: none;
        padding: 0;
        margin-bottom: 24px;
        border-radius: 16px;
    }
    .scan-konsol .scan-sisa .num { font-size: 68px; line-height: 1; letter-spacing: -.04em; }
    .scan-konsol .scan-sisa .label { font-size: 13px; }
    /* Keadaan "masih ada antrean" ditandai WARNA ANGKANYA, bukan kapsul
       oranye. Aturan .scan-sisa.warn berdiri sesudah blok ini di berkas dan
       akan mengembalikan latar kapsulnya, jadi ia dibatalkan khusus di sini.
       Pada 68px, angka oranye jauh lebih keras berbicara daripada kotak
       oranye -- dan kotak sebesar itu justru menarik perhatian ke dirinya
       sendiri. */
    .scan-konsol .scan-sisa.warn { background: none; }
    .scan-konsol a.scan-sisa { padding: 10px 18px; }
    .scan-konsol a.scan-sisa:hover { background: var(--hair); transform: none; }
    /* Ikon barcode berdenyut dibuang HANYA kalau ada angka sisa di kartu
       itu -- karena alasan membuangnya memang "angka 68px sudah memegang
       peran 'di sini tempat scan'".

       Alasan itu tidak berlaku di tab Retur, yang tidak punya angka sisa
       sama sekali: dibuang tanpa syarat, kolom kirinya tinggal teks dan
       kotak input di tengah lapangan kosong, tanpa satu pun tanda bahwa di
       situlah scanner diarahkan. :has() membuat aturannya menyebutkan
       syaratnya sendiri, bukan mengandaikan semua kartu scan punya angka. */
    .scan-konsol .scan-box:has(.scan-sisa)::before { display: none; }
    /* Kolomnya separuh lebar; input 560px akan menempel ke dua tepi kartu. */
    .scan-konsol .scan-input { max-width: 440px; }
}

/* KOTAK SCAN ADALAH PUSAT HALAMAN, BUKAN KARTU PERTAMA.

   Halaman ini halaman KERJA: operator berdiri di depannya sepanjang sore
   menembak barcode, dan tiga kartu di bawahnya (ringkasan + riwayat) cuma
   dibaca sesekali untuk memastikan. Selama kotak scan memakai permukaan yang
   persis sama dengan ketiganya, hierarki itu tidak terbaca sama sekali --
   yang terlihat empat kartu bertumpuk, dan yang paling penting kebetulan
   ada di paling atas.

   Yang membedakannya sekarang: latar bertint aksen dan cincin aksen tipis,
   bukan kaca netral seperti kartu lain. Tint-nya sengaja sangat encer (5%)
   -- ia harus cukup untuk terbaca "ini permukaan yang berbeda", tapi tidak
   sampai mengganggu kotak input putih di tengahnya, yang justru harus jadi
   benda paling terang di layar. */
.scan-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: 40px 24px 34px;
    background:
        radial-gradient(120% 100% at 50% 0%,
            color-mix(in srgb, var(--primary) 7%, transparent),
            transparent 70%),
        var(--card);
    box-shadow:
        var(--d-kaca-cincin),
        inset 0 0 0 1px color-mix(in srgb, var(--primary) 12%, transparent),
        0 1px 1px rgba(0,0,0,.03),
        0 24px 50px -26px color-mix(in srgb, var(--primary) 40%, transparent);
}
/* Lencana ikon di atas judul -- digambar CSS, bukan <img>, supaya ikut
   warna aksen yang dipilih user. */
.scan-box::before {
    content: "";
    width: 52px;
    height: 52px;
    border-radius: 15px;
    margin-bottom: 16px;
    animation: d-pulseRing 2.4s ease-out infinite;
    background: color-mix(in srgb, var(--primary) 12%, transparent);
    background-image: linear-gradient(var(--primary), var(--primary)),
                      linear-gradient(var(--primary), var(--primary)),
                      linear-gradient(var(--primary), var(--primary)),
                      linear-gradient(var(--primary), var(--primary));
    background-size: 2px 18px, 2px 18px, 2px 18px, 2px 18px;
    background-position: 18px 17px, 23px 17px, 29px 17px, 34px 17px;
    background-repeat: no-repeat;
}
.scan-box > label {
    font-size: 19px;
    font-weight: 700;
    letter-spacing: -.02em;
    margin-bottom: 6px;
}
.scan-box .scan-hint { max-width: 460px; margin: 0 0 18px; }

.scan-input {
    width: 100%;
    /* 560px, naik dari 500px. Ia benda yang dituju seluruh halaman; di
       kartu selebar 1180px, 500px membuatnya tampak malu-malu di tengah
       lapangan kosong. */
    max-width: 560px;
    padding: 18px 20px;
    border-radius: 16px;
    border: 1px solid var(--border);
    background: var(--card-solid);
    color: var(--text);
    font-size: 19px;
    font-weight: 600;
    text-align: center;
    letter-spacing: .01em;
    transition: border-color .25s, box-shadow .25s;
}
.scan-input::placeholder { color: var(--faint); font-weight: 500; }
/* Tombol di bawah kotak input scan ("Proses", "Scan dengan Kamera HP"). */
.scan-aksi {
    display: flex;
    gap: 9px;
    flex-wrap: wrap;
    justify-content: center;
    margin-top: 14px;
}
.scan-input:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary) 15%, transparent);
}

/* Angka sisa antrean di dalam kartu scan. Class .stat-box dipertahankan --
   JS (muatUlangTerkonfirmasi) memakai closest('.stat-box') untuk menyalakan
   penanda oranye saat masih ada antrean. */
.scan-sisa {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 18px;
    padding: 8px 16px;
    border-radius: 999px;
    background: var(--field);
}
.scan-sisa .num { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
.scan-sisa .label { font-size: 12.5px; color: var(--muted); }
/* Untuk admin pil ini menuju halaman Barang Keluar (daftar lengkapnya).
   Staff merendernya sebagai <div>, jadi aturan hover di bawah tidak kena. */
a.scan-sisa { text-decoration: none; color: inherit; transition: background .25s, transform .25s; }
a.scan-sisa:hover { background: var(--hair); transform: translateY(-1px); text-decoration: none; }
a.scan-sisa:active { transform: scale(.98); }
.scan-sisa.warn {
    background: color-mix(in srgb, var(--orange) 14%, transparent);
}
.scan-sisa.warn .num { color: var(--oranye-teks); }

/* Ringkasan total di kaki form COD. */
.cod-total {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 14px;
    margin-top: 18px;
    padding: 14px 18px;
    border-radius: 14px;
    background: color-mix(in srgb, var(--green) 9%, transparent);
}
.cod-total > span { font-size: 13px; color: var(--muted); }
.cod-total > strong {
    font-size: 21px;
    font-weight: 700;
    letter-spacing: -.02em;
    color: var(--green);
    font-variant-numeric: tabular-nums;
}

.d-search { position: relative; }
.d-search-kotak {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 16px;
    border-radius: 14px;
    background: var(--card-solid);
    border: 1px solid var(--border);
    transition: border-color .25s, box-shadow .25s;
}
.d-search-kotak:focus-within {
    border-color: var(--primary);
    box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--primary) 16%, transparent);
}
.d-search-kotak svg { width: 17px; height: 17px; flex: 0 0 17px; color: var(--faint); }
/* Tombol kosongkan, muncul hanya saat kotaknya berisi (komponen SearchBox
   handoff). Dipasang oleh assets/app.js supaya berlaku di semua kotak cari
   sekaligus, bukan ditulis ulang di tiap halaman. */
.d-search-kosongkan {
    flex: 0 0 22px;
    width: 22px;
    height: 22px;
    border: none;
    border-radius: 50%;
    background: var(--field);
    color: var(--muted);
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
    /* TRANSITION, bukan animation. `animation` pada elemen yang lahir
       display:none tidak pernah berjalan -- animasinya dibatalkan selagi
       tersembunyi, dan menyalakan display kembali tidak memulainya lagi
       (terukur: 0 animasi aktif, transform langsung `none`). Transisi juga
       memberi gerakan DUA arah: mengecil lagi saat kotaknya dikosongkan,
       bukan lenyap seketika. */
    transform: scale(.5);
    opacity: 0;
    visibility: hidden;
    transition: transform .26s var(--d-ease-pop), opacity .2s ease,
                visibility .26s, background .2s, color .2s;
}
/* Muncul begitu kotaknya berisi. :placeholder-shown benar TEPAT saat input
   tidak berisi apa pun -- termasuk ketika nilainya disetel dari kode, yang
   tidak memicu event 'input'. */
.d-search-kotak input:not(:placeholder-shown) ~ .d-search-kosongkan {
    transform: scale(1);
    opacity: 1;
    visibility: visible;
}
/* Hover harus SELEKTIF SETARA aturan di atas, kalau tidak scale(1)-nya
   menang dan tombolnya tidak bereaksi disentuh kursor. */
.d-search-kotak input:not(:placeholder-shown) ~ .d-search-kosongkan:hover {
    transform: scale(1.14);
    background: var(--hair);
    color: var(--text);
}
.d-search-kotak input:not(:placeholder-shown) ~ .d-search-kosongkan:active {
    transform: scale(.92);
}
.d-search-kotak input { flex: 1; border: none; background: none; color: var(--text); font-size: 14.5px; min-width: 0; }

/* KOTAK CARI TABEL (SOP tabel pemilik, 15 Sep 2026). Kecil, bukan selebar
   halaman: duduk di ujung kanan bar pill / periode, atau sendirian di atas
   tabel (d_table() memasangnya). Di HP ia mengambil satu baris penuh. */
.d-table-alat   { display: flex; margin: 0 0 10px; }
.d-alat-bar     { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; margin: 0 0 14px; }
.d-alat-bar > .d-segmen { margin-bottom: 0; }
.d-cari-tabel   { flex: 0 1 260px; min-width: 180px; margin-left: auto; }
.d-cari-tabel .d-search-kotak { padding: 7px 12px; gap: 8px; border-radius: 12px; }
.d-cari-tabel .d-search-kotak svg { width: 15px; height: 15px; flex-basis: 15px; }
.d-cari-tabel .d-search-kotak input { font-size: 13.5px; outline: none; }
/* Tombol x bawaan peramban dimatikan: kotak cari sudah punya tombol
   kosongkannya sendiri (app.js), dua tombol yang sama membingungkan. */
.d-cari-tabel input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
@media (max-width: 560px) {
    .d-cari-tabel { flex: 1 1 100%; min-width: 0; margin-left: 0; }
}
/* Panel saran HANYA saat ada teks -- kalau selalu tampil, panelnya
   menutupi daftar di bawahnya (catatan SearchBox di handoff). */
.d-search-saran {
    position: absolute;
    top: calc(100% + 8px);
    left: 0;
    right: 0;
    z-index: 20;
    padding: 6px;
    border-radius: 16px;
    background: var(--card-solid);
    border: 1px solid var(--border);
    box-shadow: 0 24px 50px -18px rgba(0,0,0,.4);
    animation: d-fadeUp .28s var(--d-ease-out) backwards;
}
.d-search-saran button {
    display: flex;
    width: 100%;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 12px;
    border: none;
    background: none;
    border-radius: 11px;
    cursor: pointer;
    text-align: left;
    color: var(--text);
    font-size: 13.5px;
    transition: background .2s;
}
.d-search-saran button:hover { background: var(--field); }
.d-search-saran b { font-weight: 600; }
.d-search-saran i { font-style: normal; font-size: 12px; color: var(--faint); }

/* -------------------------------------------------------------------------
   16. SHEET (modal tengah), TOAST, KABAR
   Handoff menyebutnya "bottom sheet" tapi prototipe & screenshot desktop
   sama-sama menengahkannya (max-width 620px, radius 26px penuh). Yang
   dipakai di sini adalah prototipe.
   ------------------------------------------------------------------------- */
.d-sheet-backdrop {
    position: fixed;
    inset: 0;
    z-index: 70;
    background: rgba(0,0,0,.34);
    animation: d-backdropIn .28s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 40px;
}
.d-sheet {
    width: 100%;
    max-width: 620px;
    max-height: 84vh;
    overflow-y: auto;
    background: var(--card-solid);
    /* TANPA GARIS TEPI, TIMBUL LEWAT BAYANGAN (pemilik 15 Sep 2026: "outline-nya
       seperti berantakan"). Garis 1px di layar berskala 125% jatuh jadi 0,8px,
       dan di lengkung radius 26px ia tergambar tebal-tipis; kepala sheet yang
       sticky (latar & radius sendiri) menutupi sebagian garis di sudut atas,
       jadi sudutnya terlihat compang. Tanpa garis tidak ada yang bisa meleset:
       batas sheet dibentuk dua lapis bayangan -- satu lebar & lembut untuk
       kesan melayang, satu rapat di bawahnya supaya tepinya tetap tegas di atas
       latar gelap. */
    border: none;
    border-radius: 26px;
    box-shadow: 0 32px 80px -16px rgba(0,0,0,.55),
                0 12px 28px -10px rgba(0,0,0,.35);
    animation: d-sheetIn .5s cubic-bezier(.2,.95,.25,1.05);
}
/* CINCIN FOKUS BAWAAN PERAMBAN DIMATIKAN DI BADAN SHEET. Uwms.sheet() (app.js)
   memindahkan fokus ke sheet itu sendiri (tabindex=-1) kalau tidak ada tombol
   utama -- supaya pembaca layar & Tab mulai dari dalam sheet. Peramban lalu
   menggambar cincin hitam 2px mengikuti radius 26px: itulah "outline" tebal
   yang dilihat pemilik (15 Sep, mode terang, zoom 100%), bukan border yang
   sudah dibuang. Badan sheet bukan kontrol; tombol & isian di dalamnya tetap
   punya penanda fokusnya sendiri. */
.d-sheet:focus,
.d-sheet:focus-visible { outline: none; }
.d-sheet-head {
    position: sticky;
    top: 0;
    background: var(--card-solid);
    padding: 22px 26px 14px;
    border-bottom: 1px solid var(--hair);
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px;
    border-radius: 26px 26px 0 0;
    z-index: 1;
}
.d-sheet-judul { font-size: 19px; font-weight: 700; letter-spacing: -.02em; }
.d-sheet-sub   { font-size: 13px; color: var(--muted); margin-top: 3px; }
.d-sheet-tutup {
    flex: 0 0 30px;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    border: none;
    cursor: pointer;
    background: var(--field);
    color: var(--muted);
    font-size: 15px;
    line-height: 1;
    transition: transform .25s, background .25s;
}
.d-sheet-tutup:hover { transform: rotate(90deg); }
.d-sheet-isi { padding: 20px 26px 26px; }
/* flex-wrap WAJIB. Tanpa itu, begitu deretan tombol tidak muat, `nowrap` +
   `justify-content: flex-end` membuat kelebihannya meluber ke arah AWAL --
   yaitu KELUAR lewat tepi KIRI sheet, bukan ke kanan seperti yang orang duga.
   Terukur di sheet Edit Produk (19 Agu 2026): 5 tombol butuh 536px di ruang
   497px, dan tombol "Hapus" (yang ber-margin-right:auto) berdiri 12px di luar
   badan sheet. Gejalanya bikin bingung karena tombol yang keluar justru yang
   PALING KIRI, sementara yang kepanjangan ada di kanan.
   align-items:center menjaga tinggi tombol tetap sejajar begitu barisnya
   pecah jadi dua. */
.d-sheet-aksi {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 9px;
    justify-content: flex-end;
    margin-top: 22px;
}

/* Penampil foto bukti (Uwms.lihatFoto). Foto di TENGAH bidangnya, utuh
   (contain, bukan cover -- yang dinilai isinya, tidak boleh terpotong), dan
   tingginya dibatasi supaya foto HP yang tegak tidak memaksa sheet bergulir. */
.d-foto-lihat {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 220px;
    background: var(--field);
    border-radius: 14px;
    overflow: hidden;
}
.d-foto-lihat img {
    display: block;
    max-width: 100%;
    max-height: calc(84vh - 150px);
    object-fit: contain;
}

.d-toast {
    position: fixed;
    /* Dipusatkan lewat margin auto, BUKAN left:50% + translateX(-50%).
       Dengan cara lama, animasi masuk d-toastIn ikut menulis transform dan
       menimpa translateX(-50%) selama animasi berjalan -- toast muncul
       serong ke kanan (tepi kirinya di tengah layar) lalu baru melompat ke
       tengah begitu animasinya habis. Pemusatan sekarang tidak lagi
       bergantung pada transform, jadi ia lahir tepat di tengah. */
    left: 0;
    right: 0;
    margin-inline: auto;
    width: fit-content;
    max-width: calc(100% - 32px);
    bottom: 34px;
    /* 80 = di atas backdrop sheet (70), di bawah dropdown/popover/kalender
       (90). Ini nilai DESKTOP dan sengaja dibiarkan apa adanya; yang perlu
       lebih tinggi cuma HP, lihat blok @media di bawah. */
    z-index: 80;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 13px 20px;
    border-radius: 16px;
    background: rgba(28,28,30,.99);
    color: #fff;
    font-size: 14px;
    font-weight: 600;
    box-shadow: 0 18px 50px -12px rgba(0,0,0,.5);
    animation: d-toastIn .5s var(--d-ease-pop);
}
.d-toast-ikon {
    width: 19px;
    height: 19px;
    border-radius: 50%;
    background: var(--d-toast-warna, var(--green));
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 19px;
}
.d-toast-ikon svg { width: 11px; height: 11px; }

/* =========================================================================
   KAPSUL PULAU
   =========================================================================
   SALINAN LANGSUNG dari .kapsul milik aplikasi sekolah (kerangka.css-nya),
   bukan tiruan hasil mengukur tangkapan layar. Versi sebelumnya di sini
   memang hasil mengukur gambar, dan meleset di hampir semua angka: lebarnya
   400 bukan 470, barisnya dipatok 54px padahal di sana barisnya membagi rata
   sisa tinggi, gilirannya memudar-silang padahal di sana bergeser, dan
   nada kabarnya cuma dua padahal di sana empat.

   TIGA HAL YANG SENGAJA BERBEDA, dan cuma tiga:

   1. top: 30px, bukan 26px. Komentar mereka sendiri menyebut 26 itu padding
      atas .isi milik mereka -- angkanya ada supaya kapsul berdiri persis di
      tempatnya sebelum halaman digulir. Padding atas .d-page kita 30px.
      Menyalin 26 justru MELANGGAR aturannya, bukan menaatinya.

   2. Tinggi mekarnya ikut jumlah baris. Mereka mematoknya 262px karena
      daftarnya selalu tiga. Baris di sana memakai flex:1 -- membagi rata
      sisa tinggi -- jadi 262px dengan DUA baris menghasilkan baris setinggi
      101px, dan itu justru tidak mirip lagi dengan gambarnya. Rumus di
      bawah menjaga TINGGI BARISNYA tetap sama seperti di sana (63,33px),
      yang memang yang terlihat.

   (Dulu ada butir ketiga: kapsul sunyi disembunyikan sampai ada kabar.
   DIBATALKAN 7 Sep 2026 -- lihat catatan di .d-kapsul-hanya-kabar.)
   ========================================================================= */
/* UKURANNYA DIKECILKAN DARI ASLINYA, dan alasannya bukan selera.

   Aslinya 470x46. Angka itu benar DI SANA, dan salah di sini, karena yang
   menentukan besar-kecilnya bukan angkanya melainkan kolom yang ditumpanginya:

     .isi mereka  flex:1 tanpa batas -- di jendela 1536px kolomnya ~1384px
     .d-page kita max-width 1180px   -- kolomnya 1100px

   Kapsul yang sama 470px berarti 34% lebar kolom di sana, tapi 43% di sini.
   Menyalin angkanya mentah-mentah justru membuatnya TIDAK sama -- ia tampil
   sepertiga lebih besar terhadap segala yang ada di sekitarnya.

   380px mengembalikan perbandingan itu ke 34,5%, praktis sama dengan mereka.
   Tingginya ikut turun 46->42 supaya bentuk pilnya tidak jadi gempal, dan
   HURUFNYA TIDAK IKUT DIKECILKAN: 13px sudah kecil, dan kapsul yang
   menyusut sambil membawa huruf 11px berhenti bisa dibaca sekilas -- padahal
   dibaca sekilas satu-satunya gunanya. */
:root {
    --d-kapsul-l: 380px;
    --d-kapsul-t: 42px;
    /* --ez dan --ez2 milik mereka. --d-ease-rail kita kebetulan sama persis
       dengan --ez (cubic-bezier(.32,.72,0,1)), jadi yang perlu ditambahkan
       cuma --ez2. */
    --d-kapsul-ez2: cubic-bezier(.22,1,.36,1);
}

.d-kapsul {
    position: fixed;
    /* 85 = di atas toast (80), di bawah dropdown/popover/kalender (90).
       Mereka memakai 60 karena tumpukan mereka berbeda; yang penting sama:
       di ATAS blok judul, karena kapsul yang memuai menutupi judul. */
    z-index: 85;
    /* 30px = padding-atas .d-page. Lihat butir 1 di atas. */
    top: 30px;
    left: 80px;
    right: 0;
    margin: 0 auto;
    width: var(--d-kapsul-l);
    height: var(--d-kapsul-t);
    box-sizing: border-box;
    padding: 0 16px;
    border-radius: 21px;
    background: #0b0b0c;
    color: #fff;
    /* Bayangannya DIKEMBALIKAN. Ia sempat dibuang di sini waktu diminta
       "tanpa outline, blended ke sekitarnya" -- dan yang dimaksud memang
       outline, sedangkan ini bayangan jatuh. Aslinya punya bayangan, dan
       permintaan terakhir "persis sekali" mengalahkan tafsiran itu. */
    box-shadow: 0 14px 34px -14px rgba(16,24,40,.5);
    overflow: hidden;
    cursor: pointer;
    transition:
        width .55s var(--d-ease-rail),
        height .55s var(--d-ease-rail),
        border-radius .55s var(--d-ease-rail),
        padding .55s var(--d-ease-rail);
}

/* Memuai ke bawah saja -- lebarnya tetap, hanya tingginya tumbuh.

   Rumusnya DITURUNKAN dari tinggi terukur, bukan dari angka yang diinginkan.
   Yang tetap: 24 padding + 13,6 label + 18 jarak label->baris (margin 9 DAN
   sela flex 9, keduanya berlaku) = 55,6; tiap baris menambah 52 + 9 sela,
   jadi 46,6 + 61n.

     n=3 -> 229,6px     (aslinya 262px, ikut turun bersama lebarnya)
     n=2 -> 168,6px
     n=1 -> 107,6px

   Baris 52px, bukan 60,14px seperti aslinya. Ia ikut turun dengan faktor
   yang sama seperti lebarnya (0,81) supaya panel yang terbuka tidak jadi
   jangkung: kapsul yang menyempit 19% tapi barisnya tetap akan terbaca
   sebagai kotak sempit dan tinggi, bukan sebagai kapsul yang sama.

   Catatan cara menurunkannya, karena mudah salah: jarak label ke baris
   pertama dihitung DUA KALI -- margin-bottom label DAN sela flex keduanya
   berlaku. Percobaan sebelumnya menghitungnya sekali dan meleset 1,6px. */
.d-kapsul:hover,
.d-kapsul:has(:focus-visible) {
    height: calc(46.6px + var(--d-kapsul-n, 1) * 61px);
    border-radius: 26px;
    padding: 12px 18px;
}

/* Dua isi bertumpuk di ruang yang sama, saling menyilang. Bukan satu isi yang
   tumbuh: kapsul memuai bersamaan dengan pergantian isinya, dan isi yang ikut
   mengubah tinggi akan membuat gerakannya tersentak di tengah. */
.d-kapsul-ringkas,
.d-kapsul-penuh {
    position: absolute;
    inset: 0;
    padding: inherit;
    box-sizing: border-box;
    transition: opacity .28s var(--d-kapsul-ez2);
}

.d-kapsul-ringkas {
    display: flex;
    align-items: center;
    gap: 12px;
    height: var(--d-kapsul-t);
    padding: 0 16px;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: -.015em;
    opacity: 1;
}

.d-kapsul:hover .d-kapsul-ringkas,
.d-kapsul:has(:focus-visible) .d-kapsul-ringkas {
    opacity: 0;
    pointer-events: none;
}

.d-kapsul-penuh {
    display: flex;
    flex-direction: column;
    gap: 9px;
    padding: 12px 18px;
    opacity: 0;
    pointer-events: none;
}

.d-kapsul:hover .d-kapsul-penuh,
.d-kapsul:has(:focus-visible) .d-kapsul-penuh {
    opacity: 1;
    pointer-events: auto;
}

.d-kapsul-label {
    margin: 0 0 9px;
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: rgba(255,255,255,.45);
}

/* flex:1 -- ketiga baris membagi rata sisa tinggi kapsul. Tanpa itu masing-
   masing memakai tinggi isinya sendiri dan menyisakan ruang mati di dasar
   kapsul, yang di atas latar hitam justru paling kelihatan. */
.d-kapsul-tindakan {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    box-sizing: border-box;
    padding: 9px 11px;
    border-radius: 14px;
    color: #fff;
    /* 13px, SAMA PERSIS dengan .d-kapsul-ringkas -- dan ini satu-satunya
       angka huruf yang sengaja beda dari aslinya.

       Di sana pita ringkasnya 13px dan baris tindakannya 13,5px, jadi
       hurufnya memang membesar 0,5px begitu kapsulnya dibuka. Sudah dicek
       langsung di kerangka.css mereka, bukan dikira-kira.

       Beda setengah piksel itu tidak kelihatan di kotak selebar 470px milik
       mereka, dan kelihatan di kotak 380px kita: barisnya lebih rapat (322px
       ruang dalam, bukan 402px), jadi huruf yang sama mengisi bidang yang
       lebih sempit dan pertambahannya jadi terbaca. Yang terlihat bukan
       hierarki melainkan goyangan -- dan 0,5px terlalu kecil untuk pernah
       dimaksudkan sebagai hierarki.

       Disamakan ke 13px, bukan dinaikkan ke 13,5px: keadaan RINGKAS yang
       paling lama tampil di layar, jadi itu yang jadi patokan. Dengan begini
       tidak ada satu pun huruf yang berubah ukuran saat kapsulnya membuka. */
    font-size: 13px;
    font-weight: 700;
    letter-spacing: -.015em;
    text-decoration: none;
    background: rgba(255,255,255,.06);
    transition:
        background .3s var(--d-ease-rail),
        transform .3s var(--d-ease-rail);
}
/* text-decoration DIULANG DI SINI, dan itu wajib, bukan berlebihan.

   Aturan dasar `.d-kapsul-tindakan { text-decoration: none }` bobotnya
   (0,1,0), sedangkan `a:hover` global di bagian dasar berkekuatan (0,1,1) --
   jadi begitu kursor menyentuhnya, garis bawah global itu MENANG dan baris
   di dalam kapsul tampil bergaris bawah. Baris ini sudah punya kotaknya
   sendiri yang menyala dan bergeser 3px saat disentuh; garis bawah di
   atasnya cuma membuatnya terlihat seperti tautan teks biasa yang kebetulan
   berlatar. */
.d-kapsul-tindakan:hover {
    background: rgba(255,255,255,.14);
    transform: translateX(3px);
    color: #fff;
    text-decoration: none;
}
.d-kapsul-tindakan > span:first-child {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.d-kapsul-jumlah {
    flex: 0 0 auto;
    font-variant-numeric: tabular-nums;
    color: rgba(255,255,255,.62);
    font-size: 12px;
    font-weight: 700;
}
.d-kapsul-tenang {
    font-size: 13px;
    font-weight: 700;
    color: rgba(255,255,255,.72);
}

/* Titik berdenyut: titik padat, plus cincin yang melebar lalu hilang. */
.d-kapsul-denyut {
    position: relative;
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #34d399;
}
.d-kapsul-denyut::after {
    content: '';
    position: absolute;
    inset: -3px;
    border-radius: 50%;
    border: 1.5px solid #34d399;
    animation: d-cincin-denyut 2.4s ease-out infinite;
}
@keyframes d-cincin-denyut {
    0%   { transform: scale(.85); opacity: .55; }
    100% { transform: scale(1.6); opacity: 0; }
}

/* =========================================================================
   PITA BERGILIR -- satu hal pada satu waktu.
   =========================================================================
   DIGESER dengan transform, bukan diganti isinya. Mengganti isi membuat
   kalimat lama lenyap dan kalimat baru muncul di tempat yang sama -- terbaca
   sebagai kedipan. Bergeser terbaca sebagai "ada yang lain di sebelah", yang
   memang benar. (Versi kita sebelumnya memudar-silang; itu keliru.) */
.d-kapsul-jendela {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
}
.d-kapsul-rel {
    display: flex;
    transition: transform .62s var(--d-ease-rail);
}
/* flex: 0 0 100% terhadap JENDELA, bukan terhadap isinya. Tiap hal menempati
   persis satu lebar pita, jadi pergeseran satu langkah selalu 100% -- tidak
   perlu mengukur apa pun di peramban. */
.d-kapsul-slide {
    flex: 0 0 100%;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.d-kapsul-tunggal {
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Noktah urutan. Tanpa ini, yang kebetulan tidak sedang melihat saat pitanya
   bergeser tidak punya petunjuk bahwa ada hal lain di belakangnya -- dan
   tidak akan pernah membuka kapsulnya untuk mencari. */
.d-kapsul-urut {
    display: flex;
    flex: 0 0 auto;
    gap: 4px;
}
.d-kapsul-noktah {
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: rgba(255,255,255,.26);
    transition: background .45s var(--d-ease-rail);
}
.d-kapsul-noktah-kini { background: rgba(255,255,255,.9); }

/* =========================================================================
   LAPISAN KABAR -- seluruh pemberitahuan sesaat aplikasi lewat sini.
   =========================================================================
   Lapisan KETIGA di ruang yang sama dengan ringkas dan penuh, bukan benda
   melayang di pojok layar. Sebabnya: mata orang sudah tahu kapsul ini tempat
   aplikasi berbicara -- di situlah angka hari ini berdiri di setiap halaman.
   Kabar di pojok kanan bawah menuntut mata mencarinya lebih dulu. */
.d-kapsul-kabar {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 0 16px;
    box-sizing: border-box;
    opacity: 0;
    pointer-events: none;
    /* .42s, dan angkanya BUKAN selera: ia harus sama dengan KABAR_REDUP di
       app.js, yang menahan kalimatnya tetap tergambar selama redup ini
       berjalan. Menaikkan salah satunya saja mengembalikan kedipan yang
       justru diperbaiki di sini. */
    transition: opacity .42s var(--d-kapsul-ez2);
}
.d-kapsul[data-kabar] .d-kapsul-kabar { opacity: 1; }
/* Ringkasan mundur selama ada kabar -- keduanya menempati baris yang sama,
   dan perpindahannya harus selambat kabarnya supaya keduanya benar-benar
   bersilangan, bukan saling menyusul. */
.d-kapsul[data-kabar] .d-kapsul-ringkas {
    opacity: 0;
    transition-duration: .42s;
}
.d-kapsul:hover .d-kapsul-kabar,
.d-kapsul:has(:focus-visible) .d-kapsul-kabar { opacity: 0; }

/* Warna hanya di titiknya, tidak di seluruh kapsul. Kapsul hitam yang berubah
   jadi merah penuh menarik perhatian jauh melebihi bobot kabarnya -- dan yang
   paling sering lewat justru kabar "tersimpan" yang tidak menuntut apa-apa. */
.d-kapsul-titik-kabar {
    position: relative;
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--d-kabar-warna, #34d399);
    box-shadow: 0 0 9px -1px var(--d-kabar-warna, #34d399);
    animation: d-kabar-titik-masuk .55s var(--d-kapsul-ez2) .06s both;
}
.d-kapsul-kabar-teks {
    min-width: 0;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: -.015em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    animation: d-kabar-teks-masuk .5s var(--d-kapsul-ez2) .1s both;
}

/* KAPSUL MEMUAI UNTUK KALIMAT YANG TIDAK MUAT SATU BARIS.

   Satu baris di lebar 380px memuat sekitar empat puluhan huruf (aslinya 470px
   dan lima puluhan; ambangnya di app.js ikut turun). Kalimat yang
   lebih panjang dipotong titik-titik -- dan yang terpotong justru bagian yang
   paling sering menentukan artinya: jumlahnya, atau kata "belum". Jadi
   kapsulnya yang mengalah, bukan kalimatnya.

   Dua baris adalah batasnya. Yang butuh lebih dari itu bukan kabar sesaat
   melainkan keterangan, dan keterangan tempatnya di halaman. */
.d-kapsul[data-panjang] {
    height: 62px;
    border-radius: 24px;
}
.d-kapsul[data-panjang] .d-kapsul-kabar-teks {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    white-space: normal;
    line-height: 1.35;
}

.d-kapsul[data-kabar="ok"]    { --d-kabar-warna: #34d399; }
.d-kapsul[data-kabar="ingat"] { --d-kabar-warna: #fbbf24; }
.d-kapsul[data-kabar="gagal"] { --d-kabar-warna: #fb7185; }
.d-kapsul[data-kabar="kabar"] { --d-kabar-warna: #60a5fa; }

/* ZOOM & GOYANG -- permintaan pemilik 13 Sep 2026 ("sedikit zoom dan
   bergoyang smooth"). Nada yang memilih gerakannya, lihat gerakKapsul() di
   app.js: gagal saja yang bergoyang.

   Memakai properti `scale` dan `transform` TERPISAH, bukan satu transform
   berisi keduanya: di bawah 1200px kapsulnya meluncur turun lewat properti
   `translate`, dan animasi yang menulis `transform: scale()` akan menimpa
   luncuran itu -- kapsulnya melompat ke posisi akhir di tengah animasi.
   Tiga properti terpisah disusun peramban tanpa saling menimpa.

   Besarnya sengaja kecil, 7%. Kapsul hitam di kepala SETIAP halaman yang
   membesar lebih dari itu mulai menutupi judul di sebelahnya. */
@keyframes d-kapsul-pop {
    0%   { scale: 1; }
    34%  { scale: 1.07; }
    60%  { scale: .985; }
    80%  { scale: 1.01; }
    100% { scale: 1; }
}
@keyframes d-kapsul-goyang {
    0%, 100% { transform: translateX(0); }
    14% { transform: translateX(-9px); }
    28% { transform: translateX(8px); }
    43% { transform: translateX(-6px); }
    58% { transform: translateX(4px); }
    74% { transform: translateX(-2px); }
    88% { transform: translateX(1px); }
}
.d-kapsul[data-gerak="pop"] {
    animation: d-kapsul-pop .56s cubic-bezier(.33,1,.68,1);
}
.d-kapsul[data-gerak="goyang"] {
    animation:
        d-kapsul-pop .56s cubic-bezier(.33,1,.68,1),
        d-kapsul-goyang .62s cubic-bezier(.36,.07,.19,.97) .12s;
}

/* KABAR YANG TERPOTONG BISA DIKETUK UNTUK DIBACA UTUH.

   Island paling banyak dua baris, dan 18 dari 94 pesan gagal di aplikasi ini
   lebih panjang dari itu (terpanjang 219 huruf). Yang terpotong biasanya
   justru cara membetulkannya. Lihat pasangKetukKabar() di app.js. */
.d-kapsul[data-terpotong] { cursor: pointer; }

/* SELAMA ADA KABAR, kursor di atas kapsul TIDAK menukarnya dengan panel
   tindakan. Kursor yang datang saat kabar tampil datang untuk MEMBACA;
   panel yang menggantikannya tepat saat itu adalah kalimat yang hilang di
   bawah kursornya sendiri. Kabar yang terpotong malah memuai jadi utuh
   (pasangKetukKabar() di app.js). Panelnya bisa dibuka lagi begitu kabarnya
   pergi.

   KOREKSI 13 Sep 2026: versi pertama aturan ini hanya berlaku untuk kabar
   TERPOTONG, dengan alasan kabar pendek "tidak ada lagi yang perlu dibaca".
   Salah -- pemilik menemukan kabar satu baris LENYAP begitu disentuh. Di
   hari tenang (.d-kapsul-hanya-kabar) panelnya pun tidak memuai, jadi yang
   tersisa kapsul hitam kosong: kabarnya sudah disembunyikan, dan tidak ada
   apa pun yang menggantikannya. */
.d-kapsul[data-kabar]:hover,
.d-kapsul[data-kabar]:has(:focus-visible) {
    height: var(--d-kapsul-t);
    border-radius: 21px;
    padding: 0 16px;
}
.d-kapsul[data-kabar][data-panjang]:hover,
.d-kapsul[data-kabar][data-panjang]:has(:focus-visible) {
    height: 62px;
    border-radius: 24px;
}
.d-kapsul[data-kabar]:hover .d-kapsul-kabar,
.d-kapsul[data-kabar]:has(:focus-visible) .d-kapsul-kabar { opacity: 1; }
.d-kapsul[data-kabar] .d-kapsul-ringkas,
.d-kapsul[data-kabar] .d-kapsul-penuh,
.d-kapsul[data-kabar]:hover .d-kapsul-penuh,
.d-kapsul[data-kabar]:has(:focus-visible) .d-kapsul-penuh {
    opacity: 0;
    pointer-events: none;
}

.d-kapsul[data-utuh] { border-radius: 24px; max-height: 60vh; }
.d-kapsul[data-utuh] .d-kapsul-kabar {
    align-items: flex-start;
    padding-top: 13px;
    padding-bottom: 13px;
}
.d-kapsul[data-utuh] .d-kapsul-titik-kabar { margin-top: 5px; }
.d-kapsul[data-utuh] .d-kapsul-kabar-teks {
    display: block;
    -webkit-line-clamp: unset;
    white-space: normal;
    overflow: visible;
    line-height: 1.35;
}

/* Tanda bisa diketuk: panah turun selama terpotong, silang sesudah dibuka. */
.d-kapsul-kabar-ketuk {
    display: none;
    position: relative;
    flex: 0 0 auto;
    align-self: center;
    width: 16px;
    height: 16px;
    margin-left: auto;
    opacity: .55;
}
.d-kapsul[data-terpotong] .d-kapsul-kabar-ketuk { display: block; }
.d-kapsul[data-utuh] .d-kapsul-kabar-ketuk { align-self: flex-start; margin-top: 1px; }
.d-kapsul-kabar-ketuk::before,
.d-kapsul-kabar-ketuk::after {
    content: '';
    position: absolute;
    top: 50%;
    width: 7px;
    height: 1.8px;
    border-radius: 2px;
    background: #fff;
    transition: transform .3s var(--d-kapsul-ez2), left .3s var(--d-kapsul-ez2), width .3s var(--d-kapsul-ez2);
}
.d-kapsul-kabar-ketuk::before { left: 2.5px; transform: translateY(-50%) rotate(40deg); }
.d-kapsul-kabar-ketuk::after  { left: 6.5px; transform: translateY(-50%) rotate(-40deg); }
.d-kapsul[data-utuh] .d-kapsul-kabar-ketuk::before,
.d-kapsul[data-utuh] .d-kapsul-kabar-ketuk::after { left: 2.5px; width: 11px; }
.d-kapsul[data-utuh] .d-kapsul-kabar-ketuk::before { transform: translateY(-50%) rotate(45deg); }
.d-kapsul[data-utuh] .d-kapsul-kabar-ketuk::after  { transform: translateY(-50%) rotate(-45deg); }

/* GERAKAN MASUKNYA SENGAJA HAMPIR TIDAK TERASA.

   Versi pertama mereka melempar titiknya dari skala 0.2 dan menaikkan
   teksnya tujuh piksel. Di kotak setinggi 46px yang berdiri di kepala SETIAP
   halaman, itu terbaca sebagai sentakan -- dan yang tersentak justru kalimat
   yang sedang diminta dibaca.

   Yang tersisa: pudar, dengan pergeseran sekecil mungkin supaya arah
   datangnya masih terasa. Titik dan teks bergerak berurutan dengan jeda
   sangat pendek, jadi terbaca sebagai satu benda yang mengendap. */
@keyframes d-kabar-titik-masuk {
    from { transform: scale(.72); opacity: 0; }
    to   { transform: scale(1);   opacity: 1; }
}
@keyframes d-kabar-teks-masuk {
    from { transform: translateY(3px); opacity: 0; }
    to   { transform: translateY(0);   opacity: 1; }
}

/* HARI YANG TENANG: kapsulnya TETAP TERLIHAT, cuma tidak memuai.

   Sampai 7 Sep 2026 ia disembunyikan (opacity 0) sampai ada kabar masuk,
   dengan alasan kapsul hitam berisi "tidak ada apa-apa" memakan tempat paling
   menonjol di layar tanpa memberi apa pun. Alasan itu terdengar masuk akal
   dan salah di lapangan: begitu naik ke server produksi -- yang hari itu
   memang tidak punya satu pun resi atau restok tertunda -- yang terlihat
   pemilik adalah kapsul yang TIDAK ADA. Tidak ada cara membedakan "sudah
   terpasang dan sedang diam" dari "gagal terpasang", dan yang pertama
   dilaporkan sebagai yang kedua.

   Itu juga mengembalikan kita ke perilaku aslinya: di aplikasi sekolah
   .kapsul-hanya-kabar tetap tergambar, cuma tidak memuai. Kelas itu di sana
   dipakai untuk orang yang tidak berhak melihat angka sama sekali -- BUKAN
   untuk hari yang angkanya kebetulan nol.

   Tidak memuai saat disentuh, dan itu tetap dipertahankan: yang akan terbuka
   cuma panel kosong berisi basa-basi. Kalimat "tidak ada yang menunggu"
   sudah utuh di pita ringkasnya. */
.d-kapsul-hanya-kabar {
    cursor: default;
}
/* Menahan tinggi & sudutnya saat disentuh. Aturan :hover di atas ditulis
   dengan dua kelas (.d-kapsul:hover), jadi satu kelas di sini tidak cukup
   untuk mengalahkannya -- karena itu :hover ikut disebut. */
.d-kapsul-hanya-kabar:hover,
.d-kapsul-hanya-kabar:has(:focus-visible) {
    height: var(--d-kapsul-t);
    border-radius: 21px;
    padding: 0 16px;
}
/* Kalimat panjang tetap boleh memuainya jadi dua baris: itu soal kabar yang
   sedang lewat, bukan soal memuai jadi panel. */
.d-kapsul-hanya-kabar[data-panjang]:hover {
    height: 62px;
    border-radius: 24px;
}
/* Ringkasnya tetap terbaca saat disentuh -- di sini tidak ada lapisan penuh
   yang akan menggantikannya, jadi memudarkannya cuma mengosongkan kapsul.

   :not([data-kabar]) supaya ia TIDAK ikut campur saat ada kabar lewat: di
   keadaan itu yang harus terbaca kabarnya, dan aturan bawaan sudah
   mengurusnya. Tanpa penjagaan ini keduanya seri kekhususan dan yang menang
   cuma ditentukan urutan baris. */
.d-kapsul-hanya-kabar:not([data-kabar]):hover .d-kapsul-ringkas { opacity: 1; }
.d-kapsul-hanya-kabar .d-kapsul-penuh { display: none; }

/* Pemesan ruang di .d-head. Lebarnya persis lebar kapsul; tingginya nol
   supaya ia tidak ikut menentukan tinggi baris header. */
.d-kapsul-ruang {
    flex: 0 0 var(--d-kapsul-l);
    height: 0;
}

/* DI BAWAH 1200px: KAPSUL JADI PULAU YANG DATANG DAN PERGI.

   Pemesan ruangnya tetap pergi, dengan alasan lama: bukan kapsulnya yang
   tidak muat -- 380px selalu muat -- tapi JUDUL yang tidak muat di
   sebelahnya. Ruang baca (w - 80) dibagi tiga: kapsul di tengah, sisanya
   dibelah dua untuk judul dan tombol aksi.

   Yang berubah 13 Sep 2026: kapsulnya sendiri TIDAK ikut hilang lagi. Dulu
   display:none, dan akibatnya di iPad (1180px landscape) seluruh kabar
   aplikasi jatuh ke toast di dasar layar -- padahal pemilik memakai iPad dan
   meminta kabarnya di island. Sekarang ia bersembunyi di atas layar, tidak
   memesan ruang, dan TURUN hanya selama ada kabar: persis Dynamic Island.
   Selama ia tampil, bagian tengah atas header tertutup beberapa detik. Itu
   harganya, dan dipilih dengan sadar.

   Pita ringkas & panel mekar TIDAK dibawa. Di layar sentuh "hover" lengket
   sesudah diketuk, dan panel yang memuai di atas header saat orang cuma mau
   membaca kabar adalah tabrakan, bukan fitur. */
@media (max-width: 1199px) {
    .d-kapsul-ruang { display: none; }

    .d-kapsul {
        top: calc(env(safe-area-inset-top, 0px) + 10px);
        left: 0;
        right: 0;
        width: min(var(--d-kapsul-l), calc(100% - 32px));
        /* Di atas pemilih HP (320) dan toast (400); di bawah kabar pindai (500). */
        z-index: 405;
        opacity: 0;
        translate: 0 calc(-100% - 24px);
        pointer-events: none;
        cursor: default;
        transition:
            translate .5s var(--d-ease-rail),
            opacity .32s var(--d-kapsul-ez2),
            width .55s var(--d-ease-rail),
            height .55s var(--d-ease-rail),
            border-radius .55s var(--d-ease-rail),
            padding .55s var(--d-ease-rail);
    }
    .d-kapsul[data-kabar] { opacity: 1; translate: 0 0; }
    /* Hanya kabar yang bisa diketuk yang menangkap ketukan. Kabar pendek
       membiarkan ketukan lewat ke header di belakangnya. */
    .d-kapsul[data-kabar][data-terpotong] { pointer-events: auto; cursor: pointer; }

    .d-kapsul-ringkas,
    .d-kapsul-penuh { display: none; }

    .d-kapsul:hover,
    .d-kapsul:has(:focus-visible) {
        height: var(--d-kapsul-t);
        border-radius: 21px;
        padding: 0 16px;
    }
    .d-kapsul[data-panjang]:hover,
    .d-kapsul[data-panjang]:has(:focus-visible) {
        height: 62px;
        border-radius: 24px;
    }
    .d-kapsul[data-kabar]:hover .d-kapsul-kabar,
    .d-kapsul[data-kabar]:has(:focus-visible) .d-kapsul-kabar { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .d-kapsul-titik-kabar,
    .d-kapsul-kabar-teks { animation: none; }
    .d-kapsul-denyut::after { animation: none; opacity: 0; }
    /* Tanpa zoom dan goyang: warna titiknya sudah membawa artinya. */
    .d-kapsul[data-gerak] { animation: none; }
}
@media (max-width: 1199px) and (prefers-reduced-motion: reduce) {
    /* Tidak meluncur dari atas -- cukup muncul dan memudar di tempatnya. */
    .d-kapsul { translate: 0 0; }
}

/* Banner peringatan lebar penuh (koneksi putus, sinkron gagal, restok). */
.d-kabar {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 16px 20px;
    border-radius: 18px;
    margin-bottom: 14px;
    background: color-mix(in srgb, var(--d-kabar-warna, var(--orange)) 9%, var(--card-solid));
    border: 1px solid color-mix(in srgb, var(--d-kabar-warna, var(--orange)) 35%, transparent);
    text-decoration: none;
    color: inherit;
    animation: d-fadeUp .5s var(--d-ease-out) backwards;
}
a.d-kabar:hover { text-decoration: none; }
.d-kabar-ikon {
    flex: 0 0 34px;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--d-kabar-warna, var(--orange)) 18%, transparent);
    color: var(--d-kabar-warna, var(--orange));
    font-size: 17px;
    font-weight: 700;
}
.d-kabar-judul { font-size: 14.5px; font-weight: 700; letter-spacing: -.01em; }
.d-kabar-teks  { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
.d-kabar-aksi  { margin-left: auto; display: flex; gap: 8px; flex: 0 0 auto; }
.d-kabar-warn  { --d-kabar-warna: var(--orange); }
.d-kabar-buruk { --d-kabar-warna: var(--red); }
/* Tanda di dalam ikon memakai warna TEKS -- isian oranye di atas tint
   oranye cuma 1,7:1 di mode terang. Lingkaran latarnya tetap warna isian. */
.d-kabar-warn .d-kabar-ikon  { color: var(--oranye-teks); }
.d-kabar-buruk .d-kabar-ikon { color: var(--merah-teks); }
/* Hijau untuk kabar BERHASIL. Sebelumnya pesan sukses memakai nada warn,
   jadi "Produk berhasil diperbarui" tampil oranye seperti peringatan. */
.d-kabar-ok    { --d-kabar-warna: var(--green); }

/* Varian ringkas: satu baris keterangan tanpa judul & tanpa tombol. Dipakai
   untuk catatan yang sesekali perlu dibereskan, supaya tidak menyaingi
   banner di atasnya yang isinya pekerjaan hari ini. */
.d-kabar-ringkas { padding: 11px 16px; gap: 11px; border-radius: 14px; }
.d-kabar-ringkas .d-kabar-ikon { flex: 0 0 24px; width: 24px; height: 24px; font-size: 13px; }
.d-kabar-ringkas .d-kabar-teks { margin-top: 0; }

/* Varian untuk banner BERPARAGRAF (mis. peringatan "337 transaksi belum
   punya HPP" di Laporan yang isinya beberapa kalimat + tautan + tombol).
   .d-kabar biasa itu flex row -- setiap potongan teks & tautan di dalamnya
   jadi kolom flex tersendiri, sehingga kalimatnya terpotong-potong jadi
   kolom-kolom sempit. Varian ini kembali ke aliran teks normal. */
.d-kabar-blok {
    display: block;
    font-size: 13.5px;
    line-height: 1.55;
}
.d-kabar-blok .d-btn { margin-top: 10px; }

/* Sparkline mungil di dalam kartu KPI Laporan (Chart.js). Tingginya
   sebelumnya dipatok blok <style> di laporan.php yang ikut dibuang saat
   migrasi -- tanpa aturan ini kanvasnya memuai setinggi kartunya. */
.mini-chart {
    display: block;
    width: 100% !important;
    height: 36px !important;
    margin-top: 10px;
    pointer-events: none;
}

/* -------------------------------------------------------------------------
   17. POPOVER (portal ke <body>)
   JEBAKAN #2: ini WAJIB anak langsung <body>, bukan di dalam kartu.
   Kartu ber-backdrop-filter bikin stacking context; popover di dalamnya
   akan tertimpa kartu berikutnya berapa pun z-index-nya.
   ------------------------------------------------------------------------- */
.d-pop {
    position: fixed;
    z-index: 90;
    padding: 12px 14px;
    border-radius: 16px;
    /* 99%, bukan 97%: panel ini melayang di atas konten, dan tanpa blur
       kepekatan sendiri yang memisahkan tulisannya dari tulisan di
       baliknya. 97% terukur masih membayang -- lihat catatan .d-actionbar. */
    background: color-mix(in srgb, var(--card-solid) 99%, transparent);
    backdrop-filter: var(--d-kaca-pop);
    -webkit-backdrop-filter: var(--d-kaca-pop);
    border: 1px solid var(--border);
    box-shadow: var(--d-shadow-pop);
    animation: d-popIn .2s var(--d-ease-pop) backwards;
    pointer-events: none;
}
.d-pop-tgl { font-size: 12.5px; font-weight: 700; margin-bottom: 8px; letter-spacing: -.01em; }
.d-pop-baris {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 12.5px;
    line-height: 1.7;
    white-space: nowrap;
}
.d-pop-titik { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 7px; }
.d-pop-nama  { color: var(--muted); }
.d-pop-nilai { margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums; padding-left: 14px; }

/* -------------------------------------------------------------------------
   18. CHART TREN
   SVG dirender PHP; JS cuma mengurus hover snapping & popover.
   ------------------------------------------------------------------------- */
/* Dua rasio grafik tren, satu tampil per lebar layar -- lihat catatan di
   index.php. Versi sempit mati di sini, dinyalakan di blok @media HP. */
.d-tren-sempit { display: none; }
.d-chart { position: relative; }
.d-chart svg { width: 100%; height: auto; display: block; overflow: visible; }
.d-chart-seri {
    stroke-dasharray: 2600;
    animation: d-dash 1.9s cubic-bezier(.2,.8,.3,1) backwards;
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* Titik chart ikut popIn saat grafik pertama tampil (handoff: popIn dipakai
   untuk titik chart, popover, & badge). Delay 1s menunggu garisnya selesai
   digambar oleh d-dash -- kalau bersamaan, titiknya muncul di garis yang
   belum ada. */
.d-chart-titik {
    transition: r .18s cubic-bezier(.34,1.5,.42,1), fill .18s;
    animation: d-popIn .3s var(--d-ease-pop) 1s backwards;
    transform-box: fill-box;
    transform-origin: center;
}
.d-chart-panduan { opacity: 0; transition: opacity .15s; }
.d-chart.d-hover .d-chart-panduan { opacity: .5; }
.d-chart-sumbu {
    display: flex;
    justify-content: space-between;
    font-size: 11.5px;
    color: var(--faint);
    margin-top: 8px;
    padding: 0 14px;
}
.d-chart-legenda { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 14px; }
.d-chart-legenda span { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--muted); }

/* -------------------------------------------------------------------------
   19. KALENDER KUSTOM
   Pengganti <input type="date"> yang dilarang handoff. Latar 97% opak
   supaya angka di belakangnya tidak menembus lewat blur.
   ------------------------------------------------------------------------- */
.d-kal { position: relative; display: inline-block; }
.d-kal-tombol {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    /* Kiri lebih sempit: gambar ikon kalender baru mulai di x=3 dari 24, jadi
       padding simetris membuat isinya tampak bergeser ke kanan. */
    padding: 10px 16px 10px 12px;
    border-radius: 12px;
    border: 1px solid var(--border);
    background: var(--card-solid);
    color: var(--text);
    font-size: 13.5px;
    font-weight: 600;
    cursor: pointer;
    transition: border-color .25s;
}
.d-kal-tombol:hover { border-color: var(--primary); }
.d-kal-tombol svg { width: 15px; height: 15px; color: var(--faint); }
.d-kal-panel {
    position: fixed;
    z-index: 90;
    width: 268px;
    padding: 14px;
    border-radius: 18px;
    /* 99%, bukan 97%: panel ini melayang di atas konten, dan tanpa blur
       kepekatan sendiri yang memisahkan tulisannya dari tulisan di
       baliknya. 97% terukur masih membayang -- lihat catatan .d-actionbar. */
    background: color-mix(in srgb, var(--card-solid) 99%, transparent);
    backdrop-filter: var(--d-kaca-pop);
    -webkit-backdrop-filter: var(--d-kaca-pop);
    border: 1px solid var(--border);
    box-shadow: 0 30px 60px -20px rgba(0,0,0,.55);
    animation: d-popIn .2s var(--d-ease-pop) backwards;
}
.d-kal-nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.d-kal-nav button {
    width: 28px; height: 28px; border-radius: 8px; border: none;
    background: var(--field); color: var(--text); cursor: pointer; font-size: 14px; line-height: 1;
}
.d-kal-bulan { font-size: 13.5px; font-weight: 700; }
.d-kal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.d-kal-hari { font-size: 10.5px; font-weight: 700; color: var(--faint); text-align: center; padding: 4px 0; text-transform: uppercase; }
.d-kal-tgl {
    aspect-ratio: 1;
    border: none;
    background: none;
    border-radius: 9px;
    font-size: 12.5px;
    color: var(--text);
    cursor: pointer;
    transition: background .2s;
}
.d-kal-tgl:hover { background: var(--field); }
.d-kal-tgl.kosong { visibility: hidden; }
.d-kal-tgl.pilih { background: var(--primary); color: #fff; font-weight: 700; }
.d-kal-backdrop { position: fixed; inset: 0; z-index: 89; background: transparent; }

/* -------------------------------------------------------------------------
   20. ACTION BAR STICKY BAWAH (dipakai HPP Reseller)
   ------------------------------------------------------------------------- */
.d-actionbar {
    position: sticky;
    bottom: 0;
    z-index: 11;
    margin-top: 14px;
    padding: 14px 20px;
    border-radius: 18px;
    display: flex;
    align-items: center;
    gap: 12px;
    /* --d-kaca-tumpang, BUKAN --card. Bar ini melayang di atas tabel yang
       masih tergulir di belakangnya; sejak blur dibuang, kepekatan sendiri
       yang jadi satu-satunya pemisah antara tulisan tombol dan tulisan
       tabel. --card sudah diturunkan ke .46 supaya kartu terasa tembus, dan
       kartu boleh setembus itu karena di belakangnya cuma latar halaman.
       Bar ini tidak punya kemewahan itu.

       .99, DAN ANGKA ITU DIUKUR, BUKAN DIPILIH. Pada .94 nama SKU di baris
       yang tertutup masih terbaca menembus bar; pada .97 masih membayang;
       .99 yang menghabiskannya. Teks berkontras tinggi memang menuntut
       hampir kepekatan penuh begitu tidak ada blur yang meleburkannya.

       Karena itu backdrop-filter dilepas dari sini: di balik permukaan
       .99 tidak ada lagi yang tersisa untuk disaturasi, jadi yang tertinggal
       cuma satu lapisan komposit tambahan yang tidak mengubah apa pun di
       layar. */
    background: var(--d-kaca-tumpang);
    /* Cincin kaca yang sama seperti kartu, tapi bayangan jatuhnya ke ATAS:
       bar ini menempel di dasar viewport, jadi yang perlu terpisah dari
       latarnya adalah tepi atasnya. */
    box-shadow: var(--d-kaca-cincin), 0 -8px 30px -18px rgba(0,0,0,.4);
}
.d-actionbar-ket { font-size: 13px; font-weight: 600; color: var(--muted); }
.d-actionbar-aksi { margin-left: auto; display: flex; gap: 9px; }

/* -------------------------------------------------------------------------
   21. UTILITAS
   ------------------------------------------------------------------------- */
.d-stack   { display: flex; flex-direction: column; gap: 14px; }
.d-row     { display: flex; align-items: center; gap: 10px; }
.d-row-wrap{ display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.d-spacer  { margin-left: auto; }
.d-muted   { color: var(--muted); }
.d-faint   { color: var(--faint); font-size: 12px; }
.d-sub     { font-size: 12px; color: var(--faint); margin-top: 2px; }
/* .d-mini dipakai di 14 halaman sejak commit-commit awal tapi TIDAK PERNAH
   didefinisikan -- jadi semua teks yang dimaksudkan kecil-dan-redup selama
   ini tampil sebesar teks biasa. Kelas yang tidak ada gagal DIAM-DIAM: tidak
   ada galat, cuma halaman yang hierarkinya rata. Ditemukan 10 Sep 2026 waktu
   paragraf penutup di sheet Cetak QR keluar sebesar isi utamanya. */
.d-mini    { font-size: 12.5px; color: var(--muted); line-height: 1.65; }
/* Sub-baris di dalam sel: .d-sub sendiri tidak menyetel display (dipakai
   inline juga di beberapa tempat), jadi yang mau turun baris memakai ini. */
.d-sub-blok { display: block; }
/* Pasangan util untuk ISI DI DALAM sel tabel. .d-hp-off tidak bisa dipakai
   di sini: aturannya di-scope ke anak LANGSUNG .d-table-row/.d-table-head
   (kalau tidak, ia akan menyembunyikan sel yang kebetulan bersarang).
   Keduanya mengasumsikan elemen BLOK -- lihat aturan HP di bagian mobile. */
.d-hp-saja { display: none; }
.d-tebal   { font-weight: 700; }
.d-sr {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* ---- Util lama yang aturan DESKTOP-nya dulu ada di style.css ----
   Nama-nama ini masih dipakai markup di beberapa halaman. Yang versi
   mobilenya sudah ada di mobile.css (.hide-mobile, .m-only, .m-sembunyi)
   tidak perlu ditulis ulang -- di desktop mereka memang harus tampil apa
   adanya. Yang WAJIB ada di sini cuma yang aturan desktopnya menyembunyikan
   sesuatu, karena tanpa itu elemennya bocor ke tampilan desktop. */
.show-mobile-only { display: none; }

/* ---- .grid-stats / .stat-box ----
   Pola kartu angka warisan style.css yang masih dipakai beberapa halaman
   (Scan, Laporan, Reseller). Ditema di sini alih-alih diganti markup satu
   per satu: bentuknya memang sama dengan kartu KPI, dan mengubah puluhan
   blok markup untuk hasil visual yang identik cuma menambah risiko.
   :not(.scan-sisa) mengecualikan pil angka di dalam kotak scan -- itu
   memakai .stat-box juga (JS-nya mencari closest('.stat-box')), tapi
   bentuknya pil, bukan kartu. */
.grid-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 14px;
    margin-bottom: 14px;
}
.stat-box:not(.scan-sisa) {
    background: var(--card);
    backdrop-filter: var(--d-kaca-card);
    -webkit-backdrop-filter: var(--d-kaca-card);
    border-radius: 20px;
    padding: 16px 18px;
    text-decoration: none;
    color: inherit;
    box-shadow: var(--d-shadow-card);
    animation: d-fadeUp .5s var(--d-ease-out) backwards;
    transition: transform .3s var(--d-ease-pop), box-shadow .3s;
}
a.stat-box:not(.scan-sisa):hover { transform: translateY(-3px); }
.stat-box:not(.scan-sisa) .num {
    font-size: 22px;
    font-weight: 700;
    letter-spacing: -.02em;
    font-variant-numeric: tabular-nums;
}
.stat-box:not(.scan-sisa) .label {
    font-size: 12.5px;
    color: var(--muted);
    margin-top: 4px;
    line-height: 1.35;
}
.stat-box.warn:not(.scan-sisa) .num  { color: var(--oranye-teks); }
.stat-box.danger:not(.scan-sisa) .num { color: var(--red); }
.stat-box.success:not(.scan-sisa) .num { color: var(--green); }
.stat-box .num.positif { color: var(--green); }
.stat-box .num.negatif { color: var(--red); }

/* ---- <details> sebagai kartu yang bisa dilipat ---- */
details.d-card { padding: 0; overflow: hidden; }
details.d-card > summary {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: var(--d-pad-card);
    cursor: pointer;
    list-style: none;
    user-select: none;
}
/* Segitiga bawaan browser diganti tanda sendiri supaya sama di semua OS. */
details.d-card > summary::-webkit-details-marker { display: none; }
details.d-card > summary::marker { content: ""; }
details.d-card > summary::after {
    content: "";
    margin-left: auto;
    width: 8px;
    height: 8px;
    border-right: 2px solid var(--faint);
    border-bottom: 2px solid var(--faint);
    transform: rotate(45deg) translate(-2px, -2px);
    transition: transform .28s var(--d-ease-out);
}
details.d-card[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
details.d-card > summary h2 { font-size: 17px; font-weight: 700; letter-spacing: -.02em; }
details.d-card > summary:hover { background: color-mix(in srgb, var(--primary) 4%, transparent); }
.details-card-body { padding: 0 var(--d-pad-card) var(--d-pad-card); }

/* Judul seksi dengan keterangan kecil di kanannya. */
.judul-dengan-meta {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
}

/* ---- Modal kamera scan (dipakai di HP, dibuka dari tab Sore & Retur) ----
   Aturannya dulu di style.css. Yang paling penting display:none-nya: tanpa
   itu tombol tutup & teks petunjuknya tercecer di dasar halaman desktop,
   padahal modalnya belum dibuka sama sekali. */
.camera-modal {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 200;
    background: rgba(0,0,0,.88);
    align-items: center;
    justify-content: center;
    padding: 20px;
}
.camera-modal.show { display: flex; }
.camera-modal-inner { position: relative; width: 100%; max-width: 460px; }
.camera-modal-close {
    position: absolute;
    top: -46px;
    right: 0;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: none;
    cursor: pointer;
    background: rgba(255,255,255,.16);
    color: #fff;
    font-size: 18px;
    line-height: 1;
}
.camera-modal-hint {
    margin-top: 14px;
    text-align: center;
    color: rgba(255,255,255,.82);
    font-size: 13px;
}
#cameraReader { border-radius: 18px; overflow: hidden; }

/* ---- Sisa komponen warisan style.css yang masih dipakai markup ----
   Ditemukan lewat penyisiran: seluruh class yang dipakai halaman termigrasi,
   ada di style.css, tapi tidak ada di app.css maupun mobile.css. Tanpa blok
   ini elemennya tampil telanjang (paling terasa di keranjang Retur Manual
   yang dipakai harian). */

/* Baris tabel yang bisa diklik. */
.row-clickable { cursor: pointer; }
.row-clickable:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* Deretan chip/tag di atas daftar. */
.tag-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }

/* Badge status retur (ungu -- bukan merah: retur bukan kegagalan). */
.badge-retur {
    display: inline-block;
    padding: 4px 10px;
    border-radius: 999px;
    font-size: 11.5px;
    font-weight: 700;
    background: color-mix(in srgb, var(--purple) 14%, transparent);
    color: var(--purple);
}

/* Badge tren naik/turun di kartu Laporan. */
.trend-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-top: 6px;
    padding: 3px 9px;
    border-radius: 999px;
    font-size: 11.5px;
    font-weight: 700;
    line-height: 1;
}
.trend-badge.up   { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--green); }
.trend-badge.down { background: color-mix(in srgb, var(--red) 14%, transparent);   color: var(--red); }
/* Varian netral: dipakai saat datanya belum cukup untuk menghitung tren
   ("Data belum cukup" / "0% hari ini") -- selama ini tidak punya aturan
   sama sekali sehingga badge-nya tampil tanpa latar. */
.trend-badge.flat { background: var(--field); color: var(--muted); }

/* Kartu pemilih reseller (tab Performa). Dipindah dari <style> sebaris di
   data_reseller.php; warnanya dulu dipatok teal sehingga tidak ikut warna
   aksen yang dipilih pengguna. */
.reseller-card {
    cursor: pointer;
    position: relative;
    transition: box-shadow .22s var(--d-ease-out), border-color .22s var(--d-ease-out), transform .22s var(--d-ease-out);
}
.reseller-card:hover {
    border-color: var(--primary);
    transform: translateY(-2px);
    box-shadow: 0 12px 28px -20px color-mix(in srgb, var(--primary) 70%, transparent);
}
.reseller-card:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.reseller-card.active {
    border-color: var(--primary);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 30%, transparent);
}
.reseller-rank { position: absolute; top: 12px; right: 14px; font-size: 22px; line-height: 1; }

/* Judul kolom yang bisa diurutkan (nama lama, dipakai JS pasangSortTabel). */
.th-sort { cursor: pointer; user-select: none; }
.th-sort:hover { color: var(--text); }
.th-sort.sort-asc::after  { content: " \2191"; color: var(--primary); }
.th-sort.sort-desc::after { content: " \2193"; color: var(--primary); }

/* Keranjang Retur Manual di halaman Scan. */
.retur-item-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 11px 13px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--card-solid);
    margin-bottom: 8px;
    transition: border-color .2s, background .2s;
}
.retur-item-row:hover { border-color: var(--primary); }
.retur-item-row input[type="checkbox"] { width: 18px; height: 18px; flex: 0 0 18px; }
.retur-item-row:has(input:checked) {
    border-color: var(--green);
    background: color-mix(in srgb, var(--green) 8%, var(--card-solid));
}
.retur-item-nama { flex: 1; font-size: 14px; min-width: 0; }
.retur-item-qty  { font-size: 13px; color: var(--muted); font-weight: 600; }
.retur-item-row-wrap { margin-bottom: 8px; }
.retur-item-row-wrap .retur-item-row { margin-bottom: 0; }

/* Satu barang di daftar Retur Manual: kepala (kode, jumlah, Hapus) + baris
   kondisi di bawahnya. Barisnya sengaja MEMAKAI ULANG .retur-item-row milik
   tab Retur biasa -- centang "masih layak pakai" artinya sama persis di dua
   tempat, jadi tidak ada gunanya melahirkan komponen kedua yang mirip. */
.rm-barang { padding: 12px 0; border-bottom: 1px solid var(--hair); }
.rm-barang:last-child { border-bottom: none; padding-bottom: 0; }
.rm-barang:first-child { padding-top: 0; }
.rm-barang-kepala {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.rm-barang-nama { flex: 1; min-width: 160px; font-size: 14px; }
.rm-barang-satuan { font-size: 13px; color: var(--muted); }
/* Komponen paket berderet ke bawah dalam satu pembungkus, jadi jaraknya
   diatur di sini -- .retur-item-row-wrap sengaja menolkan margin bawahnya.
   Aturan kedua untuk baris yang didahului kotak "berapa yang selamat":
   popup-nya menempel ke barisnya sendiri (border-top: none), jadi jarak baru
   dibuka sesudah popup itu, bukan sebelumnya. */
.rm-barang .retur-item-row + .retur-item-row { margin-top: 8px; }
.rm-barang .retur-item-popup + .retur-item-row { margin-top: 8px; }
.retur-item-popup {
    margin: -1px 0 0;
    padding: 12px 14px;
    border: 1px solid var(--green);
    border-top: none;
    border-radius: 0 0 12px 12px;
    background: color-mix(in srgb, var(--green) 6%, var(--card-solid));
}
.retur-item-popup-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 13.5px; }
.qty-field {
    width: 76px;
    padding: 8px 10px;
    border-radius: 10px;
    border: 1px solid var(--border);
    background: var(--card-solid);
    color: var(--text);
    font-size: 13.5px;
    font-weight: 600;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.qty-field:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent);
}

/* Alert lama yang belum sempat jadi .d-kabar. */
.alert {
    padding: 14px 18px;
    border-radius: 16px;
    margin-bottom: 14px;
    font-size: 13.5px;
    line-height: 1.55;
    border: 1px solid color-mix(in srgb, var(--d-alert-warna, var(--orange)) 35%, transparent);
    background: color-mix(in srgb, var(--d-alert-warna, var(--orange)) 9%, var(--card-solid));
}
.alert-success { --d-alert-warna: var(--green); }
.alert-danger  { --d-alert-warna: var(--red); }
.alert-warn    { --d-alert-warna: var(--orange); }

/* ---- Kontrol halaman Pengaturan & Profil ----
   Nama class-nya dipertahankan (JS pengaturan.php menyasar .segmented
   button, .swatch, dan input di dalam .switch). Ditulis ulang di sini
   dengan bahasa visual UI baru, bukan disalin mentah dari style.css. */

/* Segmented control kecil (Terang / Gelap / Ikuti Sistem). */
.segmented {
    display: inline-flex;
    gap: 3px;
    padding: 3px;
    background: var(--field);
    border-radius: 13px;
}
.segmented button {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 10px 16px;
    border: none;
    border-radius: 10px;
    background: none;
    color: var(--muted);
    font-size: 12.5px;
    font-weight: 600;
    cursor: pointer;
    transition: background .25s, color .25s;
}
.segmented button svg { width: 19px; height: 19px; }
.segmented button:hover { color: var(--text); }
.segmented button.active {
    background: var(--card-solid);
    color: var(--primary);
    box-shadow: 0 1px 3px rgba(0,0,0,.14);
}

/* Baris preferensi dengan switch di kanan. */
.switch-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 14px 0;
}
.switch-row + .switch-row { border-top: 1px solid var(--hair); }
.switch-row-text  { display: flex; flex-direction: column; gap: 3px; }
.switch-row-title { font-weight: 600; font-size: 14px; }
.switch-row-desc  { font-size: 12.5px; color: var(--muted); line-height: 1.45; }

.switch { position: relative; display: inline-block; width: 50px; height: 30px; flex: 0 0 50px; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch-track {
    position: absolute;
    inset: 0;
    background: var(--field);
    border-radius: 999px;
    cursor: pointer;
    transition: background .3s;
}
.switch-track::before {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 3px rgba(0,0,0,.3);
    transition: transform .32s var(--d-ease-pop);
}
.switch input:checked + .switch-track { background: var(--green); }
.switch input:checked + .switch-track::before { transform: translateX(20px); }
.switch input:focus-visible + .switch-track {
    box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--primary) 16%, transparent);
}

/* Swatch warna aksen. */
.swatch-row { display: flex; flex-wrap: wrap; gap: 20px; margin-top: 14px; }
.swatch {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 7px;
    padding: 2px;
    border: none;
    background: none;
    cursor: pointer;
    font-family: inherit;
}
.swatch-dot {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    flex: 0 0 32px;
    background: var(--sw);
    transition: box-shadow .3s, transform .3s var(--d-ease-pop);
}
.swatch-label { font-size: 11.5px; font-weight: 600; color: var(--muted); transition: color .3s; }
.swatch:hover .swatch-dot { transform: scale(1.06); }
/* Cincin memakai warna swatch-nya sendiri, bukan --primary: saat pengguna
   membandingkan enam pilihan, penandanya harus warna yang dipilih itu. */
.swatch.active .swatch-dot {
    transform: scale(1.1);
    box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--sw) 30%, transparent);
}
.swatch.active .swatch-label { color: var(--text); }

/* Blok setelan yang kontrolnya turun ke baris berikutnya (Warna aksen). */
.d-atur-blok { padding: 16px 0 4px; border-top: 1px solid var(--hair); }

/* Blok "Foto latar belakang": pratinjau kecil + keterangan, di kotak --field
   seperti handoff. */
.d-atur-latar {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 16px;
    border-radius: 16px;
    background: var(--field);
}
.d-atur-latar-pratinjau {
    flex: 0 0 72px;
    width: 72px;
    height: 48px;
    border-radius: 11px;
    border: 1px solid var(--hair);
}
.d-atur-latar-teks { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }

/* Deretan tombol di kaki kartu setelan (tes suara, aksi akun). */
.d-atur-tombol { display: flex; gap: 9px; flex-wrap: wrap; padding-top: 16px; }
.d-atur-tombol + .d-atur-tombol { padding-top: 9px; }

/* Dua kontrol berdampingan di kanan satu baris setelan (toggle + tombol). */
.d-atur-kontrol { display: flex; align-items: center; gap: 9px; flex: 0 0 auto; }

/* Isian angka dengan satuan tertulis di sebelahnya ("200 juta"). Satuannya
   teks biasa, bukan bagian input -- jadi nilai yang dikirim tetap angka
   bersih dan tidak perlu dibersihkan lagi di server. */
.d-satuan { display: inline-flex; align-items: center; gap: 8px; }
.d-satuan-label { font-size: 12.5px; color: var(--muted); }

/* Sepasang tombol di kolom Aksi tabel, rata kanan & tidak boleh terpotong. */
.d-aksi-baris { display: inline-flex; align-items: center; gap: 8px; justify-content: flex-end; }
/* Komisi Diamondo: hijau tebal -- itu angka yang dicari saat menyetel HPP. */
.d-komisi { color: var(--green); }

/* Kotak isian HPP Reseller dibatasi lebarnya. .d-input bawaannya 100%, jadi
   kotaknya memenuhi seluruh kolom -- padahal isinya cuma bilangan enam digit,
   dan kotak selebar itu membuat angkanya melayang jauh dari judul kolomnya.
   Ditempel ke kanan supaya sejajar dengan kolom angka lain di tabel. */
.input-hpp-reseller {
    width: 100%;
    max-width: 176px;
    margin-left: auto;
    text-align: right;
}

/* Progres target (kartu reseller terpilih). */
.d-progres-blok { margin-top: 18px; }
.d-progres-label { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; color: var(--muted); margin-bottom: 7px; }
.d-progres-track { height: 8px; border-radius: 6px; background: var(--field); overflow: hidden; }
.d-progres-isi {
    height: 100%;
    border-radius: 6px;
    background: linear-gradient(90deg, var(--primary), var(--purple));
    transform-origin: left;
    animation: d-growX .8s var(--d-ease-out) backwards;
    transition: width .6s;
}
/* Slider volume -- satu-satunya <input type="range"> di aplikasi. Handoff
   tidak punya komponen ini (volume scan tambahan kita sendiri), jadi gayanya
   diselaraskan dengan .d-progres-track di atas: tinggi 8px, radius 6px, alas
   --field, isian warna aksen. Sebelumnya dibiarkan native, jadi satu-satunya
   kontrol biru di seluruh UI burgundy.

   Isian di kiri thumb digambar sebagai gradien dua-henti pada track, BUKAN
   ::-moz-range-progress: WebKit tidak punya padanan pseudo-element itu, dan
   satu gradien memberi hasil identik di kedua mesin. Persennya dititipkan JS
   lewat --d-range-isi (lihat pengaturan.php). */
.d-range {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 18px; /* ruang untuk thumb -- tinggi TRACK diatur di pseudo-elementnya */
    background: none;
    cursor: pointer;
}
.d-range:focus { outline: none; }
.d-range::-webkit-slider-runnable-track {
    height: 8px;
    border-radius: 6px;
    background: linear-gradient(90deg,
        var(--primary) var(--d-range-isi, 70%), var(--field) var(--d-range-isi, 70%));
}
.d-range::-moz-range-track {
    height: 8px;
    border-radius: 6px;
    background: linear-gradient(90deg,
        var(--primary) var(--d-range-isi, 70%), var(--field) var(--d-range-isi, 70%));
}
.d-range::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 18px;
    height: 18px;
    margin-top: -5px; /* (8 - 18) / 2, memusatkan thumb di track */
    border-radius: 50%;
    background: var(--card-solid);
    border: 2px solid var(--primary);
    box-shadow: 0 1px 4px rgba(0, 0, 0, .18);
    transition: transform .2s var(--d-ease-pop), box-shadow .2s;
}
.d-range::-moz-range-thumb {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--card-solid);
    border: 2px solid var(--primary);
    box-shadow: 0 1px 4px rgba(0, 0, 0, .18);
    transition: transform .2s var(--d-ease-pop), box-shadow .2s;
}
.d-range:hover::-webkit-slider-thumb  { transform: scale(1.12); }
.d-range:hover::-moz-range-thumb      { transform: scale(1.12); }
.d-range:active::-webkit-slider-thumb { transform: scale(1.2); }
.d-range:active::-moz-range-thumb     { transform: scale(1.2); }
/* Cincin fokus digantung di thumb, bukan outline elemen -- outline pada
   <input type=range> menggambar kotak mengelilingi SELURUH lebar slider. */
.d-range:focus-visible::-webkit-slider-thumb {
    box-shadow: 0 1px 4px rgba(0, 0, 0, .18), 0 0 0 4px color-mix(in srgb, var(--primary) 22%, transparent);
}
.d-range:focus-visible::-moz-range-thumb {
    box-shadow: 0 1px 4px rgba(0, 0, 0, .18), 0 0 0 4px color-mix(in srgb, var(--primary) 22%, transparent);
}
/* Nonaktif saat toggle "Suara saat scan" dimatikan: isian & thumb kehilangan
   warna aksen supaya tidak terlihat masih bisa disetel. */
.d-range:disabled { cursor: not-allowed; }
.d-range:disabled::-webkit-slider-runnable-track {
    background: linear-gradient(90deg,
        var(--faint) var(--d-range-isi, 70%), var(--field) var(--d-range-isi, 70%));
}
.d-range:disabled::-moz-range-track {
    background: linear-gradient(90deg,
        var(--faint) var(--d-range-isi, 70%), var(--field) var(--d-range-isi, 70%));
}
.d-range:disabled::-webkit-slider-thumb { border-color: var(--faint); transform: none; }
.d-range:disabled::-moz-range-thumb     { border-color: var(--faint); transform: none; }

/* Kolom yang isinya jangan pernah dipotong ellipsis (tombol aksi). */
.d-tabel td.d-keep { white-space: nowrap; }

/* Segmented versi teks -- tanpa ikon, dipakai baris setelan yang kontrolnya
   duduk di kanan label. Tanpa ini tiap tombol jadi kolom (ikon di atas teks)
   sehingga barisnya jauh lebih tinggi daripada baris lain. */
.segmented-teks button { flex-direction: row; padding: 9px 15px; font-size: 13px; }

/* Sorotan sesaat untuk menunjukkan tujuan sebuah tombol lompat. */
.d-sorot { box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 35%, transparent); transition: box-shadow .3s; }

/* Baris tabel yang BARU saja ditambahkan lewat AJAX -- disorot sebentar
   supaya mata tahu ke mana harus melihat setelah menekan Simpan.
   Sorotannya di <td>, bukan box-shadow di <tr> seperti .d-sorot: di ponsel
   tiap baris jadi kartu dengan box-shadow sendiri (.d-tabel tr:not(.rt-head)
   di blok mobile) yang menang spesifisitas dan menelan sorotannya. */
.d-tabel tr.d-baris-baru td {
    background: color-mix(in srgb, var(--primary) 12%, var(--card-solid));
    transition: background .45s;
}
.d-atur-blok > .switch-row-title { display: block; font-weight: 600; font-size: 14px; }
.d-atur-blok > .switch-row-desc { display: block; font-size: 12.5px; color: var(--muted); margin-top: 3px; line-height: 1.45; }

/* Ringkasan akun & daftar pasangan kunci-nilai (Tentang Aplikasi). */
.settings-account { display: flex; align-items: center; gap: 14px; }
.settings-account-name { font-size: 15px; font-weight: 700; }
.settings-account-meta { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
.settings-kv { display: flex; flex-direction: column; gap: 2px; }
.settings-kv-item {
    display: flex;
    justify-content: space-between;
    gap: 14px;
    padding: 10px 0;
    font-size: 13.5px;
}
.settings-kv-item + .settings-kv-item { border-top: 1px solid var(--hair); }
.settings-kv-item .k { color: var(--muted); }

/* -------------------------------------------------------------------------
   HALAMAN PROFIL -- identitas terpusat
   -------------------------------------------------------------------------
   Profil sengaja dibuat RINGKAS (15 Agu 2026, permintaan pemilik): kartu
   foto/informasi akun/detail akun pindah ke Pengaturan, yang memang sudah
   punya sheet "Edit Profil & Foto". Yang tersisa di sini cuma identitas +
   tiga baris pintasan, jadi halamannya terbaca sekali lihat di HP.

   Semua di luar @media, dipakai di lebar berapa pun -- di desktop blok ini
   dibatasi lebarnya lewat .d-profil-sempit supaya tidak melar jadi pita
   selebar layar. */
.d-profil-sempit { max-width: 460px; margin-left: auto; margin-right: auto; }

.d-profil-identitas { text-align: center; padding: 4px 0 20px; }
.d-profil-avatar {
    width: 96px;
    height: 96px;
    margin: 0 auto 14px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    font-size: 38px;
    font-weight: 700;
    color: #fff;
    /* Gradasi diturunkan dari --primary, bukan dipatok #a82440 seperti
       .user-avatar, supaya ikut waktu user mengganti warna aksen. */
    background: linear-gradient(150deg,
        color-mix(in srgb, var(--primary) 82%, #fff),
        color-mix(in srgb, var(--primary) 78%, #000));
    box-shadow: 0 10px 26px -10px color-mix(in srgb, var(--primary) 60%, transparent);
}
.d-profil-avatar img { width: 100%; height: 100%; object-fit: cover; }
.d-profil-nama { font-size: 20px; font-weight: 700; letter-spacing: -.02em; }
.d-profil-identitas .d-badge { margin-top: 9px; }
.d-profil-meta { font-size: 13px; color: var(--muted); margin-top: 9px; }

/* Nada badge merek -- .d-badge-info sudah dipakai teal, dan role "Admin"
   di rujukan berwarna burgundy (identitas Diamondo). */
.d-badge-merek { --d-badge-warna: var(--primary); }

/* Baris kartu yang bisa diketuk, dengan chevron di kanan. Memakai .switch-row
   sekaligus supaya garis rambut antar baris (aturan .switch-row + .switch-row)
   tetap berlaku walau barisnya <button>, bukan <div>. */
.d-baris-tuju {
    width: 100%;
    background: none;
    border: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    border-radius: 10px;
}
.d-baris-tuju:hover { background: var(--surface-tint-2); }
.d-baris-tuju:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 22%, transparent);
}
.d-baris-tuju .d-chevron { width: 18px; height: 18px; color: var(--faint); flex: 0 0 18px; }

/* Avatar (dipakai Pengaturan & Profil). */
.user-avatar {
    width: 44px;
    height: 44px;
    flex: 0 0 44px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(150deg, #a82440, #96371f);
    color: #fff;
    font-size: 17px;
    font-weight: 700;
    overflow: hidden;
}
.user-avatar-img { object-fit: cover; }

/* Foto profil besar di halaman Profil. */
.profile-photo,
.profile-photo-placeholder {
    width: 132px;
    height: 132px;
    border-radius: 50%;
    margin: 0 auto 14px;
    display: flex;
    align-items: center;
    justify-content: center;
    object-fit: cover;
    overflow: hidden;
}
.profile-photo-placeholder {
    background: linear-gradient(150deg, #a82440, #96371f);
    color: #fff;
    font-size: 46px;
    font-weight: 700;
}

/* ---- Tur onboarding (assets/tour.js) ----
   Gayanya dulu di style.css. Sorotannya memakai trik box-shadow raksasa:
   satu kotak transparan seukuran elemen sasaran, dengan bayangan sebesar
   layar -- jadi seluruh halaman gelap KECUALI elemen itu, tanpa perlu
   memotong lubang di elemen lain. */
.tour-spotlight {
    position: fixed;
    z-index: 300;
    border-radius: 16px;
    pointer-events: none;
    box-shadow: 0 0 0 9999px rgba(0,0,0,.62);
    transition: all .35s var(--d-ease-out);
}
.tour-popover {
    position: fixed;
    z-index: 301;
    width: 320px;
    max-width: calc(100vw - 32px);
    padding: 20px 22px;
    border-radius: 20px;
    background: var(--card-solid);
    border: 1px solid var(--border);
    box-shadow: 0 30px 70px -20px rgba(0,0,0,.55);
    animation: d-popIn .28s var(--d-ease-pop) backwards;
}
.tour-popover-judul { font-size: 16.5px; font-weight: 700; letter-spacing: -.02em; }
.tour-popover-isi   { font-size: 13.5px; color: var(--muted); line-height: 1.55; margin-top: 8px; }
.tour-popover-aksi  { display: flex; align-items: center; gap: 10px; margin-top: 18px; }
.tour-popover-progress { font-size: 12px; color: var(--faint); margin-right: auto; }
.tour-btn-lanjut {
    padding: 9px 18px;
    border-radius: 12px;
    border: none;
    background: var(--primary);
    color: #fff;
    font-size: 13.5px;
    font-weight: 600;
    cursor: pointer;
}
.tour-btn-lewati {
    padding: 9px 14px;
    border-radius: 12px;
    border: none;
    background: none;
    color: var(--muted);
    font-size: 13.5px;
    font-weight: 600;
    cursor: pointer;
}
.tour-btn-lewati:hover { color: var(--text); }

/* Legenda ringkasan per kategori / per ekspedisi di halaman Scan. */
.scan-legend {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 10px 18px;
    align-items: center;
}
.scan-legend > * { display: flex; align-items: baseline; gap: 8px; font-size: 13.5px; }


/* -------------------------------------------------------------------------
   MENU "LAINNYA" -- kartu fitur berkelompok
   -------------------------------------------------------------------------
   Dipakai lainnya.php: satu kartu per halaman, dikelompokkan (Operasional,
   Keuangan, Akun), mengikuti template yang diberikan pemilik. Ikon & warna
   tint-nya datang dari PHP lewat inline style, jadi menambah menu baru cukup
   menambah satu baris di $grupMenu tanpa menyentuh CSS.

   Sengaja DI LUAR @media: halaman ini sah dibuka di lebar berapa pun. Di HP
   ia jadi satu-satunya jalan ke menu yang tidak muat di bar bawah; di
   desktop ia jadi peta fitur yang menyebutkan keterangan tiap halaman --
   sesuatu yang tidak diberikan rail kiri.
   ------------------------------------------------------------------------- */
.d-menu-grup { margin-bottom: 24px; }

.d-menu-judul {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--faint);
    margin: 0 0 10px 2px;
}

.d-menu-grid {
    display: grid;
    /* Di HP tepat 2 kolom seperti template; di layar lebar ikut mengisi
       ruang yang ada tanpa kartu jadi selebar layar. */
    grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
    gap: 12px;
}

.d-menu-item {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: 14px;
    border-radius: 16px;
    background: var(--card-solid);
    border: 1px solid var(--hair);
    box-shadow: 0 1px 3px rgba(0,0,0,.05);
    text-decoration: none;
    color: inherit;
    transition: transform .18s var(--d-ease-out, ease), box-shadow .18s, border-color .18s;
}
.d-menu-item:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 20px -10px rgba(0,0,0,.28);
    border-color: color-mix(in srgb, var(--primary) 35%, transparent);
}
.d-menu-item:active { transform: translateY(0); }

.d-menu-ikon {
    width: 38px;
    height: 38px;
    border-radius: 11px;
    display: grid;
    place-items: center;
    font-size: 18px;
    line-height: 1;
    margin-bottom: 11px;
}

.d-menu-label {
    font-size: 14px;
    font-weight: 700;
    color: var(--text);
    line-height: 1.25;
}
.d-menu-sub {
    font-size: 11.5px;
    color: var(--muted);
    line-height: 1.35;
    margin-top: 3px;
}


/* -------------------------------------------------------------------------
   22. PENYESUAIAN MOBILE (<=768px)
   Ditulis DI SINI, bukan di mobile.css, supaya revolusi desktop ini tidak
   menyentuh berkas mobile sama sekali.

   Wrapper-nya memakai class GANDA di header.php: .app-shell.d-shell dan
   .container.d-page. Nama lama dipertahankan karena mobile.css masih
   memakai .app-shell untuk menyisakan ruang setinggi bar navigasi bawah.

   PERUBAHAN 14 Agu 2026 (keputusan pemilik "satu UI saja"): mobile.css
   TIDAK lagi berisi UI mobile tersendiri -- ia menyusut dari 2.426 baris
   jadi ~140, isinya cuma menetralkan markup .m-* dan menggambar navigasi
   bawah. Konsekuensinya untuk blok ini: topbar mobile SUDAH TIDAK ADA,
   jadi judul halaman & padding badan halaman yang dulu ditanggung
   mobile.css sekarang jadi tanggung jawab di sini.
   ------------------------------------------------------------------------- */
@media (max-width: 768px) {

/* Rail kiri tidak dipakai di HP -- sulit dijangkau jempol. Navigasinya
   pindah ke bar bawah (.m-tabbar, digambar mobile.css). */
.d-rail, .d-ambient { display: none; }

/* Padding badan halaman. Versi desktop (.d-page, 30px 40px 80px) hidup di
   dalam @media (min-width:769px), jadi di HP tidak ada padding sama sekali
   -- dulu itu disediakan mobile.css lewat .container. Tanpa ini kartu
   menempel ke tepi layar dan judul seksi terpotong.
   Jarak ke bar bawah datang dari .app-shell (lihat mobile.css). */
.d-page { padding: 16px 16px 24px; }

/* Judul halaman TAMPIL lagi. Dulu disembunyikan karena topbar mobile
   sudah menampilkannya; topbar itu ikut pensiun bersama UI mobile
   terpisah, jadi tanpa aturan ini halaman tidak punya judul sama sekali.
   Ukurannya diturunkan dari 32px -- di layar 390px judul sebesar itu
   memakan seperempat layar sebelum isi pertama terlihat. */
.d-head { display: block; margin-bottom: 14px; }
.d-head h1 { font-size: 23px; letter-spacing: -.02em; }
.d-head p  { font-size: 12.5px; margin-top: 3px; }
.d-head-aksi { flex-wrap: wrap; width: 100%; margin-top: 12px; }
/* Tombol kepala melar mengisi barisnya. Dengan lebar sesuai isi, barisnya
   berakhir ragged: di Stok (bar 348px) baris pertama cuma terpakai 312px
   dan baris kedua 153px, jadi "+ Tambah Produk" menggantung di kiri dengan
   195px ruang kosong di sebelahnya. Karena wrap-nya tetap ditentukan lebar
   asli isinya (flex-basis auto), pengelompokan baris tidak berubah -- yang
   berubah cuma sisa ruang dibagi rata, dan tombol yang sendirian di
   barisnya jadi selebar layar, yang kebetulan persis perlakuan yang pantas
   untuk aksi utama. */
.d-head-aksi > * { flex: 1 1 auto; }

/* Banner .d-kabar: tombolnya turun ke baris sendiri. Di 390px,
   .d-kabar-aksi (margin-left:auto) menyisakan cuma 147px untuk teks --
   judul "12 produk perlu restok" pecah jadi dua baris dan daftar SKU-nya
   patah-patah, kartunya menjulang 159px. flex-basis 100% memaksa aksinya
   wrap, sehingga teks dapat lebar penuh. .d-kabar-blok tidak kena: ia
   display:block, jadi flex-wrap di sini tidak berlaku baginya. */
.d-kabar { flex-wrap: wrap; }
.d-kabar > :not(.d-kabar-ikon):not(.d-kabar-aksi) { flex: 1 1 0; min-width: 0; }
.d-kabar-aksi { flex: 1 1 100%; margin-left: 0; }
.d-kabar-aksi > * { flex: 1 1 auto; }

/* Deret tombol .d-row-wrap ikut mengisi barisnya, dengan alasan sama
   seperti .d-head-aksi: di Stok, "Nonaktifkan Terpilih" (181px) dan
   "Aktifkan Terpilih" (159px) tidak muat berdampingan di 315px, jadi
   keduanya turun sendiri-sendiri dan menggantung ragged di kiri.
   Sengaja > .d-btn, bukan > * : wadah yang sama dipakai pencarian.php
   untuk deretan pill contoh pencarian, dan pill memang harus selebar
   isinya, bukan melar. */
.d-row-wrap > .d-btn { flex: 1 1 auto; }

/* Satu kolom. Kartu KPI 26px berdampingan di layar 390px membuat label
   panjang seperti "Total Omzet Setelah Potongan Marketplace" pecah jadi
   empat baris. */
.d-grid-kpi, .d-grid-rasio, .d-grid-2, .d-grid-15, .d-grid-11 { grid-template-columns: 1fr; }

/* PEMISAH SEL BERPUTAR 90 DERAJAT DI SATU KOLOM.

   Di desktop pemisahnya garis KIRI, dan garis milik sel terkiri sengaja
   jatuh di luar bentang lalu dipotong `overflow: hidden`. Begitu gridnya
   jadi satu kolom, SETIAP sel berada di tepi kiri -- jadi setiap garis
   ikut terpotong dan bentangnya berubah jadi satu blok tanpa pemisah sama
   sekali. Empat angka yang menyatu tanpa batas lebih buruk daripada empat
   kartu terpisah.

   Yang benar di sini garis ATAS, dengan mekanisme yang sama persis: outset
   ke atas, jadi milik sel teratas jatuh di luar bentang dan terpotong. */
.d-grid-kpi > .d-kpi,
.d-grid-rasio > .d-kpi,
.d-bentang-2 > * { box-shadow: 0 -1px 0 var(--hair); }
.d-bentang-2 { grid-template-columns: 1fr; }
.d-bentang-2 > * { padding: 16px 18px; }
.d-grid-rasio > .d-kpi { padding: 14px 16px; }
.d-grid-kpi > .d-kpi { padding: 16px 18px; }
/* Bagian grafik ikut dirapatkan sejajar dengan sel di atasnya, supaya tepi
   kiri angka dan tepi kiri grafik tetap satu garis. */
.d-bentang-tren { padding: 14px 18px 16px; }
.d-kpi-spark-wrap { margin: 12px -18px 0; }
.d-kpi.d-kpi-spark { padding: 16px 18px 0; }

/* ---- Segmented control jadi strip yang bisa digulir ----
   Di desktop tiap tab selebar 1fr dan thumb-nya bergeser translateX(i*100%).
   Di layar 400px, empat label seperti "Daftar & Target" + "HPP Reseller" +
   "Performa" + "Rekonsiliasi" butuh ~444px -- kolom 1fr yang sama lebar
   memaksa teksnya (nowrap) menonjol keluar dan halaman ikut bergeser.
   Di sini grid ditukar jadi flex: tiap tab selebar isinya sendiri, dan
   yang berlebih digulir di dalam strip-nya. Thumb disembunyikan -- posisi
   persentasenya tidak lagi berarti begitu lebar tiap kolom berbeda-beda;
   tab aktif ditandai latar padat langsung pada tombolnya. */
.d-seg {
    display: flex;
    width: 100%;
    max-width: none;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}
.d-seg::-webkit-scrollbar { display: none; }
.d-seg-thumb { display: none; }
.d-seg-item { flex: 0 0 auto; padding: 9px 13px; }
/* Strip berisi 5 tab atau lebih dirapatkan. Di 390px, kelima tab Stok
   ("Produk", "Restok 12", "Paket", "Batch", "SKU 5") butuh 340px di kotak
   selebar 315px -- ia memang bisa digulir, tapi tab terakhir tertinggal
   terpotong di tepi dan itu terbaca sebagai rusak, bukan sebagai "geser
   saya". Padding 13px -> 8px membebaskan 50px, cukup untuk memuat semuanya
   sekaligus, dan strip yang tabnya sedikit (Biaya 3 tab, Transaksi 4)
   tetap lapang karena aturan ini tidak mengenainya.
   Pola :nth-last-child(n+5) = "hitung dari BELAKANG": cocok untuk item yang
   masih punya >=4 saudara sesudahnya. Sengaja dihitung dari belakang, bukan
   :first-child:nth-last-child(...) seperti resep quantity-query yang biasa
   dikutip -- anak pertama .d-seg adalah <span class="d-seg-thumb">, bukan
   tab, jadi versi :first-child tidak akan pernah cocok. Dari belakang,
   tab-tab itu memang elemen terakhir, jadi ada-tidaknya thumb tidak
   berpengaruh. */
.d-seg-item:nth-last-child(n+5),
.d-seg-item:nth-last-child(n+5) ~ .d-seg-item {
    padding-left: 8px;
    padding-right: 8px;
}
.d-seg-item.aktif {
    background: var(--card-solid);
    border-radius: 10px;
    box-shadow: 0 1px 3px rgba(0,0,0,.14);
}

/* ---- Label pendek khusus HP ----
   Elemen ber-atribut data-m-teks menampilkan isi atribut itu sebagai
   gantinya: teks aslinya di-font-size:0, ::before yang menggambar. Dipakai
   pada tab Stok & Reseller supaya kelima label ("Restok (12)", "SKU Baru
   (3)", "Daftar & Target") muat utuh dan strip tabnya tidak perlu digulir
   menyamping sama sekali. Di desktop atributnya tidak berefek apa pun.
   !important karena .d-seg-item sudah memasang font-size sendiri dengan
   spesifisitas yang sama. */
[data-m-teks] { font-size: 0 !important; }
[data-m-teks]::before {
    content: attr(data-m-teks);
    display: block;
    font-size: 12.5px;
    font-weight: 700;
    letter-spacing: normal;
    text-align: center;
}

/* Tabel <table> tetap lebar di HP (sel-selnya nowrap supaya angka tidak
   terbelah), jadi SETIAP kartu harus bisa menggulirkan isinya sendiri.
   Tanpa ini badan halaman yang ikut bergeser mendatar -- persis yang
   dilarang handoff. Tabel .d-table (grid) tidak terpengaruh: ia sudah
   menumpuk jadi kartu di lebar ini. */
.d-card { padding: 16px; border-radius: 18px; overflow-x: auto; }


/* ---- <table class="d-tabel">: TETAP TABEL, TIDAK DITUMPUK ----
   Keputusan pemilik 15 Agu 2026, membalik aturan sebelumnya. Sebelum ini
   tabel dibongkar jadi blok (kepala disembunyikan, tiap sel berdiri sendiri
   berlabel) demi syarat lama "tabel tidak boleh lebih lebar dari layar".
   Syarat itu TETAP berlaku -- yang berubah cara memenuhinya: kolom yang
   tidak krusial disembunyikan dengan .hide-mobile, bukan seluruh tabel
   dibongkar jadi kartu vertikal.

   .hide-mobile bukan kelas baru: konvensinya sudah dipakai checkout.php,
   produk.php, dashboard_staff.php, daftar_tenant.php, integrasi.php, dan
   paket_view.php. Yang baru cuma <th>-nya ikut disembunyikan -- dulu tidak
   perlu karena seluruh <thead> memang mati. */
.d-tabel {
    /* fixed: lebar kolom ditentukan kepala tabel, bukan isi selnya. Tanpa
       ini satu SKU panjang melebarkan kolomnya dan mendorong tabel keluar
       layar -- persis yang harus dihindari. */
    table-layout: fixed;
    width: 100%;
}
.d-tabel thead { display: table-header-group; }
.d-tabel tr.rt-head { display: table-row; }
.d-tabel th,
.d-tabel td {
    padding: 9px 7px;
    font-size: 12px;
    /* DIBUNGKUS, bukan dipotong ellipsis (permintaan pemilik 15 Agu 2026:
       judul kolom "tulis seutuhnya, boleh dipotong jadi 2 [baris]", dan isi
       sel "jangan sampai terpotong juga").
       Yang menjaga tabel tetap muat kini table-layout:fixed + pembungkusan:
       lebar kolom sudah dipatok, jadi isi yang panjang turun ke baris
       berikutnya alih-alih mendorong tabel keluar layar.
       overflow-wrap:anywhere WAJIB: tanpa itu satu kata tanpa spasi (SKU,
       nomor resi, email) tidak bisa dipatahkan dan tetap meluber. */
    white-space: normal;
    overflow-wrap: anywhere;
    vertical-align: middle;
}
/* SELURUH penyetelan kepala tabel di HP ditulis dengan selektor PANJANG
   (.d-tabel .rt-head th / .d-tabel thead th, spesifisitas 0,2,1) supaya
   menyamai versi desktopnya. `.d-tabel th` saja cuma 0,1,1 -- kalah, dan
   itu membuat TIGA setelan sekaligus tidak pernah berlaku di HP:
   white-space, padding, dan font-size.
   Akibatnya terukur di tabel HPP Reseller: judul tetap `nowrap` sehingga
   "Komisi Diamondo" minta 133px di kolom 96px -> tabel meluber 36px; dan
   setelah itu dibungkus pun padding desktop 14px menyisakan ruang isi cuma
   68px sementara kata "DIAMONDO" butuh 71px -> pecah jadi "DIAMOND / O".
   Dengan padding 7px + font 10.5px, ruang isinya 82px dan katanya utuh.
   Sel <td> tidak kena masalah ini: aturan desktopnya juga 0,1,1, jadi blok
   HP yang ada di bawahnya menang lewat urutan. */
.d-tabel .rt-head th,
.d-tabel thead th {
    padding: 0 7px 8px;
    font-size: 10.5px;
    line-height: 1.25;
    white-space: normal;
    /* break-word, BUKAN anywhere: `anywhere` memotong di tengah kata walau
       katanya sebenarnya muat sendirian. `break-word` hanya memotong kata
       yang memang tidak muat, jadi judul pecah di SPASI lebih dulu. */
    overflow-wrap: break-word;
    /* Judul dua baris tetap rata bawah dengan judul satu baris. */
    vertical-align: bottom;
}
/* Perataan desktop DIPERTAHANKAN -- dulu dipaksa rata kiri karena label &
   nilai berdiri berlawanan waktu ditumpuk. Sekarang tidak ditumpuk lagi. */

/* Dengan table-layout:fixed, kolom TANPA lebar membagi sisa ruang RATA.
   Akibatnya kolom angka pendek ("742", badge "Aman") mendapat jatah sama
   besar dengan kolom SKU, dan SKU-lah yang pecah jadi banyak baris -- padahal
   ia satu-satunya kolom yang membuat barisnya bisa dikenali. Kolom rata-kanan
   (angka & status) dipatok secukupnya supaya sisanya jatuh ke kolom teks.
   96px, bukan 84: "Rp 345.541.950" pada 12px butuh ~92px, dan sejak isi sel
   dibungkus (bukan dipotong) angka sesempit itu akan pecah jadi dua baris.
   Halaman yang butuh lain tinggal menimpanya dengan style="width:..." di
   <th>-nya, pola yang memang sudah dipakai (mis. kolom kotak centang 32px
   di produk.php). */
.d-tabel th.d-kanan { width: 96px; }
/* td juga: sebagian tabel tidak punya <th> sama sekali (mis. Laba Rugi di
   laporan.php). Di table-layout:fixed yang menentukan lebar adalah BARIS
   PERTAMA, jadi aturan ini hanya berlaku untuk tabel tanpa kepala -- yang
   punya <th> tetap memakai angka di atas. */
.d-tabel td.d-kanan { width: 96px; }
.d-tabel th.hide-mobile,
.d-tabel td.hide-mobile { display: none; }
/* ---- SEL BERISI KENDALI, BUKAN TEKS ----
   Pembungkusan di atas ditujukan untuk TEKS. Kalau kena kendali, hasilnya
   justru rusak: label satuan "juta" pecah jadi j-u-t-a menurun, dan tombol
   "Nonaktifkan" jadi kolom huruf. Kendali dikecualikan di sini. */
.d-tabel .d-satuan-label,
.d-tabel .d-btn,
.d-table-row .d-satuan-label,
.d-table-row .d-btn { white-space: nowrap; overflow-wrap: normal; }

/* Kotak isian di dalam tabel menyusut ikut kolomnya. Lebar inline (mis.
   style="width:110px" di kolom Target) ditimpa max-width, yang memang
   berkuasa atas width. */
.d-tabel .d-input,
.d-table-row .d-input { max-width: 100%; min-width: 0; }
.d-tabel .d-satuan,
.d-table-row .d-satuan { display: flex; gap: 5px; min-width: 0; }
.d-tabel .d-satuan > .d-input,
.d-table-row .d-satuan > .d-input { flex: 1 1 auto; }

/* Deretan tombol aksi TURUN ke baris berikutnya alih-alih memaksa kolomnya
   melebar sampai tabel meluber. */
.d-tabel .d-aksi-baris,
.d-table-row .d-aksi-baris { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }

/* Tombol aksi dikecilkan supaya MUAT di kolomnya. Sel .d-keep sengaja
   overflow:visible (tombol tidak boleh terpotong), tapi itu berarti tombol
   yang kelewat lebar menindih kolom sebelahnya -- terukur "Nonaktifkan"
   menimpa label satuan 7px. Dengan 11px + padding 8px, tombol terpanjang
   (~88px) muat di kolom 96px. */
.d-tabel .d-aksi-baris .d-btn,
.d-table-row .d-aksi-baris .d-btn {
    font-size: 11px;
    padding: 5px 8px;
}
/* ...KECUALI tombol berlambang, yang justru di HP-lah ia dibutuhkan seukuran
   itu. Aturan di atas (padding 5px 8px) akan memipihkannya jadi 34x28 -- di
   bawah ambang sasaran sentuh yang nyaman. Ditulis terpisah di sini, bukan
   digabung ke aturan dasarnya, karena selektor di atas lebih spesifik dan
   akan menang. */
.d-tabel .d-aksi-baris .d-btn-aksi-ikon,
.d-table-row .d-aksi-baris .d-btn-aksi-ikon {
    padding: 0;
    width: 38px;
    height: 36px;
}

.d-tabel tr.rt-extra { display: none; }
.d-tabel tr.rt-extra.show { display: table-row; }

/* Tabel biasa (tanpa .d-tabel) dipadatkan saja -- jumlahnya sedikit dan
   kolomnya pendek. */
table:not(.d-tabel) th,
table:not(.d-tabel) td { padding: 8px 6px; font-size: 12px; }
table:not(.d-tabel) .hide-mobile { display: none; }

/* ---- .show-mobile-only: pasangan mobile-nya sempat hilang ----
   app.css memasang display:none untuk kelas ini di semua lebar, dan aturan
   yang MENYALAKANNYA kembali di HP dulu tinggal di mobile.css -- ikut
   terbuang waktu berkas itu diringkas dari 2.331 jadi 357 baris. Akibatnya
   tombol "Scan dengan Kamera HP" di checkout.php (tab Sore & Return)
   tidak pernah muncul di ponsel, padahal seluruh alurnya masih utuh:
   modal #cameraModal, html5-qrcode, dan listener-nya semua ada.
   Baris kedua perlu karena .d-btn memakai inline-flex untuk menengahkan
   ikon & teksnya -- kalau dipaksa block, isinya kembali rata kiri. */
.show-mobile-only { display: block; }
.d-btn.show-mobile-only { display: inline-flex; }

/* ---- Toast harus menang atas modal kamera scan ----
   .camera-modal ber-z-index 200, jadi dengan nilai desktop (80) hasil scan
   lewat kamera HP diumumkan DI BALIK layar hitam kamera dan tidak pernah
   terlihat. Kenaikan ini sengaja dikurung di lebar HP: satu-satunya pemicu
   modal itu tombol .show-mobile-only di checkout.php, yang di desktop
   display:none -- jadi di sana modalnya mustahil terbuka dan tumpukan
   desktop tidak perlu ikut diutak-atik sama sekali.

   Dipilih @media, bukan body:has(.camera-modal.show), supaya tidak
   bergantung pada dukungan :has(): kalau :has() tidak didukung, toast diam-
   diam turun lagi ke 80 dan bug-nya balik tanpa ketahuan. */
.d-toast { z-index: 400; }
/* Isi <details> adalah wadah TERSENDIRI di dalam kartu, jadi overflow milik
   kartu tidak menolongnya -- tabel di dalam <details> (mis. "Resi Belum
   Discan" di Scan) tetap mendorong halaman melebar tanpa aturan ini. */
.details-card-body { overflow-x: auto; padding: 0 16px 16px; }

/* Bar filter tidak menempel di HP: topbar-nya sudah position:fixed, jadi
   sticky top:0 akan menyelinap di baliknya. */
.d-filter { position: static; padding: 12px 14px; }
.d-lap-bar { position: static; }

/* ---- BAR CARI + TAB (Stok, Transaksi) ----
   Di desktop kotak cari dan tab strip duduk sebaris. Di HP tidak muat, dan
   cara gagalnya diam-diam: .d-seg dipatok `flex: 0 0 auto` (di Stok 5 tab =
   315px, sudah selebar layar) sementara .d-search `flex: 1; min-width: 0`,
   jadi kotak carinya diperas sampai LEBAR 0 -- bukan sekadar sempit, tapi
   lenyap sepenuhnya di balik tab strip. Terukur di 390px: .d-search width 0,
   input #cariStok width 4px, keduanya bertumpuk dengan .d-seg.
   Ditumpuk dua baris. Urutan DOM dipertahankan (cari di atas, tab di bawah)
   supaya urutan fokus keyboard tetap sama dengan urutan yang terlihat. */
.d-stok-bar,
.d-trx-bar { flex-wrap: wrap; }
.d-stok-bar > .d-search,
.d-trx-bar  > .d-search,
.d-stok-bar > .d-seg,
.d-trx-bar  > .d-seg { flex: 1 1 100%; min-width: 0; }

/* ---- TABEL GRID (.d-table): TETAP TABEL, TIDAK DITUMPUK ----
   Keputusan pemilik 15 Agu 2026, membalik pola sebelumnya: "jangan ada card
   vertikal untuk tabel, jadikan ke samping saja". Yang dulu ditumpuk jadi
   label-atas-nilai kini kembali jadi baris menyamping seperti di desktop.

   Supaya tetap muat tanpa gulir mendatar, kolom yang tidak krusial DIBUANG
   di HP, bukan dikecilkan: d_table() menandainya dengan .d-hp-off (lihat
   opsi 'hp' => false di includes/ui.php) dan sekaligus menyiapkan
   --d-kolom-hp -- daftar lebar kolom yang TERSISA saja. Tanpa var kedua itu
   grid tetap memesan tempat untuk kolom yang sudah disembunyikan, dan
   kepala tabel jadi meleset dari isinya. */
.d-table-head,
.d-table-row {
    grid-template-columns: var(--d-kolom-hp, var(--d-kolom));
    gap: 8px;
}
/* min-width:0 WAJIB di kedua baris: jalur fr tidak mau menyusut di bawah
   ukuran isinya tanpa ini, jadi tabel tetap meluber walau lantai minmax
   sudah dilepas. */
.d-table-head > *,
.d-table-row > * { min-width: 0; }
/* Padding & border kepala DISAMAKAN dengan baris. Keduanya grid terpisah,
   jadi jalur fr-nya dihitung dari lebar-isi kotaknya masing-masing: selisih
   border 1px saja sudah membuat kolom meleset ~1px dan makin menumpuk ke
   kanan. Border transparan, cuma untuk menyamakan perhitungan. */
.d-table-head {
    padding: 0 10px 8px;
    border-left: 1px solid transparent;
    border-right: 1px solid transparent;
    font-size: 10px;
}
/* Judul kolom DIBUNGKUS, tidak dipotong ellipsis (permintaan pemilik 15 Agu
   2026: "jangan ditulis menjadi ... ketika kepanjangan, tulis seutuhnya").
   "Target Bulanan" jadi dua baris, bukan "TARGET BUL...". align-items:end
   pada kepala supaya judul satu baris & dua baris tetap rata di bawah. */
.d-table-head {
    align-items: end;
    line-height: 1.25;
}
.d-table-head > * { white-space: normal; overflow-wrap: anywhere; }
.d-table-body { gap: 6px; }
.d-table-row { padding: 10px; }

/* Kolom yang dinyatakan tidak krusial untuk layar HP. */
.d-table-head > .d-hp-off,
.d-table-row > .d-hp-off { display: none; }

/* Isi DI DALAM sel yang berbeda antara HP & desktop -- beda dari .d-hp-off
   di atas yang membuang SATU KOLOM PENUH. Dipakai sub-baris di bawah No.
   Resi pada Barang Keluar: nomor pesanan di desktop, jam impor di HP.
   Server merender dua-duanya; di sinilah salah satunya dipilih. */
.d-hp-sembunyi { display: none; }
.d-hp-saja     { display: block; }


/* Isi sel juga DIBUNGKUS, tidak dipotong. Barisnya jadi lebih tinggi -- itu
   konsekuensi yang diterima pemilik, asal tidak ada yang hilang.
   overflow-wrap:anywhere: kata tanpa spasi (SKU panjang, nomor resi) tetap
   dipatahkan di tengah, jadi tidak ada yang meluber keluar kolomnya. */
.d-table-row > * {
    font-size: 12.5px;
    min-width: 0;
    overflow: visible;
    white-space: normal;
    overflow-wrap: anywhere;
}
/* (.d-keep tidak perlu aturan overflow lagi di sini -- sejak isinya
   dibungkus, seluruh sel sudah overflow:visible.) */

.d-pangsa { justify-content: flex-start; }

/* ---- SHEET: jadi bottom sheet ----
   Di HP inilah bentuk yang benar, dan kebetulan cocok dengan istilah
   "bottom sheet" di README handoff -- prototipe desktopnya sendiri
   menengahkan (lihat catatan di bagian 16). */
.d-sheet-backdrop { align-items: flex-end; padding: 0; }
.d-sheet {
    max-width: none;
    max-height: 92vh;
    border-radius: 26px 26px 0 0;
    animation: d-sheetUpMobile .42s cubic-bezier(.2,.95,.25,1.02);
}
.d-sheet-head { border-radius: 26px 26px 0 0; padding: 18px 20px 12px; }
.d-sheet-isi  { padding: 16px 20px 24px; }
.d-sheet-aksi { flex-direction: column-reverse; }
.d-foto-lihat img { max-height: calc(92vh - 140px); }
.d-sheet-aksi > * { width: 100%; }

/* Action bar sticky duduk di atas tab bar, bukan di baliknya.
   var() dengan fallback: kalau mobile.css berubah, ini turun jadi 0
   dan barnya tetap terlihat, cuma menempel dasar layar. */
.d-actionbar { bottom: var(--m-tabbar-h, 0px); flex-wrap: wrap; }
.d-actionbar-aksi { margin-left: 0; width: 100%; }

.d-chart-legenda { gap: 10px; }
/* Rasio ponsel (380x230) menggantikan rasio desktop (880x210): di kartu
   selebar ~315px yang desktop cuma setinggi 75px. */
.d-tren-lebar  { display: none; }
.d-tren-sempit { display: block; }
.d-field-baris { grid-template-columns: 1fr; }

}

@keyframes d-sheetUpMobile {
    from { opacity: 0; transform: translateY(100%); }
    to   { opacity: 1; transform: none; }
}


/* =========================================================================
   HALAMAN DI LUAR HANDOFF -- login, lupa password, set password, menu staff
   =========================================================================
   Dipindahkan dari assets/style.css saat berkas itu dihapus (Tahap 7b).
   Keputusan user: halaman-halaman ini "ikut token baru, layout tetap" --
   jadi aturannya sengaja TIDAK dirancang ulang, cuma diangkut. Warnanya
   ikut berubah sendiri karena semua menyebut token (--primary, --card,
   --text) yang kini bernilai palet baru; nama token lama yang tidak ada di
   app.css dipetakan lewat blok "Alias token lama" di bagian atas berkas.
   ========================================================================= */

html[data-theme="dark"] .badge-warn { background: rgba(255,149,0,0.2); color: #ffb84d; }

html[data-theme="dark"] .badge-kuning { background: rgba(255,204,0,0.22); color: #e0c04a; }

html[data-theme="dark"] .badge-danger { background: rgba(255,59,48,0.2); color: #ff6961; }

.brand-mark {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    flex-shrink: 0;
    border-radius: 9px;
    background: rgba(255,255,255,0.14);
    box-shadow: inset 0 0 0 1px rgba(255,255,255,0.25), 0 1px 2px rgba(0,0,0,0.15);
    font-weight: 700;
    font-size: 15px;
    letter-spacing: -0.02em;
    color: #fff;
}

/* Tabel yang lebih ramping (dipakai di Daftar Produk) supaya tidak terlalu lebar */
.table-compact th, .table-compact td {
    font-size: 13px;
    padding: 9px 11px;
}

.badge-warn { background: rgba(255,149,0,0.15); color: #c93400; }

.badge-kuning { background: rgba(255,204,0,0.22); color: #8a6d00; }

.badge-danger { background: rgba(255,59,48,0.14); color: #d70015; }

.badge-muted { background: rgba(110,110,115,0.12); color: var(--muted); }

.field-password-wrap { position: relative; }

.field-password-wrap input { padding-right: 42px; }

.btn-toggle-password {
    position: absolute;
    right: 4px;
    top: 50%;
    transform: translateY(-50%);
    background: none;
    border: none;
    cursor: pointer;
    padding: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--muted);
    border-radius: 6px;
    transition: color 0.15s var(--ease), background 0.15s var(--ease);
}

.btn-toggle-password:hover { color: var(--text); background: var(--hairline-soft); }

.btn-toggle-password svg { width: 19px; height: 19px; }

/* ================= HALAMAN LOGIN & SETUP ADMIN ================= */
body.login-page {
    background: linear-gradient(160deg, var(--sidebar-color-dark) 0%, var(--sidebar-color) 55%, var(--sidebar-color-light) 100%);
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
}

.login-wrap {
    width: 100%;
    padding: 20px;
    display: flex;
    justify-content: center;
    transition: opacity 0.3s ease, transform 0.3s ease;
}

.login-card {
    background: var(--card);
    border-radius: 24px;
    padding: 36px 32px;
    width: 100%;
    max-width: 400px;
    box-shadow: var(--shadow-lg);
}

.login-brand {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    font-size: 19px;
    font-weight: 600;
    letter-spacing: -0.01em;
    color: var(--text);
    margin-bottom: 6px;
}

.login-brand .brand-mark {
    color: #fff;
    font-weight: 700;
    align-items: center;
    justify-content: center;
}

.login-subtitle {
    color: var(--muted);
    font-size: 13.5px;
    text-align: center;
    margin: 0 0 22px;
}

.login-btn {
    width: 100%;
    padding: 12px 16px;
    font-size: 15px;
    background: var(--sidebar-color);
    margin-top: 6px;
}

.login-btn:hover { background: var(--sidebar-color-dark); }

/* ================= BOOT / GREETING SCREEN (setelah login berhasil) ================= */
.boot-screen {
    position: fixed;
    inset: 0;
    z-index: 999;
    overflow: hidden;
    background: linear-gradient(180deg, var(--sidebar-color-dark) 0%, var(--sidebar-color-light) 100%);
    opacity: 1;
    animation: bootScreenIn 0.7s var(--ease);
    transition: opacity 0.8s var(--ease);
    padding: 24px;
    text-align: center;
}

.boot-screen.fade-out { opacity: 0; }

/* Langit sesuai jam login (ditentukan lewat JS, lihat login.php) --
   gradasi dipilih supaya tetap lembut/tidak silau, bukan warna jenuh
   penuh, karena ini cuma latar belakang sebentar di belakang teks. */
.boot-screen.boot-pagi {
    background: linear-gradient(180deg, #3d5b82 0%, #7fa6c9 45%, #f4c99a 100%);
    --boot-tanggal: #4c7090;
    --boot-sapaan:  #12293f;
    --boot-nama:    #516c83;
    --boot-garis:   #c04a2a;
}

.boot-screen.boot-siang {
    background: linear-gradient(180deg, #4f8fc7 0%, #86bfe6 55%, #cfe7f5 100%);
    /* Siang punya langitnya sendiri (biru penuh, tanpa semburat jingga di
       bawah), tapi PALETNYA sama dengan pagi -- kedua langit sama-sama biru
       terang, jadi keduanya butuh teks gelap yang sama. */
    --boot-tanggal: #4c7090;
    --boot-sapaan:  #12293f;
    --boot-nama:    #516c83;
    --boot-garis:   #c04a2a;
}

.boot-screen.boot-sore {
    background: linear-gradient(180deg, #3c3a63 0%, #a4536f 42%, #e6875e 72%, #ffce97 100%);
    --boot-tanggal: #e4c3ca;
    --boot-sapaan:  #ffffff;
    --boot-nama:    #f1d6d4;
    --boot-garis:   #f7e7e6;
}

.boot-screen.boot-malam {
    background: linear-gradient(180deg, #05070d 0%, #0f1626 55%, #1c2942 100%);
    --boot-tanggal: #9395a1;
    --boot-sapaan:  #f5f5f7;
    --boot-nama:    #a4a5af;
    --boot-garis:   #c8354f;
}

/* Gambar dipasang di layer .boot-scene (bukan langsung di .boot-screen)
   supaya bisa di-zoom pelan tanpa ikut menggeser teks/konten. */
.boot-screen.boot-malam .boot-scene {
    background: url('img/boot-malam.jpg') center center / cover no-repeat;
}

.boot-screen.boot-pagi .boot-scene {
    background: url('img/boot-pagi.jpg') center center / cover no-repeat;
}

.boot-screen.boot-siang .boot-scene {
    background: url('img/boot-siang.jpg') center center / cover no-repeat;
}

.boot-screen.boot-sore .boot-scene {
    background: url('img/boot-sore.jpg') center center / cover no-repeat;
}

/* Elemen dekoratif (matahari/awan/bintang) -- murni visual, tidak
   mengganggu pembaca layar (aria-hidden di HTML) & tidak menangkap klik.
   Dizoom pelan ("Ken Burns", kalem ala Apple) di keempat tema -- foto malam
   maupun langit awan pagi/siang/sore -- supaya latarnya terasa hidup. */
.boot-scene {
    position: absolute;
    inset: 0;
    z-index: 0;
    overflow: hidden;
    pointer-events: none;
    animation: bootPhotoZoom 26s ease-in-out infinite alternate;
}

.boot-stars {
    position: absolute;
    inset: 0;
    opacity: 0;
    background-image:
        radial-gradient(1.6px 1.6px at 10% 20%, rgba(255,255,255,1), transparent),
        radial-gradient(1.6px 1.6px at 25% 45%, rgba(255,255,255,0.85), transparent),
        radial-gradient(1.2px 1.2px at 40% 15%, rgba(255,255,255,1), transparent),
        radial-gradient(1.6px 1.6px at 55% 62%, rgba(255,255,255,0.8), transparent),
        radial-gradient(1.2px 1.2px at 70% 25%, rgba(255,255,255,1), transparent),
        radial-gradient(1.8px 1.8px at 85% 50%, rgba(255,255,255,0.9), transparent),
        radial-gradient(1.2px 1.2px at 15% 72%, rgba(255,255,255,0.85), transparent),
        radial-gradient(1.6px 1.6px at 33% 82%, rgba(255,255,255,0.95), transparent),
        radial-gradient(1.2px 1.2px at 60% 88%, rgba(255,255,255,0.85), transparent),
        radial-gradient(1.8px 1.8px at 92% 78%, rgba(255,255,255,1), transparent),
        radial-gradient(1.2px 1.2px at 5% 40%, rgba(255,255,255,0.8), transparent),
        radial-gradient(1.6px 1.6px at 47% 30%, rgba(255,255,255,1), transparent),
        radial-gradient(1.2px 1.2px at 78% 10%, rgba(255,255,255,0.9), transparent),
        radial-gradient(1.6px 1.6px at 95% 35%, rgba(255,255,255,0.8), transparent),
        radial-gradient(1.2px 1.2px at 20% 58%, rgba(255,255,255,1), transparent),
        radial-gradient(1.4px 1.4px at 8% 88%, rgba(255,255,255,0.85), transparent),
        radial-gradient(1.2px 1.2px at 30% 8%, rgba(255,255,255,0.9), transparent),
        radial-gradient(1.6px 1.6px at 50% 92%, rgba(255,255,255,0.85), transparent),
        radial-gradient(1.2px 1.2px at 65% 8%, rgba(255,255,255,1), transparent),
        radial-gradient(1.4px 1.4px at 75% 68%, rgba(255,255,255,0.8), transparent),
        radial-gradient(1.2px 1.2px at 88% 20%, rgba(255,255,255,0.9), transparent),
        radial-gradient(1.6px 1.6px at 3% 60%, rgba(255,255,255,0.95), transparent),
        radial-gradient(1.2px 1.2px at 42% 68%, rgba(255,255,255,0.85), transparent),
        radial-gradient(1.4px 1.4px at 98% 58%, rgba(255,255,255,0.9), transparent);
    background-repeat: no-repeat;
}

/* Kerlip tipis di atas foto -- aksen halus, bukan lapisan bintang penuh
   (foto malam sudah punya bintangnya sendiri), jadi rentang opacity-nya
   sengaja dibuat rendah lewat keyframe terpisah dari tema lain.
   opacity statis WAJIB ada: basis .boot-stars itu opacity 0, dan pada
   prefers-reduced-motion animasinya dimatikan -- tanpa nilai statis ini
   bintangnya hilang sama sekali untuk pengguna tersebut. */
.boot-screen.boot-malam .boot-stars {
    opacity: 0.3;
    animation: bootTwinkleSubtle 4.5s ease-in-out infinite;
}

/* Bintang jatuh -- streak yang muncul sekali lalu memudar, cukup untuk
   durasi tampil boot screen (~5 detik), tidak perlu berulang. */
.boot-shooting-star {
    position: absolute;
    width: 2px;
    height: 2px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 0 8px 1.5px rgba(255,255,255,0.9);
    opacity: 0;
}

.boot-shooting-star::after {
    content: "";
    position: absolute;
    top: 50%;
    right: 0;
    width: 90px;
    height: 1.5px;
    background: linear-gradient(90deg, rgba(255,255,255,0.85), rgba(255,255,255,0));
    transform: translateY(-50%) rotate(-35deg);
    transform-origin: right center;
}

.boot-screen.boot-malam .boot-shooting-star.s1 {
    top: 18%;
    left: 66%;
    animation: bootShootingStar1 1.5s linear 2.5s 1;
}

/* Scrim sekarang sangat tipis, bukan lagi peredup kuat.
   Dulu ia perlu gelap karena SEMUA tema memakai teks putih di atas foto
   terang. Ilustrasi baru memilih warna teksnya sendiri per tema (pagi pakai
   teks gelap), jadi meredupkan langit justru merusak keduanya: langitnya
   kotor DAN kontras teks gelap ikut turun. Yang tersisa cuma peneduh sangat
   halus untuk berjaga di layar yang sangat terang. */
.boot-scrim {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: none;
}

/* Sore & malam tetap dapat peneduh tipis: teksnya putih, dan di layar HP
   pemotongan 'cover' bisa mendaratkan teks di bagian langit yang paling
   terang (pita jingga di sore, pantulan bulan di malam). */
.boot-screen.boot-sore .boot-scrim {
    background: radial-gradient(ellipse 60% 42% at 50% 50%, rgba(60,20,40,0.16) 0%, rgba(60,20,40,0.06) 62%, rgba(60,20,40,0) 88%);
}

.boot-screen.boot-malam .boot-scrim {
    background: radial-gradient(ellipse 60% 42% at 50% 50%, rgba(0,0,0,0.22) 0%, rgba(0,0,0,0.08) 62%, rgba(0,0,0,0) 88%);
}

.boot-content {
    position: relative;
    z-index: 2;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* gap 0: jarak antar unsur diatur sendiri-sendiri lewat margin, karena
       mockup memakai jarak yang BERBEDA di tiap celah (tanggal->sapaan rapat,
       nama->garis renggang). Satu nilai gap tidak bisa meniru itu. */
    gap: 0;
}

/* Font "Hello" ala Apple: bentuk rounded/friendly dari sistem font.
   ui-rounded & -apple-system-rounded adalah nilai khusus yang dikenali
   Safari/WebKit untuk memakai varian SF Pro Rounded milik sistem (tanpa
   perlu mengunggah file font Apple, yang memang tidak boleh didistribusikan
   di luar platform Apple). Di browser lain otomatis jatuh ke system-ui. */
.boot-tanggal,
.boot-greeting,
.boot-name {
    font-family: -apple-system-rounded, ui-rounded, "SF Pro Rounded", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}

/* Baris tanggal & jam. Huruf kapital berjarak lebar -- ini yang membuatnya
   terbaca sebagai keterangan, bukan judul kedua. */
.boot-tanggal {
    font-size: clamp(11px, 1.05vw, 14px);
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--boot-tanggal, rgba(255,255,255,0.75));
    margin: 0 0 18px;
    opacity: 0;
    transform: translateY(8px);
    animation: bootTextIn 0.9s var(--ease) 0.35s forwards;
}

.boot-greeting {
    font-size: clamp(32px, 4.4vw, 56px);
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1.1;
    color: var(--boot-sapaan, #fff);
    margin: 0;
    opacity: 0;
    transform: translateY(10px);
    animation: bootTextIn 0.9s var(--ease) 0.75s forwards;
}

.boot-name {
    font-size: clamp(16px, 1.55vw, 23px);
    font-weight: 400;
    letter-spacing: 0;
    color: var(--boot-nama, rgba(255,255,255,0.92));
    margin: 10px 0 0;
    opacity: 0;
    transform: translateY(8px);
    animation: bootTextIn 0.9s var(--ease) 1.35s forwards;
}

/* Garis aksen pendek penutup. Warnanya beda per tema (merah tua di pagi,
   putih di sore, merah terang di malam) -- diambil dari mockup, bukan satu
   warna merek yang dipaksakan ke tiga langit yang kontrasnya jauh berbeda. */
.boot-rule {
    width: 54px;
    height: 3px;
    border-radius: 999px;
    background: var(--boot-garis, #fff);
    margin: 30px 0 0;
    opacity: 0;
    transform: translateY(6px);
    animation: bootTextIn 0.7s var(--ease) 1.9s forwards;
}

@media (prefers-reduced-motion: reduce) {
    .boot-tanggal, .boot-greeting, .boot-name, .boot-rule {
        animation: none !important;
        opacity: 1 !important;
        transform: none !important;
    }
    .boot-stars {
        animation: none !important;
    }
    .boot-shooting-star {
        display: none !important;
    }
    .boot-screen { transition: none; animation: none !important; opacity: 1 !important; }
    .boot-scene { animation: none !important; transform: none !important; }
    .login-wrap { transition: none !important; }
}

@keyframes bootPhotoZoom {
    from { transform: scale(1); }
    to { transform: scale(1.08); }
}

@keyframes bootTwinkleSubtle {
    0%, 100% { opacity: 0.18; }
    50% { opacity: 0.4; }
}

@keyframes bootTextIn {
    to { opacity: 1; transform: translateY(0); }
}

@keyframes bootShootingStar1 {
    0% { opacity: 0; transform: translate(0, 0); }
    45% { opacity: 1; }
    68% { opacity: 1; }
    100% { opacity: 0; transform: translate(-170px, 115px); }
}

/* =====================================================================
   LAYAR LOGIN (handoff design_handoff_login -- opsi 2a gelap / 1a terang)
   =====================================================================
   Panel merek kiri + form kanan. Warnanya TIDAK dipatok burgundy mati:
   semuanya diturunkan dari --primary, jadi tema tersimpan DAN warna aksen
   pilihan user ikut berlaku di layar login (aturan handoff no.4: "Login
   mengikuti tema tersimpan", bukan selalu terang).

   Handoff memberi dua nilai burgundy: #c04a2a untuk ground terang dan
   #a82440 untuk ground gelap ("satu langkah lebih terang supaya kontrasnya
   cukup"). Itu warna yang sama dinaikkan terangnya, jadi di sini dihitung
   dari --primary alih-alih ditulis dua kali -- supaya aksen non-burgundy
   pun dapat perlakuan sama. */
/* Layar login mengisi jendela penuh -- tanpa ini body membawa margin
   bawaan browser dan panel split-nya tidak menempel tepi. */
body.d-log-body { margin: 0; background: var(--bg); }

.d-log {
    --d-log-aksen: var(--primary);
    /* Cadangan untuk mesin tanpa warna relatif: campur sedikit putih. */
    --d-log-aksen-terang: color-mix(in srgb, var(--primary) 88%, #fff);
    --d-log-panel-1: var(--primary);
    --d-log-panel-2: var(--primary-dark);
    --d-log-teks-panel: #fff;
    --d-log-teks-panel-sub: rgba(255, 255, 255, .72);
    --d-log-teks-panel-poin: rgba(255, 255, 255, .9);
    --d-log-teks-panel-kaki: rgba(255, 255, 255, .5);
    --d-log-logo-bg: rgba(255, 255, 255, .18);
    --d-log-logo-br: rgba(255, 255, 255, .28);
    --d-log-centang-bg: rgba(255, 255, 255, .2);
    --d-log-fokus-tebal: 14%;

    display: grid;
    grid-template-columns: .86fr 1fr;
    min-height: 100vh;
    background: var(--bg);
}
/* Warna relatif: menaikkan lightness, bukan mencampur putih -- hasilnya
   mempertahankan saturasi. #c04a2a jadi rgb(170,34,62), praktis identik
   dengan #a82440 yang diminta handoff; color-mix dengan putih hanya
   memberi rgb(154,55,75) yang jelas pudar.

   `calc(l + 7)` TANPA satuan persen: di sintaks warna relatif hsl(), `l`
   resolve jadi NUMBER, bukan percentage -- `calc(l + 7%)` mencampur dua
   tipe dan seluruh deklarasinya dibuang browser (tombol Masuk sempat
   kehilangan latarnya sama sekali di mode gelap karena ini).

   Kondisi @supports HARUS menguji bentuk yang benar-benar dipakai:
   `hsl(from red h s l)` saja lolos di Chrome sementara versi calc-nya
   tidak, jadi penjaga yang terlalu longgar akan meloloskan nilai rusak.
   Ditulis SETELAH nilai color-mix di atas supaya mesin tanpa dukungan
   tetap dapat cadangannya. */
@supports (color: hsl(from red h s calc(l + 7))) {
    .d-log { --d-log-aksen-terang: hsl(from var(--primary) h s calc(l + 7)); }
}

html[data-theme="dark"] .d-log {
    --d-log-aksen: var(--d-log-aksen-terang);
    --d-log-panel-1: color-mix(in srgb, var(--primary) 42%, #000);
    --d-log-panel-2: color-mix(in srgb, var(--primary) 9%, #000);
    --d-log-teks-panel: #f5f5f7;
    --d-log-teks-panel-sub: rgba(245, 245, 247, .6);
    --d-log-teks-panel-poin: rgba(245, 245, 247, .82);
    --d-log-teks-panel-kaki: rgba(245, 245, 247, .4);
    --d-log-logo-bg: rgba(255, 255, 255, .1);
    --d-log-logo-br: rgba(255, 255, 255, .16);
    --d-log-centang-bg: color-mix(in srgb, var(--d-log-aksen-terang) 60%, transparent);
    --d-log-fokus-tebal: 26%;
}

/* ---------------------------------------------- panel merek (kiri) ---- */
.d-log-merek {
    position: relative;
    overflow: hidden;
    padding: 52px 46px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    background: linear-gradient(150deg, var(--d-log-panel-1), var(--d-log-panel-2));
    border-right: 1px solid var(--border);
}
.d-log-glow {
    position: absolute;
    width: 440px;
    height: 440px;
    top: -150px;
    right: -170px;
    border-radius: 50%;
    background: radial-gradient(circle, color-mix(in srgb, var(--d-log-aksen-terang) 55%, transparent), transparent 68%);
    filter: blur(10px);
    pointer-events: none;
}
/* Semua isi panel duduk di ATAS glow. */
.d-log-merek > *:not(.d-log-glow) { position: relative; }

.d-log-merek-atas { display: flex; align-items: center; gap: 12px; }
.d-log-logo {
    width: 38px;
    height: 38px;
    border-radius: 11px;
    background: var(--d-log-logo-bg);
    border: 1px solid var(--d-log-logo-br);
    display: grid;
    place-items: center;
    font-size: 17px;
    font-weight: 700;
    color: var(--d-log-teks-panel);
    cursor: default;
    transition: transform .4s var(--d-ease-pop), background .3s, box-shadow .4s;
}
.d-log-merek-atas:hover .d-log-logo {
    transform: rotate(-6deg) scale(1.1);
    background: color-mix(in srgb, var(--d-log-logo-bg) 70%, #fff);
    box-shadow: 0 10px 24px -10px color-mix(in srgb, var(--d-log-aksen-terang) 90%, transparent);
}
.d-log-nama {
    font-size: 14.5px;
    font-weight: 700;
    letter-spacing: -.01em;
    color: var(--d-log-teks-panel);
}

.d-log-judul {
    margin: 0;
    font-size: 38px;
    line-height: 1.14;
    font-weight: 700;
    letter-spacing: -.035em;
    color: var(--d-log-teks-panel);
    max-width: 340px;
    text-wrap: pretty;
}
.d-log-sub {
    margin: 18px 0 0;
    font-size: 14px;
    line-height: 1.6;
    color: var(--d-log-teks-panel-sub);
    max-width: 320px;
}
.d-log-poin {
    margin: 34px 0 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 13px;
}
.d-log-poin li {
    display: flex;
    align-items: center;
    gap: 11px;
    font-size: 13px;
    color: var(--d-log-teks-panel-poin);
    cursor: default;
    transition: transform .35s var(--d-ease-out), color .3s;
}
.d-log-poin li:hover { transform: translateX(6px); color: #fff; }
.d-log-centang {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    flex: none;
    display: grid;
    place-items: center;
    font-size: 10px;
    color: #fff;
    background: var(--d-log-centang-bg);
}
.d-log-kaki { font-size: 11.5px; color: var(--d-log-teks-panel-kaki); }

/* ----------------------------------------------- panel form (kanan) --- */
.d-log-form {
    padding: 52px 56px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    background: var(--bg);
}
.d-log-form-isi { width: 100%; max-width: 380px; }

.d-log-h2 {
    margin: 0;
    font-size: 27px;
    font-weight: 700;
    letter-spacing: -.03em;
    color: var(--text);
}
.d-log-h2-sub { margin: 7px 0 0; font-size: 13.5px; color: var(--muted); }

.d-log-form form {
    margin-top: 30px;
    display: flex;
    flex-direction: column;
    gap: 16px;
}
.d-log-field { display: flex; flex-direction: column; gap: 7px; }
.d-log-label { font-size: 12.5px; font-weight: 600; color: var(--muted); }
.d-log-label-baris { display: flex; justify-content: space-between; align-items: baseline; }

.d-log-lupa {
    font-size: 12.5px;
    font-weight: 600;
    color: var(--d-log-aksen);
    text-decoration: none;
    display: inline-block;
    transition: color .25s, transform .3s var(--d-ease-out);
}
.d-log-lupa:hover { color: var(--text); transform: translateX(-2px); }

.d-log-input {
    width: 100%;
    box-sizing: border-box;
    padding: 13px 14px;
    font: inherit;
    font-size: 14px;
    color: var(--text);
    background: var(--card-solid);
    border: 1px solid var(--border);
    border-radius: 12px;
    transition: border-color .25s, box-shadow .3s, transform .3s var(--d-ease-out);
}
.d-log-input:hover {
    border-color: color-mix(in srgb, var(--border) 60%, var(--text));
    box-shadow: 0 8px 20px -14px rgba(0, 0, 0, .9);
    transform: translateY(-1px);
}
.d-log-input:focus {
    outline: none;
    border: 1.5px solid var(--d-log-aksen);
    box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--d-log-aksen) var(--d-log-fokus-tebal), transparent);
}
/* Ring fokus melebar saat kursor juga di atasnya -- disebut khusus di
   handoff untuk isian password. */
.d-log-input:focus:hover {
    box-shadow: 0 0 0 5px color-mix(in srgb, var(--d-log-aksen) calc(var(--d-log-fokus-tebal) + 8%), transparent);
    transform: translateY(-1px);
}
/* Chrome mengecat isian ter-autofill biru muda dengan !important internal --
   satu-satunya cara menahannya adalah menimpa latar lewat inset box-shadow
   raksasa. Ring fokus ikut ditulis ulang di sini karena keduanya memakai
   properti yang sama; kalau tidak, isian yang diisi password manager
   kehilangan ring fokusnya. */
.d-log-input:-webkit-autofill,
.d-log-input:-webkit-autofill:hover {
    -webkit-text-fill-color: var(--text);
    -webkit-box-shadow: 0 0 0 1000px var(--card-solid) inset;
    caret-color: var(--text);
}
.d-log-input:-webkit-autofill:focus {
    -webkit-box-shadow: 0 0 0 1000px var(--card-solid) inset,
                        0 0 0 3.5px color-mix(in srgb, var(--d-log-aksen) var(--d-log-fokus-tebal), transparent);
}

.d-log-input.salah { border-color: var(--red); }
.d-log-input.salah:focus {
    border-color: var(--red);
    box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--red) 20%, transparent);
}

.d-log-input-bungkus { position: relative; display: flex; align-items: center; }
.d-log-input-bungkus .d-log-input { padding-right: 108px; }
.d-log-lihat {
    position: absolute;
    right: 12px;
    font: inherit;
    font-size: 12px;
    font-weight: 600;
    color: var(--d-log-aksen);
    background: none;
    border: 0;
    padding: 4px 7px;
    border-radius: 8px;
    cursor: pointer;
    transition: background .25s, color .25s, transform .3s var(--d-ease-pop);
}
.d-log-lihat:hover {
    background: color-mix(in srgb, var(--d-log-aksen) 26%, transparent);
    color: #fff;
    transform: scale(1.06);
}
.d-log-lihat:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--d-log-aksen) 32%, transparent);
}

/* --------------------------------------------------- ingat saya ------ */
.d-log-ingat {
    display: flex;
    align-items: center;
    gap: 9px;
    margin-top: 2px;
    font-size: 13px;
    color: var(--text);
    cursor: pointer;
    transition: transform .3s var(--d-ease-out);
}
.d-log-ingat:hover { transform: translateX(4px); }
/* Kotak asli disembunyikan TAPI tetap fokusabel & terbaca pembaca layar --
   bukan display:none, yang akan mengeluarkannya dari urutan tab. */
.d-log-ingat input {
    position: absolute;
    opacity: 0;
    width: 20px;
    height: 20px;
    margin: 0;
    cursor: pointer;
}
.d-log-kotak {
    width: 20px;
    height: 20px;
    border-radius: 6px;
    flex: none;
    display: grid;
    place-items: center;
    font-size: 11px;
    font-weight: 700;
    color: transparent;
    background: transparent;
    border: 1.5px solid var(--border);
    transition: transform .35s var(--d-ease-pop), box-shadow .3s, background .2s, border-color .2s, color .2s;
}
.d-log-ingat input:checked ~ .d-log-kotak {
    background: var(--d-log-aksen);
    border-color: var(--d-log-aksen);
    color: #fff;
}
.d-log-ingat:hover .d-log-kotak {
    transform: scale(1.14);
    box-shadow: 0 6px 16px -8px color-mix(in srgb, var(--d-log-aksen) 100%, transparent);
}
.d-log-ingat input:focus-visible ~ .d-log-kotak {
    box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--d-log-aksen) 30%, transparent);
}

/* ------------------------------------------------------- tombol ------ */
.d-log-tombol {
    margin-top: 8px;
    padding: 13px 15px;
    font: inherit;
    font-size: 14.5px;
    font-weight: 600;
    color: #fff;
    background: var(--d-log-aksen);
    border: 0;
    border-radius: 12px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    box-shadow: 0 12px 24px -14px color-mix(in srgb, var(--d-log-aksen) 70%, transparent);
    transition: transform .32s var(--d-ease-pop), box-shadow .35s, background .25s;
}
.d-log-tombol:hover {
    transform: translateY(-3px) scale(1.015);
    background: var(--d-log-aksen-terang);
    box-shadow: 0 22px 38px -18px color-mix(in srgb, var(--d-log-aksen) 95%, transparent);
}
.d-log-tombol:active {
    transform: translateY(0) scale(.985);
    box-shadow: 0 8px 18px -12px color-mix(in srgb, var(--d-log-aksen) 90%, transparent);
}
.d-log-tombol:focus-visible {
    outline: none;
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--d-log-aksen) 32%, transparent);
}
.d-log-tombol.memuat { opacity: .6; pointer-events: none; }
.d-log-spin {
    width: 15px;
    height: 15px;
    border-radius: 50%;
    border: 2px solid rgba(255, 255, 255, .35);
    border-top-color: #fff;
    display: none;
    animation: d-log-putar .7s linear infinite;
}
.d-log-tombol.memuat .d-log-spin { display: block; }
@keyframes d-log-putar { to { transform: rotate(360deg); } }

/* ------------------------------------------------- galat & catatan --- */
.d-log-galat { margin: 0; font-size: 12.5px; color: var(--red); }
.d-log-galat-blok { margin-top: 2px; }

/* Kabar berhasil di halaman publik (lupa_password.php, set_password.php).
   Sengaja BUKAN .alert generasi lama: halaman-halaman itu ikut tata letak
   split .d-log, dan .alert lama membawa radius, padding, serta warna yang
   tidak lagi nyambung dengan sekitarnya. */
.d-log-kabar {
    margin: 0;
    padding: 13px 15px;
    border-radius: 12px;
    border: 1px solid var(--hair);
    background: var(--field);
    font-size: 13px;
    line-height: 1.55;
    color: var(--text);
}
.d-log-kabar-ok    { border-color: color-mix(in srgb, var(--green) 45%, var(--hair)); }
.d-log-kabar-buruk { border-color: color-mix(in srgb, var(--red) 45%, var(--hair)); }
.d-log-kabar strong { font-weight: 700; }

/* Blok isi yang BUKAN form (mis. layar "password tersimpan" dan "tautan
   kedaluwarsa" di set_password.php) -- jarak & tumpukannya harus sama
   dengan form supaya tombolnya tidak terlihat menempel ke judul. */
.d-log-form .d-log-blok {
    margin-top: 30px;
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.d-log-catatan {
    margin: 26px 0 0;
    padding-top: 20px;
    border-top: 1px solid var(--hair);
    font-size: 12.5px;
    line-height: 1.6;
    color: var(--faint);
}
.d-log-catatan a {
    color: var(--d-log-aksen);
    font-weight: 600;
    text-decoration: none;
    transition: color .25s;
}
.d-log-catatan a:hover { text-decoration: none; }
.d-log-legal { margin: 10px 0 0; font-size: 12px; color: var(--faint); }
.d-log-legal a { color: var(--faint); text-decoration: none; }
.d-log-legal a:hover { color: var(--text); }

/* ----------------------------------------------- animasi masuk ------- */
/* backwards, BUKAN both -- 'both' menahan transform setelah animasi selesai
   sehingga semua hover lift di atas mati (peringatan eksplisit di handoff). */
@keyframes d-log-naik {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; }
}
.d-log-form-isi > *    { animation: d-log-naik .55s var(--d-ease-out) backwards; }
.d-log-form-isi > form { animation-delay: 60ms; }
.d-log-catatan,
.d-log-legal           { animation-delay: 120ms; }

/* ------------------------------------------------------ responsif ---- */
@media (max-width: 900px) {
    .d-log { grid-template-columns: 1fr; }
    .d-log-merek {
        padding: 30px 28px;
        gap: 20px;
        border-right: 0;
        border-bottom: 1px solid var(--border);
    }
    .d-log-judul { font-size: 25px; max-width: none; }
    .d-log-sub { margin-top: 12px; max-width: none; }
    .d-log-poin { margin-top: 22px; }
    .d-log-form { padding: 34px 28px 44px; }
    .d-log-form-isi { max-width: none; }
}
@media (max-width: 600px) {
    .d-log-poin { display: none; }
}

/* Hormati prefers-reduced-motion: animasi masuk dimatikan & lift hover
   diganti perubahan warna saja (aturan handoff). */
@media (prefers-reduced-motion: reduce) {
    .d-log-form-isi > * { animation: none; }
    .d-log-poin li:hover,
    .d-log-ingat:hover,
    .d-log-input:hover,
    .d-log-input:focus:hover,
    .d-log-lupa:hover,
    .d-log-tombol:hover,
    .d-log-lihat:hover { transform: none; }
    .d-log-merek-atas:hover .d-log-logo { transform: none; }
    .d-log-ingat:hover .d-log-kotak { transform: none; }
}


/* =========================================================================
   KABAR SCAN GAGAL -- kartu besar di tengah layar
   -------------------------------------------------------------------------
   Toast biasa (.d-toast) setinggi 40px di pojok bawah tidak terbaca oleh
   petugas packing: mereka memegang barang, berdiri satu-dua meter dari
   layar, dan yang paling perlu sampai justru ALASAN gagalnya. Jadi khusus
   kegagalan scan dipakai kartu ini. Scan berhasil TETAP toast -- kalau
   semua kejadian ditengahkan, yang darurat kehilangan artinya.

   Kelasnya di-scope sendiri (.d-scan-*), TIDAK menumpang .d-sheet: sheet
   punya kepala lengket, tombol tutup yang berputar, dan padding yang
   diasumsikan ada isian form di dalamnya.

   z-index 500: di atas .camera-modal (200) dan .d-toast (400) -- kabar ini
   harus tetap terbaca walau scan dilakukan lewat kamera HP.
   ========================================================================= */
.d-scan-kabar {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: rgba(0,0,0,.5);
    animation: d-backdropIn .18s ease;
    cursor: pointer; /* seluruh lapisan = area tutup */
}
.d-scan-kabar-kartu {
    width: 100%;
    max-width: 560px;
    padding: 34px 32px 30px;
    border-radius: 24px;
    text-align: center;
    background: var(--card-solid);
    border: 2px solid var(--red);
    box-shadow: 0 40px 90px -24px rgba(0,0,0,.55);
    animation: d-scanKabarIn .34s cubic-bezier(.2,.95,.25,1.05);
}
/* Sengaja TIDAK memakai d-sheetIn: keyframe itu mulai dari translateY(40px),
   dan selama animasinya belum tuntas kartunya duduk 40px di bawah titik
   tengah. Terukur meleset persis segitu saat animasi tidak sempat berjalan
   (tab di latar dicekik browser). Skala tidak menggeser titik pusat, jadi
   kartu ini selalu tepat di tengah berapa pun keadaan animasinya. */
@keyframes d-scanKabarIn {
    from { opacity: 0; transform: scale(.94); }
    to   { opacity: 1; transform: none; }
}
.d-scan-kabar-ikon {
    width: 62px;
    height: 62px;
    margin: 0 auto 18px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: var(--red);
    color: #fff;
    font-size: 34px;
    font-weight: 800;
    line-height: 1;
}
.d-scan-kabar-judul {
    margin: 0 0 10px;
    font-size: 26px;
    font-weight: 800;
    letter-spacing: -.02em;
    line-height: 1.15;
    color: var(--red);
}
/* Alasannya yang paling penting dibaca -- jadi ia yang paling besar setelah
   judul, bukan keterangan kecil abu-abu seperti di toast. */
.d-scan-kabar-teks {
    margin: 0;
    font-size: 17px;
    line-height: 1.5;
    color: var(--text);
    word-break: break-word;
}
.d-scan-kabar-tutup {
    margin-top: 22px;
    font-size: 13px;
    color: var(--muted);
}

/* Nada "dibatalkan": kejadian yang paling mahal kalau lolos (barang keluar
   gudang tanpa pemasukan), jadi kartunya berdenyut sampai ditutup. */
.d-scan-kabar-batal .d-scan-kabar-kartu {
    animation: d-scanKabarIn .34s cubic-bezier(.2,.95,.25,1.05), d-scanDenyut 1.1s ease-in-out .34s infinite;
}
@keyframes d-scanDenyut {
    0%, 100% { box-shadow: 0 40px 90px -24px rgba(0,0,0,.55), 0 0 0 0 rgba(255,59,48,.55); }
    50%      { box-shadow: 0 40px 90px -24px rgba(0,0,0,.55), 0 0 0 16px rgba(255,59,48,0); }
}

@media (max-width: 768px) {
    .d-scan-kabar { padding: 18px; }
    .d-scan-kabar-kartu { padding: 28px 22px 24px; border-radius: 20px; }
    .d-scan-kabar-judul { font-size: 22px; }
    .d-scan-kabar-teks  { font-size: 16px; }
}

@media (prefers-reduced-motion: reduce) {
    .d-scan-kabar,
    .d-scan-kabar-kartu,
    .d-scan-kabar-batal .d-scan-kabar-kartu { animation: none; }
}


/* =====================================================================
   KALKULATOR HARGA SHOPEE (kalkulator.php + assets/kalkulator.js)
   =====================================================================
   Dua kolom di desktop: kolom kiri yang digulir berisi input, kolom kanan
   berisi kartu hasil yang MENEMPEL -- angka laba harus selalu kelihatan
   sementara pemilik mengutak-atik program di bawah. Di HP keduanya
   menumpuk dan kartu hasil naik ke ATAS input (lihat blok mobile.css),
   karena di layar sempit yang dicari duluan justru angkanya.
   ===================================================================== */

.kalk-kolom {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    gap: 14px;
    align-items: start;
}
.kalk-kartu-hasil { position: sticky; top: 18px; }

/* ----- pencarian produk & kategori ----- */
/* Panel saran menggantung di atas isi berikutnya, jadi pembungkusnya wajib
   position:relative -- tanpa itu ia mendorong kartu memanjang tiap kali
   muncul dan seluruh kolom bergoyang saat mengetik. */
.kalk-cari-bungkus { position: relative; }

/* ...tapi position:relative saja belum cukup. .d-card memasang
   backdrop-filter, dan itu MEMBUAT STACKING CONTEXT: z-index:40 di panel
   saran hanya berlaku di dalam kartunya sendiri, jadi kartu berikutnya
   ("Arah hitung") yang digambar belakangan tetap menimpanya. Yang harus
   naik itu KARTUNYA, selagi ada panel yang terbuka. Sekalian batalkan
   overflow-x:auto bawaan HP (bagian tabel di atas) yang ikut memotong
   panel di tepi bawah kartu. */
.d-card:has(.kalk-saran:not([hidden])) {
    position: relative;
    z-index: 60;
    overflow: visible;
}
.kalk-saran {
    position: absolute;
    left: 0; right: 0; top: 100%;
    z-index: 40;
    margin-top: 6px;
    max-height: 320px;
    overflow-y: auto;
    background: var(--card-solid);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--d-shadow-pop);
    padding: 5px;
}
.kalk-saran-item {
    display: flex;
    flex-direction: column;
    gap: 2px;
    width: 100%;
    text-align: left;
    padding: 9px 11px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    cursor: pointer;
    font: inherit;
    color: var(--text);
}
.kalk-saran-item:hover,
.kalk-saran-item:focus-visible { background: var(--field); }
.kalk-saran-utama { font-weight: 600; font-size: 13.5px; }
.kalk-saran-sub   { font-size: 12px; color: var(--muted); }
.kalk-saran-kosong { padding: 12px; font-size: 13px; color: var(--muted); }

.kalk-terpilih {
    margin: 8px 0 0;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
}
.kalk-terpilih b { font-variant-numeric: tabular-nums; color: var(--text); }
.kalk-kunci {
    font-weight: 500;
    color: var(--faint);
    text-transform: none;
    letter-spacing: 0;
}

/* ----- baris program ----- */
.kalk-program .kalk-baris { align-items: center; }
.kalk-baris-kanan { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.kalk-kotak { position: relative; display: flex; align-items: center; }
/* Padding kanan berlaku di SEMUA pemakaian .kalk-kotak: itu tempat duduk
   .kalk-satuan, dan tanpanya angka yang rata-kanan menabrak satuannya.
   Lebar sempitnya cuma milik baris program -- kolom "Target untung" memakai
   kotak yang sama tapi selebar kartunya, jadi biarkan .d-input width:100%. */
.kalk-kotak .d-input { padding-right: 32px; }
.kalk-program .kalk-kotak .d-input { width: 92px; }
.kalk-satuan {
    position: absolute;
    right: 10px;
    font-size: 11.5px;
    color: var(--faint);
    pointer-events: none;
}
/* Baris yang dimatikan tetap terbaca (angkanya masih informatif) tapi jelas
   tidak ikut hitungan -- diredupkan, bukan disembunyikan. */
.kalk-mati { opacity: .45; }

/* ----- kartu hasil ----- */
.kalk-kosong, .kalk-mustahil {
    padding: 22px 4px;
    font-size: 13.5px;
    color: var(--muted);
    text-align: center;
}
.kalk-mustahil {
    text-align: left;
    background: color-mix(in srgb, var(--red) 8%, transparent);
    border: 1px solid color-mix(in srgb, var(--red) 26%, transparent);
    border-radius: var(--radius-md);
    padding: 14px 16px;
    color: var(--text);
}
.kalk-mustahil strong { color: var(--red); display: block; margin-bottom: 4px; }
.kalk-mustahil p { margin: 0; font-size: 13px; color: var(--muted); line-height: 1.5; }

.kalk-angka-utama {
    display: grid;
    gap: 10px;
    margin-bottom: 18px;
}
.kalk-angka {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 12px 14px;
    background: var(--surface-tint);
    border-radius: var(--radius-md);
}
.kalk-angka-label { font-size: 11.5px; font-weight: 600; color: var(--muted); letter-spacing: .01em; }
.kalk-angka-nilai {
    font-size: 24px;
    font-weight: 700;
    letter-spacing: -.02em;
    font-variant-numeric: tabular-nums;
}
.kalk-angka-ket { font-size: 12px; color: var(--faint); }
.kalk-angka.kalk-ok   .kalk-angka-nilai { color: var(--green); }
.kalk-angka.kalk-rugi .kalk-angka-nilai { color: var(--red); }

.kalk-rinci-judul {
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--faint);
    margin-bottom: 8px;
}
.kalk-rinci {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 64px 96px;
    align-items: center;
    gap: 10px;
    padding: 7px 0;
    border-bottom: 1px solid var(--hair);
}
.kalk-rinci:last-child { border-bottom: 0; }
.kalk-rinci-kiri { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.kalk-rinci-label { font-size: 13px; font-weight: 600; }
.kalk-rinci-ket   { font-size: 11.5px; color: var(--faint); }
.kalk-rinci-bar {
    height: 5px;
    border-radius: 3px;
    background: var(--field);
    overflow: hidden;
}
.kalk-rinci-bar span {
    display: block;
    height: 100%;
    border-radius: 3px;
    background: var(--primary);
}
.kalk-rinci-nilai { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; font-size: 13.5px; }
/* Modal bukan potongan Shopee -- dipisahkan supaya tidak terbaca sebagai
   salah satu biaya marketplace, tapi tetap perlu terlihat karena ia yang
   memakan sisa uang cair. */
.kalk-rinci-modal { border-top: 1px solid var(--border); border-bottom: 0; margin-top: 4px; padding-top: 10px; }
.kalk-rinci-modal .kalk-rinci-label { color: var(--muted); }

.kalk-skenario {
    margin-top: 14px;
    padding: 11px 13px;
    border-radius: var(--radius-md);
    background: color-mix(in srgb, var(--orange) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--orange) 26%, transparent);
    font-size: 12.5px;
    line-height: 1.55;
}
.kalk-skenario strong { display: block; margin-bottom: 2px; }

/* ----- tab Tarif Kategori ----- */
/* Daftar jenis produk resmi Shopee bisa 40 item dalam satu sel; dipotong
   satu baris supaya tabelnya tetap bisa dipindai, teks penuhnya tetap ada
   di DOM untuk pencarian. */
.kalk-jenis {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 12.5px;
    color: var(--muted);
}
.kalk-baris-mati { opacity: .45; }

@media (max-width: 900px) {
    .kalk-kolom { grid-template-columns: 1fr; }
    .kalk-kartu-hasil { position: static; }
}

/* -------------------------------------------------------------------------
   HALAMAN RINCIAN RESELLER -- irama vertikal
   -------------------------------------------------------------------------
   Blok halaman ini datang dari lima tempat berbeda (kartu .m-hero dari
   mobile.css, .d-seg, dua .d-card, grid dua kolom, lalu deretan tombol aksi),
   masing-masing membawa margin sendiri. Hasilnya jarak antar blok terukur
   0 / 14 / 28 / -14 / 18 px -- dan yang -14 px itu tumpang tindih sungguhan,
   kartu Tren Setoran tertimpa grid di bawahnya (diukur 4 Sep 2026).

   Diselesaikan dengan gap, bukan dengan menambal margin satu per satu: gap
   tidak bisa meruntuh (margin collapsing) dan tidak bisa negatif, jadi
   jaraknya dijamin sama berapa pun blok yang ditambahkan nanti.

   Di luar @media: halaman ini memang dipakai di lebar berapa pun. */
.d-rincian-blok { display: flex; flex-direction: column; gap: 16px; margin-top: 16px; }
/* Margin bawaan tiap blok dinolkan supaya tidak menumpuk di atas gap.
   Ditulis dengan [class] supaya kekhususannya (0,2,0) mengalahkan aturan asal
   blok-blok itu -- .m-hero & .m-aksi-reseller tinggal di mobile.css yang
   dimuat SESUDAH berkas ini, jadi selektor satu kelas biasa justru kalah. */
.d-rincian-blok > *,
.d-rincian-blok > [class] { margin-top: 0; margin-bottom: 0; }
/* <h2> pertama di dalam kolom grid: tanpa ini judulnya menempel ke tepi atas
   kolom sementara kolom sebelahnya tidak, dan dua kolomnya berhenti lurus. */
.d-rincian-blok .d-grid > div > h2:first-child { margin-top: 0; }

/* -------------------------------------------------------------------------
   23. LENCANA UMUR STOK
   Dipakai lencana_umur() di includes/umur_helper.php. Warnanya berarti
   "layak dilihat", BUKAN "sudah rusak" -- umur simpan sebenarnya belum
   diketahui, jadi ambang 30/60 hari sengaja longgar dan netral. Begitu
   pemilik tahu umur simpan aslinya, yang diganti ambangnya di
   lencana_umur(), bukan aturan di sini.
   ------------------------------------------------------------------------- */
.umur        { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 20px;
               white-space: nowrap; letter-spacing: .01em; }
.umur-hijau  { background: color-mix(in srgb, var(--green) 15%, transparent);  color: var(--hijau-teks); }
.umur-kuning { background: color-mix(in srgb, var(--orange) 18%, transparent); color: var(--kuning-teks); }
.umur-merah  { background: color-mix(in srgb, var(--red) 13%, transparent);    color: var(--red); }
.umur-kosong { color: var(--faint); }

/* -------------------------------------------------------------------------
   24. PEMINDAI (assets/scan.js)
   Modal kamera + kartu kabar gagal. Kartunya berada DI ATAS modal kamera:
   pesan kegagalan yang tertutup viewfinder sama saja dengan tidak ada.
   ------------------------------------------------------------------------- */
.scan-modal        { position: fixed; inset: 0; z-index: 200; display: none;
                     background: rgba(0,0,0,.82); padding: 18px; }
.scan-modal.buka   { display: flex; flex-direction: column; align-items: center; justify-content: center; }
.scan-reader       { width: min(92vw, 460px); border-radius: 18px; overflow: hidden; background: #000; }
.scan-modal-hint   { color: #fff; font-size: 13.5px; margin-top: 16px; text-align: center;
                     max-width: 34em; line-height: 1.6; opacity: .92; }
.scan-modal-tutup  { margin-top: 16px; }

.scan-kabar        { position: fixed; z-index: 500; left: 50%; top: 50%;
                     transform: translate(-50%, -50%); width: min(90vw, 420px);
                     background: var(--card-solid); border-radius: 20px; padding: 24px 22px 18px;
                     box-shadow: 0 24px 70px rgba(0,0,0,.34); border: 1px solid var(--border);
                     text-align: center; }
.scan-kabar-judul  { font-size: 19px; font-weight: 650; letter-spacing: -.02em; color: var(--red); }
.scan-kabar-pesan  { margin-top: 8px; font-size: 13.5px; color: var(--muted); line-height: 1.6; }
.scan-kabar-tutup  { margin-top: 18px; border: 0; border-radius: 12px; padding: 10px 22px;
                     background: var(--primary); color: #fff; font-size: 14px; font-weight: 550;
                     cursor: pointer; font-family: inherit; }

/* -------------------------------------------------------------------------
   25. PETA FREEZER (includes/peta_helper.php)
   Pengganti papan tulis gudang. Susunannya SENGAJA meniru bentuk ruangan --
   petugas menemukan freezer lewat posisinya, bukan lewat hurufnya. Karena
   itu peta ini boleh menggulir mendatar di layar sempit (satu-satunya
   tempat di aplikasi ini yang boleh), sementara tabel tidak pernah boleh:
   tabel yang dipampatkan tetap terbaca, peta yang dipampatkan kehilangan
   seluruh gunanya.
   ------------------------------------------------------------------------- */
/* padding-top 16px, bukan 4px: pil label kotak duduk di top:-11px, dan
   wadah bergulir ini overflow-y:hidden -- apa pun yang keluar dari kotak
   akan TERPOTONG, bukan meluber. Ini ketahuan dari layar pemilik: tulisan
   "RAK" dan "ALAS FORKLIFT" terpangkas separuh. */
.pf-bungkus     { overflow-x: auto; overflow-y: hidden; margin: 0 -4px; padding: 16px 4px 4px; }
/* min-width HARUS >= lebar minimum isinya, bukan sekadar "kira-kira selebar
   petanya". .pf-kiri itu petak ber-flex:1 dengan min-width:auto -- ia
   menolak menyusut di bawah 4 x 168 + 36 = 708px. Kalau .pf lebih sempit
   dari 708 + 26 + 158 = 892, kolom kanan (K/L/M) menjulur KELUAR wadah
   bergulirnya, dan luberan semacam itu tidak ikut terhitung sebagai lebar
   gulir: terukur di iPad melintang 1024px, kolom K/L/M lewat 85px dari tepi
   kartu sementara scrollLeft mentok di 4px. Bagian itu tidak bisa dijangkau
   dengan cara apa pun -- bukan "perlu digulir", tapi hilang.
   Karena itu angka di bawah ini diturunkan dari lebar petaknya, bukan
   dikira-kira; kalau lebar kolom di media query berubah, angka ini ikut. */
.pf             { display: flex; gap: 26px; align-items: flex-start; min-width: 920px; }
.pf-kiri        { flex: 1; display: grid; gap: 12px;
                  grid-template-columns: repeat(4, minmax(168px, 1fr)); }
.pf-kanan       { display: grid; gap: 12px; grid-template-columns: minmax(158px, 178px); }

/* GARIS KOTAK MENGIKUTI BAHASA KARTU, BUKAN GARIS PAPAN TULIS.
   Versi pertama memakai `2px solid var(--text)` -- meniru spidol hitam di
   papan tulis aslinya. Di layar itu keliru: var(--text) di mode gelap nyaris
   putih, jadi tiap kotak dikelilingi garis putih tebal dan peta terbaca
   seperti sekumpulan kotak yang saling berteriak. Permintaan pemilik
   (9 Sep 2026): pakai garis selembut kartu KPI.

   Sekarang 1px var(--border) -- token yang sama dengan tombol outline dan
   pemisah sel KPI. Yang membuat kotak tetap terbaca sebagai kotak bukan
   tebal garisnya, melainkan permukaannya sendiri (--card-solid) yang beda
   dari latar kartu di belakangnya, plus bayangan tipis. */
.pf-boks        { position: relative; border: 1px solid var(--border); border-radius: 14px;
                  padding: 16px 8px 8px; background: var(--card-solid);
                  box-shadow: 0 1px 2px rgba(0,0,0,.04);
                  display: flex; flex-direction: column; }
/* Rak & palet dibedakan lewat POLA garisnya (putus-putus), bukan lewat warna
   yang lebih gelap. Dulu --faint pekat, dan itu justru lebih kontras daripada
   kotak freezer di sebelahnya -- benda yang lebih jarang disentuh malah
   paling menonjol.
   Tapi --border saja TIDAK cukup di sini: garis putus-putus hanya menutup
   sebagian tepinya, jadi pada ketebalan yang sama ia terbaca kira-kira
   setengah lebih pucat daripada garis utuh. Di mode terang itu membuatnya
   hilang sama sekali (diperiksa di layar, bukan dikira-kira). --faint pada
   55% mengembalikan pola dashnya tanpa kembali jadi garis spidol. */
.pf-boks-rak    { border-style: dashed;
                  border-color: color-mix(in srgb, var(--faint) 55%, transparent); }
/* Pil huruf dulu membalik warna (latar var(--text), teks var(--card-solid)):
   petak putih pekat menempel di tepi tiap kotak. Sekarang ia keping tenang
   dengan permukaan sama seperti kotaknya; hurufnya tetap var(--text) karena
   huruf itulah yang dicari orang saat menengok peta. */
.pf-huruf       { position: absolute; top: -11px; left: 12px; padding: 2px 9px; border-radius: 20px;
                  background: var(--card-solid); color: var(--text);
                  box-shadow: inset 0 0 0 1px var(--border);
                  font-size: 11px; font-weight: 700; letter-spacing: .04em; }
.pf-boks-rak .pf-huruf { color: var(--muted); }

/* Bentuk LUAR kotak (lebar/tinggi) dan susunan unit DI DALAMNYA sengaja
   dipisah. Freezer tegak (K/L/M) unitnya bertumpuk; rak kering juga
   digambar tegak di papan tulis TAPI tiga tingkatnya bersebelahan. Kalau
   susunan dalam diturunkan dari bentuk luar, rak akan tergambar salah. */
.pf-unit-wrap   { display: flex; gap: 6px; flex: 1; }
.pf-susun-kolom .pf-unit-wrap { flex-direction: column; }
/* DENAH BEBAS (15 Sep 2026): kotak freezer menempati beberapa sel grid dengan
   tinggi baris tetap (peta_helper.php), jadi isinya bisa lebih panjang dari
   kotaknya. Yang dipotong isi di DALAM kotak -- bukan kotaknya: pil huruf duduk
   di top:-11px, dan overflow:hidden pada .pf-boks akan memangkasnya separuh. */
.pf-denah-boks  { min-height: 0; padding: 6px; }
/* Freezer sekumpulan dirapatkan (migrate_046): jarak antarkotak cuma 4px, jadi
   pil huruf yang duduk di garis atas (top:-11px) akan menimpa kotak di atasnya.
   Di denah pil masuk ke pojok kiri atas petak, sebaris dengan kepala. */
.pf-denah-boks .pf-huruf { top: 12px; left: 13px; padding: 1px 7px; z-index: 1; }
.pf-denah-boks .pf-unit-kepala { padding-left: 28px; min-height: 17px; }
.pf-denah-boks .pf-unit-wrap { min-height: 0; overflow: hidden; }
.pf-denah-boks .pf-unit { min-height: 0; overflow: hidden; padding: 6px 8px; }
.pf-denah-boks .pf-isi  { margin-top: 3px; }
/* Freezer tegak lebarnya cuma ~106px: nama dibungkus dua baris daripada
   terpotong jadi "Dims...". Tingginya cukup untuk itu. */
.pf-denah-boks.pf-tinggi .pf-baris { white-space: normal; display: -webkit-box;
                  -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
/* TITIK UMUR di kotak peta (pemilik 15 Sep 2026): pengganti lencana
   "masuk hari ini" / "12 hari", yang terlalu makan tempat -- di freezer tegak
   dan palet ia sampai harus turun ke baris kedua. Warna sama dengan lencana
   (.umur-hijau/-kuning/-merah); tanggalnya di title titiknya (titik_umur()). */
.umur-titik { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
              margin-right: 5px; vertical-align: 1px; }
.umur-titik.umur-hijau  { background: var(--green); }
.umur-titik.umur-kuning { background: var(--orange); }
.umur-titik.umur-merah  { background: var(--red); }
.pf-unit-total { white-space: nowrap; }
/* PEMILIH TANGGAL di pojok kiri atas denah freezer (15 Sep 2026) -- tempat
   orang gudang menulis tanggal di papan tulisnya. Bukan kotak: ia duduk di
   sel kosong denah seperti tulisan di papan. */
.pf-tanggal       { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; min-width: 0; }
.pf-tanggal-label { font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
.pf-tanggal .d-kal-tombol { font-size: 13px; }
.pf-tanggal .pf-tanggal-balik { margin-top: 2px; padding: 6px 12px; font-size: 12px; white-space: nowrap; }
/* PAPAN RAK KERING JADI DENAH (migrate_048). Aturan lama rak & alas forklift
   (lebar rak 230px, tinggi minimum palet 150px, petak palet auto-fit) dibuat
   untuk dua kotak besar; di denah ukurannya ditentukan sel grid. */
.pf-denah-boks.pf-tipe-rak,
.pf-denah-boks.pf-tipe-palet { width: auto; min-height: 0; }
.pf-denah-boks.pf-tipe-palet .pf-unit-wrap { display: flex; }
/* Pil "PAL-9" lebih lebar dari pil satu huruf freezer. */
.pf-denah-boks.pf-tipe-palet .pf-unit-kepala { padding-left: 48px; }
/* Kotak rak berisi EMPAT seksi: pil "RAK" duduk di atasnya, bukan menindih
   kepala seksi pertama. */
.pf-denah-boks.pf-tipe-rak { padding-top: 30px; }
.pf-denah-boks.pf-tipe-rak .pf-huruf { top: 7px; }
.pf-denah-boks.pf-tipe-rak .pf-unit-kepala { padding-left: 0; }

/* Tingkat rak dibaca 3-2-1 dari kiri, persis seperti tertulis di papan
   tulisnya. Datanya tetap tingkat 1,2,3 -- yang dibalik cuma arah
   bacanya, karena mengubah penomorannya akan membuat "tingkat 1" di
   layar berbeda dengan "tingkat 1" yang diucapkan orang gudang. */
/* Rak di peta lantai = badan panjang yang dibelah jadi lima seksi
   bertumpuk. Tingkat TIDAK digambar di sini: peta ini melihat dari atas,
   dan dari atas tingkat memang tidak terlihat. Tingkat muncul di tampak
   depan setelah satu seksi diketuk. */
.pf-tipe-rak    { width: 230px; }
.pf-tipe-rak .pf-unit { padding: 9px 10px; }
.pf-tipe-rak .pf-baris { font-size: 10.5px; }
.pf-unit-seksi  { position: relative; }
.pf-unit-seksi::after { content: '›'; position: absolute; right: 9px; top: 7px;
                  font-size: 16px; line-height: 1; color: var(--faint); }
/* Tanda '>' duduk di pojok kanan atas, tempat yang sama dengan angka total.
   Di R1 keduanya bertumpuk dan "280" terbaca "28C". Barisnya diberi ruang
   sebesar tanda itu, bukan tandanya yang dipindah -- posisinya di pojok
   itulah yang membuat orang tahu petak ini bisa diketuk. */
.pf-unit-seksi .pf-unit-kepala { padding-right: 14px; }
.pf-unit-seksi:hover::after { color: var(--primary); }
.pf-jejak       { font-size: 10px; color: var(--faint); margin-top: 2px; }

/* PETAK RAK: DUA BARIS, TITIK.
   Petak rak menggabungkan tiga tingkat, jadi daftar isinya tiga kali lebih
   panjang daripada petak freezer -- diukur di layar pemilik, kolom rak jadi
   641px sementara alas forklift di sebelahnya cuma 150px, R5 jatuh di bawah
   lipatan dan separuh papan menganga kosong.
   Sekarang: kode + total di baris atas, keterangan + umur di baris bawah.
   Daftar barangnya tidak dihapus, cuma pindah satu ketukan ke tampak depan
   -- tempat tingkat memang terlihat, dan tempat orang membacanya kalau ia
   sungguh mau tahu isinya. Peta lantai dibaca dari jauh; yang berguna dari
   jauh cuma angka dan warnanya. */
.pf-tipe-rak .pf-unit-ringkas { padding: 8px 10px; gap: 3px; }
.pf-unit-ringkas .pf-kaki { display: flex; align-items: center; justify-content: space-between;
                  gap: 6px; min-height: 18px; }
.pf-unit-ringkas .pf-jejak { margin-top: 0; }
.pf-unit-ringkas .umur { font-size: 10px; padding: 1px 7px; }
.pf-unit-ringkas .pf-kosong { font-size: 10px; font-style: normal; color: var(--faint); }
.pf-tipe-palet  { border-style: dashed; min-height: 150px;
                  border-color: color-mix(in srgb, var(--faint) 55%, transparent); }
/* Lima petak boleh PATAH KE BARIS BERIKUTNYA, tidak boleh dipipihkan.
   Tiap alas cuma memuat satu jenis barang, jadi memampatkan petaknya
   berarti memotong nama barangnya -- satu-satunya keterangan di situ.

   PETAK, BUKAN FLEX YANG MEMBUNGKUS. Versi flex-wrap sudah dicoba dan
   ditolak: petak di baris terakhir MEMANJANG mengisi sisa barisnya, jadi
   di 1180px PAL-5 sendirian selebar 683px sementara empat temannya 166px.
   Petak menjaga lebar kolom tetap sama di semua baris, dan yang tersisa di
   baris terakhir duduk di kolom pertama seperti seharusnya.
   auto-fit minmax(130px): 5 kolom sejak lebar dalam ~674px (papan penuh
   dan iPad Air), 3 kolom di iPad melintang 1024px. Tingginya gratis --
   kolom rak di sebelahnya 320px, dua baris alas cuma ~250px. */
.pf-tipe-palet .pf-unit { min-width: 0; }
.pf-tipe-palet .pf-unit-wrap { display: grid; align-items: stretch;
                  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }

/* Papan kering jauh lebih kecil dari papan freezer -- tidak perlu
   dipaksa selebar itu. */
.pf-papan-kering { min-width: 700px; }
.pf-papan-kering { align-items: center; }
.pf-papan-kering .pf-kiri  { flex: 0 0 auto; display: block; }
/* Alas forklift jauh lebih pendek dari raknya. Dibiarkan menempel ke atas
   ia terbaca seperti "menggantung"; ditengahkan, keduanya terbaca sebagai
   dua benda yang berdiri di lantai yang sama. */
/* min-width:0 WAJIB di sini. Tanpa itu kotak alas forklift adalah item
   flex ber-min-width:auto -- ia menolak menyusut di bawah lebar isinya,
   lalu menjulur keluar kartu. Terukur di iPad melintang (1024px): tepi
   kanannya 1074px sementara kartunya berhenti di 947px, PAL-5 terpotong
   dan batang gulir peta pun tidak menjangkaunya karena luberannya tidak
   ikut terhitung sebagai lebar gulir. Papan freezer tidak kena masalah
   yang sama -- min-width-nya (860px) memang lebih besar dari isinya,
   jadi ia menggulir dengan benar; karena itu perbaikan ini sengaja
   dipatok ke papan kering saja. */
.pf-papan-kering .pf-kanan { flex: 1; min-width: 0;
                  grid-template-columns: 1fr; align-self: center; }

.pf-unit        { flex: 1; min-width: 0; display: flex; flex-direction: column;
                  padding: 8px 9px; border-radius: 10px; background: var(--field);
                  text-decoration: none; color: var(--text);
                  transition: background .15s ease, box-shadow .15s ease; }
.pf-unit:hover  { background: color-mix(in srgb, var(--primary) 12%, var(--field));
                  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--primary) 45%, transparent); }
.pf-unit-kosong { opacity: .55; }

/* PETA TIDAK PERNAH BERANIMASI MASUK.
   Isinya diganti tiap kali stok bergerak -- di gudang yang sedang bongkar
   muat itu bisa beberapa kali semenit. Animasi masuk yang terulang tiap
   penggantian berubah dari "halus" jadi berkedip, dan yang menatapnya
   pemilik yang cuma ingin membaca angka.
   Aturan ini menjaga kalau suatu saat ada yang menambahkan d-fadeUp pada
   kelas pf-* atau pada pembungkusnya. Transisi HOVER sengaja tidak ikut
   dimatikan: ia dipicu kursor, bukan penggantian isi. */
#petaFreezer, #petaFreezer *,
#petaKering, #petaKering * { animation: none !important; }

.pf-unit-kepala { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; }
.pf-unit-kode   { font-size: 13px; font-weight: 700; letter-spacing: -.01em; }
.pf-unit-total  { font-size: 12px; font-weight: 600; color: var(--primary); }

.pf-isi         { display: flex; flex-direction: column; gap: 1px; margin-top: 5px; flex: 1; }
.pf-baris       { font-size: 11px; line-height: 1.45; color: var(--muted);
                  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pf-baris b     { color: var(--text); font-weight: 650; }
/* --muted, bukan --faint: huruf 10,5px di kotak freezer terang cuma 2,9:1. */
.pf-lain        { font-size: 10.5px; color: var(--muted); margin-top: 1px; }
.pf-kosong      { font-size: 11px; color: var(--muted); font-style: italic; }
.pf-umur        { margin-top: 6px; }
.pf-umur .umur  { font-size: 10px; padding: 1px 7px; }

/* TABLET (769-900px belum, tapi 561-768px): petanya tetap utuh, tinggal
   digulir. Petak dikecilkan sedikit supaya sekali gulir sudah kelihatan satu
   baris penuh. */
@media (max-width: 768px) {
    /* 4 x 150 + 36 = 636, + 18 jarak + 158 kolom kanan = 812. Dipatok ke
       papan freezer saja -- papan kering isinya lain dan sudah punya
       min-width sendiri yang lebih kecil; kalau aturan ini tidak dipatok,
       ia menang atas .pf-papan-kering (sama-sama satu kelas, tapi lebih
       belakang) dan papan kering jadi 980px tanpa perlu. */
    .pf          { gap: 18px; }
    .pf-papan-freezer { min-width: 820px; }
    .pf-kiri     { grid-template-columns: repeat(4, minmax(150px, 1fr)); }
    .pf-baris    { font-size: 10.5px; }
}

/* HP (<=560px): denah papan tulis DIPERTAHANKAN, yang diubah susunan di
   DALAM tiap kotak.
   Masalah yang diukur: di lebar 390px peta memberi tiap unit 63px, dan di
   63px tiap baris isi terpotong jadi "50 Lum..." -- daftar isi yang tidak
   terbaca membuat seluruh petanya tidak berguna.
   Percobaan pertama menumpuk kotak jadi satu kolom. Itu keliru: posisi kotak
   dipasang lewat grid-column/grid-row inline (peta_helper.php) karena
   susunannya menyalin foto papan tulis gudang -- menumpuknya berarti
   membuang justru hal yang membuat peta ini peta.
   Yang dilakukan sekarang: kolom dipatok 190px dan DUA UNIT DI DALAM kotak
   ditumpuk ke bawah, bukan berdampingan. Tiap unit dapat ~174px (di desktop
   168px), jadi nama barang utuh; petanya tetap digulir mendatar seperti
   memang dirancang, dan letak A/B/C/D tetap sama dengan di dinding. */
@media (max-width: 560px) {
    /* 4 x 190 + 36 = 796, + 18 jarak + 158 kolom kanan = 972. */
    .pf-papan-freezer { min-width: 980px; }
    .pf-kiri      { grid-template-columns: repeat(4, 190px); }
    .pf-unit-wrap { flex-direction: column; }
    .pf-baris     { font-size: 11.5px; }
    .pf-unit      { padding: 9px 10px; }
}


/* -------------------------------------------------------------------------
   26. TAMPAK DEPAN SATU SEKSI RAK (rak_depan_html)
   Sudut pandang berganti dari atas ke depan, jadi bentuknya pun berganti:
   tiga papan mendatar bertumpuk, seperti rak yang dilihat sambil berdiri
   di depannya.
   ------------------------------------------------------------------------- */
.rd             { padding-top: 2px; }
.rd-nav         { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
.rd-balik       { font-size: 12.5px; color: var(--muted); text-decoration: none;
                  padding: 5px 10px; border-radius: 20px; background: var(--field); margin-right: 4px; }
.rd-balik:hover { color: var(--primary); }
.rd-pil         { font-size: 12.5px; font-weight: 600; text-decoration: none; color: var(--muted);
                  padding: 5px 13px; border-radius: 20px; border: 1px solid var(--border); }
.rd-pil:hover   { border-color: var(--primary); color: var(--primary); }
.rd-pil.aktif   { background: var(--primary); border-color: transparent; color: #fff; }

/* Sama seperti .pf-boks: garis kartu, bukan garis spidol. Pemisah antar
   tingkat memakai --hair -- persis token yang dipakai pemisah sel KPI, dan
   memang tugasnya sama: membagi satu permukaan, bukan memagari tiga benda. */
.rd-rak         { border: 1px solid var(--border); border-radius: 14px; overflow: hidden;
                  background: var(--card-solid); box-shadow: 0 1px 2px rgba(0,0,0,.04); }
.rd-tingkat     { display: flex; align-items: stretch; gap: 12px; padding: 12px 14px;
                  text-decoration: none; color: var(--text);
                  border-bottom: 1px solid var(--hair); transition: background .15s ease; }
.rd-tingkat:last-child { border-bottom: 0; }
.rd-tingkat:hover { background: color-mix(in srgb, var(--primary) 7%, transparent); }
.rd-tingkat-kosong { opacity: .55; }

.rd-sisi        { flex: 0 0 58px; display: flex; flex-direction: column; align-items: center;
                  justify-content: center; border-right: 1px solid var(--hair); padding-right: 10px; }
.rd-huruf       { font-size: 26px; font-weight: 700; letter-spacing: -.03em; line-height: 1; }
.rd-kode        { font-size: 10.5px; color: var(--faint); margin-top: 3px; }

.rd-isi         { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; gap: 4px 18px;
                  align-content: center; }
.rd-item        { font-size: 12.5px; color: var(--muted); line-height: 1.5; }
.rd-item b      { color: var(--text); font-weight: 650; }
.rd-item i      { font-style: normal; font-size: 11px; color: var(--faint); }
.rd-kosong      { font-size: 12.5px; color: var(--faint); font-style: italic; }

.rd-kanan       { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end;
                  justify-content: center; gap: 6px; }
.rd-total       { font-size: 17px; font-weight: 650; color: var(--primary); letter-spacing: -.02em; }

.rd-nota        { margin: 14px 0 0; font-size: 12px; color: var(--muted); line-height: 1.65; }
.rd-nota b      { color: var(--text); }

@media (max-width: 768px) {
    .rd-tingkat { flex-wrap: wrap; }
    .rd-sisi    { flex-basis: 46px; }
    .rd-isi     { flex: 1 1 100%; order: 3; margin-top: 6px; }
    .rd-kanan   { flex-direction: row; align-items: center; gap: 10px; }
}

/* ============================================================
   TAB HALAMAN (.d-tabhal)
   ------------------------------------------------------------
   Deret tab yang menggantikan butir rail sendiri-sendiri: satu
   alur = satu pintu di rail, tahapnya jadi tab di sini.

   Isinya .d-seg biasa, jadi aturan segmented pill tidak
   diduplikasi. Yang ditambahkan cuma jaraknya terhadap d_head
   dan perilakunya di layar sempit: di HP deretnya boleh
   digulir MENDATAR di dalam dirinya sendiri, bukan melebarkan
   badan halaman -- badan halaman tidak boleh pernah bergeser
   ke samping.
   ============================================================ */
.d-tabhal { margin: -4px 0 18px; }
.d-tabhal .d-seg { max-width: 560px; }

@media (max-width: 700px) {
    /* JANGAN menambahkan overflow-x di sini. .d-seg di bagian HP (sekitar
       baris 6102) SUDAH menggulir dirinya sendiri; membungkusnya dengan
       penggulir kedua menghasilkan dua batang gulir bersarang yang saling
       menahan, dan tab terakhir jadi tidak bisa dicapai. Yang boleh diatur
       di sini cuma jaraknya. */
    .d-tabhal { margin: -2px 0 16px; }
    .d-tabhal .d-seg { max-width: none; }
}

/* ============================================================
   BAR FILTER PERIODE (d_periode_filter di ui.php)
   ------------------------------------------------------------
   Hari ini | Kemarin | Rentang tanggal, di Permintaan dan Surat
   Jalan. Kalender Dari/Sampai muncul di sebelahnya saat Rentang
   dipilih, dan melipat ke baris sendiri kalau tidak muat --
   bukan mendesak pill-nya jadi terlalu sempit untuk dibaca.
   ============================================================ */
.d-periode-bar     { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; margin: 2px 0 14px; }
.d-periode         { width: min(100%, 380px); }
.d-periode-rentang { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0; }
.d-periode-sd      { font-size: 12.5px; color: var(--muted); }
/* Kuning, bukan merah: rentangnya ditolak tapi halamannya tetap berguna
   (yang tampil hari ini), dan kalimatnya bilang begitu. */
.d-periode-galat   { margin: -4px 0 14px; font-size: 12.5px; line-height: 1.6; color: var(--kuning-teks); }

/* ============================================================
   FILTER KOLOM (d_saring_kolom di ui.php)
   ------------------------------------------------------------
   Dropdown kecil di judul kolom tabel (Buku Besar: "Jenis").
   Di dalam .d-table-head tombolnya mewarisi huruf judul kolom
   (kecil, kapital) supaya duduk sejajar dengan judul lain,
   bukan kotak isian besar yang mendorong tinggi kepala tabel.

   Kolom yang disembunyikan di HP ('hp' => false) ikut
   menyembunyikan filternya -- jadi halaman memasang salinan
   .d-saring-kolom-hp di bilah filter, yang hanya tampil di HP.
   ============================================================ */
.d-saring-kolom    { display: inline-flex; max-width: 100%; margin: 0; }
.d-table-head .d-saring-kolom .d-select-tombol {
    font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit;
    min-height: 0; height: auto; padding-top: 3px; padding-bottom: 3px; max-width: 100%;
}
.d-saring-kolom-hp { display: none; }
@media (max-width: 768px) {
    .d-saring-kolom-hp { display: inline-flex; }
}

/* ============================================================
   PANEL DETAIL SKU (.sd-*) di halaman Stok
   ------------------------------------------------------------
   Tiga baris label-nilai, itu saja. Versi pertama memakai petak
   angka besar dan beberapa tabel; pemilik memangkasnya, dan
   bentuk yang tersisa memang tidak menuntut apa pun selain
   sepasang kolom yang rapi.
   Kelasnya berawalan sendiri: menumpang kelas tabel biasa akan
   membuatnya ikut berubah tiap kali tabel utama disetel, dan
   bedanya baru ketahuan di panel yang jarang dibuka.
   ============================================================ */
.sd-baris   { display: flex; align-items: baseline; justify-content: space-between;
              gap: 18px; padding: 13px 0; border-bottom: 1px solid var(--hair); }
.sd-baris:last-child { border-bottom: 0; }
.sd-label   { font-size: 13.5px; color: var(--muted); }
.sd-nilai   { font-size: 21px; font-weight: 700; letter-spacing: -.02em;
              font-variant-numeric: tabular-nums; text-align: right; }
.sd-nilai small { font-size: 11.5px; font-weight: 400; color: var(--muted);
                  margin-left: 5px; letter-spacing: 0; }
.sd-samar   { font-size: 12.5px; font-weight: 400; color: var(--faint); font-style: italic; }

/* LOKASI: daftar ke bawah, kode petak di kiri & jumlahnya di kanan (pemilik
   15 Sep 2026; dulu deretan chip huruf tanpa jumlah). Tiap baris membuka isi
   petaknya -- tujuan yang sama dengan kotak di peta dasbor.
   (Aturan chip lama ditutup satu "}" nyasar yang ikut dibuang di sini: kurung
   yang tidak berpasangan membuat peramban membuang aturan sesudahnya.) */
.sd-lokasi  { padding: 13px 0; border-bottom: 1px solid var(--hair); }
/* Formulir barang masuk di bawahnya sudah punya garis atas sendiri -- tanpa
   ini dua garis bertumpuk berjarak 20px (terlihat di layar). */
.sd-lokasi:has(+ .sd-masuk) { border-bottom: 0; padding-bottom: 0; }
.sd-lokasi > .sd-label { display: block; margin-bottom: 9px; }
.sd-lokasi-daftar { display: flex; flex-direction: column; gap: 6px; }
.sd-lokasi-baris {
    display: flex; align-items: center; gap: 12px; padding: 9px 12px;
    border-radius: 10px; background: var(--field); color: var(--text); text-decoration: none;
    transition: background .15s ease, box-shadow .15s ease;
}
.sd-lokasi-baris:hover,
.sd-lokasi-baris:focus-visible {
    outline: none;
    background: color-mix(in srgb, var(--primary) 12%, var(--field));
    box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--primary) 45%, transparent);
}
.sd-lokasi-kode { font-size: 14px; font-weight: 700; letter-spacing: .02em; min-width: 34px; }
.sd-lokasi-qty  { margin-left: auto; font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }
.sd-lokasi-qty small { font-size: 11.5px; font-weight: 400; color: var(--muted); margin-left: 4px; }
.sd-lokasi-baris::after { content: '›'; color: var(--faint); font-size: 17px; line-height: 1; }

/* Formulir barang masuk di dalam panel detail SKU. Menggantikan halaman
   Barang Masuk untuk kiriman satu-dua SKU; yang datang setruk tetap lebih
   cepat lewat halamannya sendiri. */
.sd-masuk   { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--hair); }
.sd-masuk-judul { font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em;
                  color: var(--faint); font-weight: 600; margin-bottom: 10px; }
.sd-masuk-baris { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }
.sd-masuk-baris label { display: flex; flex-direction: column; gap: 5px;
                        font-size: 11.5px; color: var(--muted); }
.sd-masuk-baris label:first-child { flex: 0 0 92px; }
.sd-masuk-baris label:nth-child(2) { flex: 1 1 140px; min-width: 0; }
.sd-masuk-baris input, .sd-masuk-baris select {
    font-family: inherit; font-size: 15px; padding: 9px 10px; border-radius: 10px;
    border: 1px solid var(--border); background: var(--card-solid); color: var(--text);
    width: 100%; min-width: 0;
}
.sd-masuk-baris input { text-align: right; font-weight: 600;
                        font-variant-numeric: tabular-nums; }
.sd-masuk-baris button { flex: 0 0 auto; }
.sd-masuk-ket { margin: 10px 0 0; font-size: 11.5px; line-height: 1.6; color: var(--muted); }
/* Saran freezer saat barang datang (kapasitas_helper.php). Tampil di bawah
   baris isian, bukan toast: ia berubah tiap ketikan dan harus tetap
   terbaca sampai tombol Simpan ditekan. */
.sd-saran, .ms-saran { margin: 8px 0 0; padding: 8px 11px; border-radius: 10px; font-size: 12px;
               line-height: 1.55; background: var(--field); color: var(--muted); }
.sd-saran b, .ms-saran b { color: var(--text); font-weight: 600; }
.ms-saran { flex: 1 0 100%; margin: 0; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.ms-saran:empty { display: none; }
.ms-saran .d-btn { flex: 0 0 auto; }

/* Catatan "masih ada yang lebih lama" di Aktivitas. (Gaya .rw-bebas milik
   bilah rentang lama dibuang 13 Sep bersama rentang_helper.php -- halaman
   riwayat kini memakai .d-periode-*.) */
.akt-batas { background: color-mix(in srgb, var(--orange) 12%, transparent); }
