/* ══════════════════════════════════════════════════════════════════════════
 * Valoobuy Member — tema
 *
 * DITULIS SEMULA 1 Sep 2026. Tiga percubaan pertama saya rata: kad putih dengan
 * garis rambut, palet tepat, dan tiada kedalaman. Itu register CETAKAN — sesuai
 * untuk katalog, salah untuk app telefon. Pemilik berkata "tak modern" tiga kali
 * dan dia betul.
 *
 * Register sekarang ialah register app: hero bergradient, radius besar berlapis,
 * bayang berwarna (bukan kelabu), nombor gergasi bertabular, chip bertona lembut,
 * dan tab bar terapung dengan FAB terangkat.
 *
 * IDENTITI KEKAL DARI KATALOG 2026 — kayu #C9A87C, merah #B3241C, walnut #241A14.
 * Warna itu diambil dari fotografi produk katalog (mihrab, PU stone, panel
 * berjalur), bukan dari operator vektor yang hanya melukis bar header.
 *
 * DAN KERANA BRAND ITU MERAH, MERAH TIDAK BOLEH BERMAKNA "RALAT".
 * Berjaya = hijau. Amaran = amber. Ralat = maroon + ikon + PERKATAAN.
 * ══════════════════════════════════════════════════════════════════════════ */

:root{
  /* Skema lalai pelayar: ini yang memberi kawalan borang asli, caret dan
   * scrollbar rupa yang betul tanpa satu baris CSS tambahan. Dua nilai khusus
   * per-keadaan ada di bawah blok pemetaan. */
  color-scheme: light dark;

  /* ── THE REAL BRAND RED, taken from Valoobuy's own logo ────────────────
   * Sampled 7 Sep 2026 from the official wordmark on valoobuy.com:
   * #FF4136. Everything in this app until now used #B3241C, derived from
   * catalogue product photography — a full step darker and duller than the
   * brand actually is.
   *
   * It is TWO tokens because one colour cannot do both jobs:
   *   --brand-pure  #FF4136  the logo, glows, decorative fills. Nothing that
   *                          carries text sits on it. Measured 3.46:1 against
   *                          white, which is large-text only.
   *   --brand-red   #D6342A  everything that is text, or is a fill with text
   *                          on it — links, the active tab, buttons, chips.
   *                          4.79:1 against white, so it clears AA for body
   *                          text in BOTH directions.
   * Using #FF4136 for the second job would have failed contrast on every link
   * and every button label in the app. */
  --brand-pure:#FF4136;
  --brand-red:#D6342A; --brand-red-dark:#A5241C; --brand-maroon:#8A2018;
  /* Gradient butang utama. Nilainya SAMA dengan --brand-red dalam light, jadi
   * tiada satu piksel pun berubah di sana. Ia token berasingan sebab dalam gelap
   * --brand-red mesti CERAH untuk teks atas kad (4.82:1) dan itu memberi teks
   * PUTIH atas butang hanya 3.19:1 — dua keperluan yang tidak boleh dipenuhi
   * satu warna. Butang kekal merah tepu dalam kedua-dua tema, sama seperti hero,
   * splash dan cakera QR yang memang sudah berkod tegar. */
  --btn-a:#D6342A;
  --wood:#C9A87C; --wood-soft:#E7D9C2; --wood-deep:#8A7355;
  --ink:#221812; --muted:#7A6A5C;
  --surface:#FFFFFF; --surface-2:#F6EFE5; --surface-3:#EFE5D7;
  /* --line = garis DALAM kad (pemisah baris, input, butang kecil).
   * --line-strong = SEMPADAN kad dan tepi tab bar. Nilai kedua-duanya sama
   * dalam light — light tidak berubah — dan bercabang dalam gelap, di mana
   * sempadan kad mesti melepasi WCAG 1.4.11 (diukur 3.09:1) sementara pemisah
   * baris kekal senyap (1.62:1). */
  --line:#E6DACA;
  --line-strong:#E6DACA;
  --ok:#1F7A4C; --ok-soft:#E4F2EA;
  --warn:#B45309;
  --r-lg:18px; --r-md:13px; --r-sm:9px;
  /* Bayang bertona hangat. Bayang kelabu atas permukaan hangat kelihatan kotor. */
  --sh-1:0 1px 2px rgba(74,48,26,.08), 0 2px 8px rgba(74,48,26,.07);
  --sh-2:0 1px 2px rgba(74,48,26,.10), 0 4px 10px rgba(74,48,26,.10), 0 14px 30px rgba(74,48,26,.10);
  --sh-hero:0 10px 30px rgba(138,26,20,.30), 0 2px 8px rgba(138,26,20,.20);

  /* ── KEDALAMAN: RIM, BUKAN BAYANG YANG LEBIH GELAP ────────────────────────
   * Yang membuat permukaan kelihatan FIZIKAL bukan bayang yang lebih besar —
   * itu cuma membuatnya melayang. Ia ialah tepi ATAS yang menangkap cahaya dan
   * tepi bawah yang tidak. Satu garis inset putih di atas, satu garis gelap
   * halus di bawah, dan mata membaca objek dengan ketebalan.
   *
   * Inilah teknik yang sama yang e-Kupon UC Tati guna untuk kad "glass" gelapnya
   * (bingkai 1px bercahaya + bayang ambien + sorotan atas) — cuma di sini ia
   * dipakai pada kedua-dua tema, bukan gelap sahaja. */
  --rim:inset 0 1px 0 rgba(255,255,255,.85), inset 0 -1px 0 rgba(74,48,26,.06);
  --rim-chip:inset 0 1px 0 rgba(255,255,255,.30), inset 0 -2px 4px rgba(0,0,0,.14);
  --rim-hero:inset 0 1px 0 rgba(255,255,255,.24), inset 0 -1px 0 rgba(0,0,0,.16);

  /* ── GLASS: DUA PERMUKAAN SAHAJA ──────────────────────────────────────────
   * backdrop-filter mengasingkan lapisan dan memaksa cat semula setiap bingkai
   * pada kawasan di belakangnya. Pada Android pertengahan — telefon yang member
   * ini sebenarnya pakai — meletakkannya pada setiap kad menukar tatal 60fps
   * menjadi tersekat-sekat. Ia dipakai HANYA pada dua kromium tetap yang
   * kandungan benar-benar bergerak di belakangnya: header melekat dan tab bar.
   * Nilai saturate() itu penting: blur sahaja memberi kelabu berkabus, dan
   * menaikkan kepekatan semula itulah yang membuat ia kelihatan seperti kaca. */
  --glass-blur:saturate(180%) blur(18px);
  /* Poppins, sama seperti e-Kupon UC Tati. Geometrik dan bulat — ia yang membaca
   * sebagai "app moden". Archivo ialah grotesque; ditetapkan dalam HURUF BESAR
   * dengan jarak huruf lebar ia membaca sebagai perindustrian, bukan mesra. */
  --fd:'Poppins',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  --fb:'Poppins',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;

  /* ── NILAI GELAP, SATU TEMPAT SAHAJA ──────────────────────────────────────
   * Dua blok pemetaan di bawah tidak memegang satu hex pun; ia cuma menunjuk ke
   * sini. Tukar palet gelap = tukar baris di blok ini, bukan dua tempat.
   *
   * Nombor diukur, bukan diagak (nisbah WCAG atas kad #2C231B):
   *   ink 13.28 · muted 5.67 · brand-red 4.82 · maroon 6.54 · ok 7.64
   *   kad lawan latar 1.229 (dari 1.086) · sempadan kad 3.09 · pemisah 1.62
   * Dalam gelap PEMISAHAN TIDAK BOLEH DATANG DARI BAYANG — bayang hitam atas
   * hampir-hitam ialah tiada bayang. Ia datang dari kad yang LEBIH CERAH
   * daripada latar, ditambah rim putih-hangat inset dan cincin 1px dalam --sh-*
   * (Material 3 membuat pindah yang sama: tona permukaan, bukan intensiti). */
  --d-ink:#F5EDE3; --d-muted:#AC9A87;
  --d-surface:#2C231B; --d-surface-2:#14100C; --d-surface-3:#3D3128;
  --d-line:rgb(255 244 230 / .16);
  --d-line-strong:rgb(255 244 230 / .36);
  --d-brand-red:#E96760; --d-brand-maroon:#E0968E;
  --d-btn-a:#C4302A;
  --d-ok:#5CCB93; --d-ok-soft:#17301F;
  --d-sh-1:inset 0 1px 0 rgb(255 244 230 / .06), 0 0 0 1px rgb(255 244 230 / .06), 0 1px 2px rgb(0 0 0 / .45);
  --d-sh-2:inset 0 1px 0 rgb(255 244 230 / .07), 0 0 0 1px rgb(255 244 230 / .07), 0 1px 2px rgb(0 0 0 / .60), 0 6px 16px -4px rgb(0 0 0 / .55), 0 18px 36px -12px rgb(0 0 0 / .50);
  --d-sh-hero:0 1px 2px rgb(0 0 0 / .60), 0 10px 26px -8px rgb(0 0 0 / .55), 0 24px 48px -22px rgb(0 0 0 / .55);
  /* Rim gelap: sorotan atas jauh lebih lemah daripada dalam cerah. Putih .85
   * pada permukaan #2C231B membaca sebagai satu garis bercahaya, bukan tepi. */
  --d-rim:inset 0 1px 0 rgb(255 244 230 / .10), inset 0 -1px 0 rgb(0 0 0 / .30);
  --d-rim-chip:inset 0 1px 0 rgb(255 255 255 / .22), inset 0 -2px 4px rgb(0 0 0 / .30);
}

/* ══ TIGA KEADAAN: system / light / dark ════════════════════════════════════
 * Blok media LAMA berbunyi ":root", tanpa syarat. Itu bermakna member pada
 * telefon gelap yang memilih Light akan TETAP dapat gelap — blok media masih
 * padan dan --ink gelap masih menang. Anchor ":root:not([data-theme='light'])"
 * itu yang membetulkannya, dan ia satu-satunya sebab blok ini ditulis semula.
 *
 * DUA BLOK DI BAWAH MESTI KEKAL IDENTIK BAIT DEMI BAIT, termasuk inden — satu
 * untuk "OS gelap dan member tidak memaksa light", satu untuk "member memaksa
 * dark". tests/test_theme_css.php mengekstrak kedua-duanya dan gagal kalau ia
 * hanyut, jadi jangan sunting satu tanpa yang lain. Inden blok kedua sengaja
 * sama dalamnya walaupun ia tidak bersarang; itu harga identiti bait. */
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    /*<dark-map>*/
    --ink:var(--d-ink); --muted:var(--d-muted);
    --surface:var(--d-surface); --surface-2:var(--d-surface-2); --surface-3:var(--d-surface-3);
    --line:var(--d-line); --line-strong:var(--d-line-strong);
    --brand-red:var(--d-brand-red); --brand-maroon:var(--d-brand-maroon);
    --btn-a:var(--d-btn-a);
    --ok:var(--d-ok); --ok-soft:var(--d-ok-soft);
    --sh-1:var(--d-sh-1); --sh-2:var(--d-sh-2); --sh-hero:var(--d-sh-hero);
    --rim:var(--d-rim); --rim-chip:var(--d-rim-chip);
    /*</dark-map>*/
  }
}
:root[data-theme="dark"]{
    /*<dark-map>*/
    --ink:var(--d-ink); --muted:var(--d-muted);
    --surface:var(--d-surface); --surface-2:var(--d-surface-2); --surface-3:var(--d-surface-3);
    --line:var(--d-line); --line-strong:var(--d-line-strong);
    --brand-red:var(--d-brand-red); --brand-maroon:var(--d-brand-maroon);
    --btn-a:var(--d-btn-a);
    --ok:var(--d-ok); --ok-soft:var(--d-ok-soft);
    --sh-1:var(--d-sh-1); --sh-2:var(--d-sh-2); --sh-hero:var(--d-sh-hero);
    --rim:var(--d-rim); --rim-chip:var(--d-rim-chip);
    /*</dark-map>*/
}
/* color-scheme khusus per-keadaan: bila member MEMAKSA satu tema, kawalan asli
 * pelayar mesti ikut, bukan ikut OS. */
:root[data-theme="light"]{ color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark;  }

*{box-sizing:border-box}
body{margin:0;background:var(--surface-2);color:var(--ink);font-family:var(--fb);
     -webkit-text-size-adjust:100%;-webkit-font-smoothing:antialiased}

/* ── Rangka ────────────────────────────────────────────────────────────────── */
/* ── THE PAGE FIELD ────────────────────────────────────────────────────────
 * Flat cream is what made these screens read as 2010: cards floated on nothing,
 * so nothing had depth and the whole page was one value. Three very low-contrast
 * warm fields now sit under everything — a wood-tinted bloom top-left, a
 * brand-tinted one top-right, and a lift at the bottom.
 *
 * MUCH quieter than the sign-in aurora ON PURPOSE. These screens carry numbers a
 * member is trying to read; the background's whole job is to give the cards
 * something to sit ON, and the moment it competes with a points balance it has
 * failed. It is fixed, so it does not slide around under a scrolling list. */
.m-shell{max-width:520px;margin:0 auto;min-height:100vh;
         background:
           radial-gradient(70% 42% at 8% 0%,   color-mix(in srgb, var(--wood) 26%, transparent) 0%, transparent 64%),
           radial-gradient(58% 34% at 100% 2%, color-mix(in srgb, var(--brand-pure) 12%, transparent) 0%, transparent 58%),
           radial-gradient(90% 30% at 50% 100%, color-mix(in srgb, var(--wood) 16%, transparent) 0%, transparent 70%),
           var(--surface-2);
         background-attachment:fixed}
.m-shell.has-tabs{padding-bottom:80px}

/* Header bergradient dengan jaluran fluted panel — produk yang Valoobuy jual
 * sendiri, dalam CSS tulen. Satu tempat sahaja: aksesori tunggal reka bentuk ini. */
/* Tajuk halaman, huruf biasa. Versi lama 11.5px HURUF BESAR jarak .16em membaca
 * sebagai label peralatan, bukan tajuk skrin app. */
.m-bar{position:relative;padding:11px 14px 10px;color:#fff;
       font-family:var(--fd);font-weight:600;font-size:15px;letter-spacing:-.01em;
       background:linear-gradient(135deg,#8A1A14 0%,#C42A21 46%,var(--brand-pure) 100%)}
.m-bar::after{content:"";position:absolute;inset:0;pointer-events:none;
              background:repeating-linear-gradient(90deg,
                 rgba(255,255,255,.055) 0 1px, transparent 1px 7px)}
.m-strip{height:2px;background:linear-gradient(90deg,var(--wood),var(--wood-soft))}

/* ── Kad ───────────────────────────────────────────────────────────────────── */
/* Bingkai 1.5px, bukan 1px. Pada skrin ini 1px terkira menjadi 0.67px sebenar dan
 * hilang sepenuhnya — dan "flat" ialah aduan yang sudah tiga kali dibuat. Kad
 * mendapat bingkai DAN bayang berlapis: satu memberi tepi, satu lagi memberi
 * ketinggian, dan tanpa kedua-duanya kad hanyut pada latar krim yang hangat. */
.m-card{background:var(--surface);border:1.5px solid var(--line-strong);
        border-radius:var(--r-lg);box-shadow:var(--rim),var(--sh-2);
        padding:12px;margin:10px}
.m-card h1,.m-card h2{margin:0 0 5px;font-size:16px;font-weight:800;
                      letter-spacing:-.01em;color:var(--ink)}
.m-card p{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}

/* Jarak huruf dikurangkan dari .22em ke .1em. Pada Poppins yang bulat, jarak
 * lebar merenggangkan perkataan sehingga ia membaca sebagai cetakan teknikal. */
.m-eyebrow{display:block;font-family:var(--fd);font-size:11px;font-weight:600;
           letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}

/* ══ HERO POINTS ═══════════════════════════════════════════════════════════
 * Kad bergradient dengan nombor gergasi. Inilah benda pertama yang member cari
 * bila dia buka app, jadi ia dapat berat visual yang paling besar.
 *
 * Garis dimensi bercap tik dikekalkan di bawah nombor: katalog menanda setiap
 * produk begitu (60cm, 120cm), dan points ialah kuantiti juga. Syarikat yang
 * mengukur bahan untuk hidup, mengukur points dengan cara yang sama. */
.m-hero{position:relative;overflow:hidden;margin:10px;padding:15px 16px 14px;
        border-radius:20px;color:#fff;
        box-shadow:var(--rim-hero),var(--sh-hero);
        background:linear-gradient(145deg,#8A1A14 0%,#C42A21 42%,var(--brand-pure) 100%)}
/* Kilauan pepenjuru. Satu jaluran cahaya sempit melintas kad pada sudut, yang
 * memberi permukaan itu arah — mata membaca cahaya datang dari satu tempat, dan
 * itulah yang membezakan kad bergradient daripada objek. Corak yang sama ada
 * dalam wallet_card.dart e-Kupon (garis kilau diputar -30 darjah). */
.m-hero::before{background:
   radial-gradient(circle,rgba(255,255,255,.16),transparent 68%)}
.m-hero .m-sheen{position:absolute;top:-30%;left:-20%;width:55%;height:180%;
                 pointer-events:none;transform:rotate(18deg);
                 background:linear-gradient(90deg,
                    transparent, rgba(255,255,255,.10) 45%, transparent)}
.m-hero::before{content:"";position:absolute;top:-58px;right:-46px;
                width:190px;height:190px;border-radius:50%;
                background:radial-gradient(circle,rgba(255,255,255,.16),transparent 68%)}
.m-hero::after{content:"";position:absolute;inset:0;pointer-events:none;
               background:repeating-linear-gradient(90deg,
                  rgba(255,255,255,.045) 0 1px, transparent 1px 8px)}
.m-hero > *{position:relative}
.m-hero .m-eyebrow{color:rgba(255,255,255,.78)}
.m-figure{font-family:var(--fd);font-weight:700;font-size:38px;line-height:1;
          letter-spacing:-.035em;font-variant-numeric:tabular-nums;margin:5px 0 0}
/* Unit duduk pada garis dasar yang sama dengan nombor. Versi lama meletakkannya
 * di hujung satu garis bertanda tik yang membaca sebagai artifak tersasar. */
.m-figure-unit{margin-left:7px;font-size:13px;font-weight:600;letter-spacing:.02em;
               color:rgba(255,255,255,.72)}
.m-hero .m-figure{color:#fff;text-shadow:0 2px 10px rgba(0,0,0,.18)}
/* Garis berdimensi bercap tik dibuang 3 Sep 2026. Niatnya ialah rujukan katalog —
 * setiap produk ditanda 60cm, 120cm, jadi points ditanda begitu juga. Pada skrin
 * ia tidak membaca sebagai rujukan; ia membaca sebagai garis tersasar di bawah
 * nombor. Unit kini duduk pada garis dasar yang sama (.m-figure-unit). */
/* ── Baris atas kad: label + cip tier ─────────────────────────────────────────
 * Cip duduk pada baris label, bukan melayang mutlak di sudut. Kedudukan mutlak
 * ialah apa yang mockup nampak seperti, dan ia pecah pada nama tier yang
 * panjang ("Platinum" dalam BM) — ia bertindan dengan nombor besar di bawahnya.
 * Baris flex membuat kedua-duanya tolak satu sama lain. */
.m-hero-top{display:flex;align-items:center;justify-content:space-between;gap:10px}
.m-tier{flex:0 0 auto;font-family:var(--fd);font-size:10px;font-weight:800;
        letter-spacing:.1em;text-transform:uppercase;padding:4px 11px;
        border-radius:999px;white-space:nowrap;
        /* Teks GELAP, dan itu bukan pilihan gaya. Warna tier datang dari admin
         * dan lazimnya logam cerah (#D4AF37 emas, #9CA3AF perak) — teks putih
         * di atasnya jatuh sekitar 1.9:1, kegagalan sama yang cip void kena
         * sebelum ni. Ink pada logam cerah lulus AA; pada tona gelap yang
         * seseorang mungkin pilih ia kekal boleh dibaca kerana cip itu kecil
         * dan bold. */
        background:var(--wood);color:#2A1F16;
        box-shadow:0 1px 3px rgba(0,0,0,.22),inset 0 1px 0 rgba(255,255,255,.35)}

/* ── Nilai RM ─────────────────────────────────────────────────────────────────
 * Terus di bawah nombor mata, kerana ia MENERANGKAN nombor itu. Lebih kecil
 * dan lebih pudar: mata ialah baki, ringgit ialah terjemahannya. */
/* PUTIH PEJAL, bukan putih .82, dan itu diukur bukan diagak.
 *
 * Draf pertama guna rgba(255,255,255,.82), yang memberi 4.26:1 atas hentian
 * tengah gradient (#C42A21) — GAGAL AA untuk teks biasa. Ini nombor yang
 * seluruh mesyuarat 10 Sep tentangnya; ia tidak boleh jadi baris paling sukar
 * dibaca pada kad.
 *
 * Putih pejal memberi 5.68:1 atas hentian tengah, sama seperti .m-hero-name
 * yang sudah ada. Perlu diakui: hujung TERANG gradient (--brand-pure #FF4136,
 * sudut bawah-kanan) hanya 3.46:1 walaupun dengan putih pejal — itu had kad
 * ini sendiri dan ia sudah ada sebelum baris ni; teks di sini duduk di kiri
 * atas, di mana latarnya #8A1A14..#C42A21 (6.7..5.7:1). Bayang teks ialah
 * sebabnya, bukan hiasan: ia menggelapkan piksel TEPAT di belakang huruf. */
.m-hero-rm{margin:4px 0 0;font-family:var(--fd);font-size:13px;font-weight:600;
           letter-spacing:.01em;color:#fff;
           text-shadow:0 1px 6px rgba(0,0,0,.28);
           font-variant-numeric:tabular-nums}

/* ── Bar kemajuan ─────────────────────────────────────────────────────────────
 * Landasan separa-lut atas kad merah, isian KAYU. Kayu dipilih dan bukan putih:
 * putih atas merah ialah pasangan yang sudah dipakai untuk teks pada kad ni,
 * jadi bar putih akan membaca sebagai satu lagi baris teks. Kayu ialah tona
 * kedua jenama dan ia satu-satunya benda pada kad yang berwarna itu.
 *
 * Lebar datang dari style= sebaris (peratusan ialah DATA), tetapi peralihan
 * ada di sini supaya ia bergerak bila nilai berubah antara muatan halaman. */
.m-hero-bar{position:relative;height:7px;margin-top:11px;border-radius:999px;
            background:rgba(255,255,255,.22);overflow:hidden;
            box-shadow:inset 0 1px 2px rgba(0,0,0,.18)}
.m-hero-bar > span{display:block;height:100%;border-radius:999px;
                   background:linear-gradient(90deg,var(--wood-soft),var(--wood));
                   box-shadow:0 0 8px rgba(201,168,124,.5);
                   transition:width .45s cubic-bezier(.2,.8,.2,1)}
/* .94 dan bukan .86: .86 diukur pada 4.54:1 atas hentian tengah, yang lulus
 * tetapi hanya dengan 0.04 — dan baris ni membawa nombor ("250 mata lagi"),
 * jadi ia perlu jarak yang lebih daripada had. .94 memberi 5.16:1. */
.m-hero-goal{margin:7px 0 0;font-size:12px;font-weight:500;line-height:1.4;
             color:rgba(255,255,255,.94);text-shadow:0 1px 5px rgba(0,0,0,.24)}
@media (prefers-reduced-motion: reduce){
  .m-hero-bar > span{transition:none}
}

.m-hero-foot{display:flex;align-items:baseline;justify-content:space-between;
             gap:9px;margin-top:10px;padding-top:9px;
             border-top:1px solid rgba(255,255,255,.18)}
.m-hero-name{font-size:13px;font-weight:600;color:#fff;overflow:hidden;
             text-overflow:ellipsis;white-space:nowrap}
.m-hero-since{flex:0 0 auto;font-size:11px;font-weight:500;letter-spacing:.01em;
              color:rgba(255,255,255,.7)}

/* ── Chip / pil ────────────────────────────────────────────────────────────── */
/* THE UNREAD DOT. One use today: the Account row for a birthday nobody has
 * filled in. It is an ELEMENT and not a ::after, so it can carry an aria-label
 * - a red circle on its own tells a screen reader nothing at all, and "not
 * set" is the entire message.
 *
 * --brand-pure and not --brand-red: this dot sits on cream beside dark text,
 * where the decorative brand tone is the one that reads as "look here", and
 * --brand-red is reserved for things that are text or carry text. */
.m-dot{display:inline-block;width:7px;height:7px;margin-left:6px;
       vertical-align:middle;border-radius:50%;background:var(--brand-pure);
       box-shadow:0 0 0 2.5px color-mix(in srgb,var(--brand-pure) 22%,transparent)}

.m-pill{display:inline-block;font-family:var(--fd);font-size:9px;font-weight:800;
        letter-spacing:.15em;text-transform:uppercase;padding:4px 9px;
        border-radius:999px;background:var(--wood);color:#2A1F16;white-space:nowrap}

/* ── Borang ───────────────────────────────────────────────────────────────── */
/* `p.m-lead`, not `.m-lead` — a bare class ties on specificity with `.m-card
 * p` above (0,1,0 vs 0,1,1) and loses, since specificity beats source order.
 * Found live on notifications.php + scan.php: the 9px bottom margin was
 * rendering as 0. Adding the tag makes this the one that wins (same file,
 * declared after `.m-card p`, so it wins the tie too). Same fix already
 * applied to .m-pur-note (purchases.css) for the identical reason. */
p.m-lead{margin:0 0 9px;color:var(--muted);font-size:12.5px;line-height:1.5}
/* Untuk lead yang berdiri di luar kad — ia perlu inden mendatar kad itu sendiri,
 * kalau tidak ia melekat pada tepi skrin. */
.m-lead-loose{margin:12px 14px 2px}
.m-label{display:block;margin:11px 0 5px;font-family:var(--fd);font-size:12px;
         font-weight:600;letter-spacing:0;color:var(--muted)}
/* min-width:0 + max-width:100% adalah pertahanan: <select> asli mempunyai lebar
 * minimum intrinsik daripada teks pilihan terpanjang dan boleh menolak mengecil
 * pada skrin sempit. Menambah negara bernama lebih panjang tidak boleh memecahkan
 * layout. */
.m-input{width:100%;max-width:100%;min-width:0;padding:11px 12px;font-size:16px;
         font-family:var(--fb);color:var(--ink);background:var(--surface-2);
         border:1.5px solid var(--line);border-radius:var(--r-md);appearance:none;
         transition:border-color .18s ease, box-shadow .18s ease}
.m-input:focus{outline:none;border-color:var(--brand-red);
               box-shadow:0 0 0 4px rgba(179,36,28,.13)}
.m-input:disabled{opacity:.6}
.m-btn{display:block;width:100%;margin-top:13px;padding:12px;border:0;
       border-radius:var(--r-md);color:#fff;cursor:pointer;
       font-family:var(--fd);font-size:14.5px;font-weight:600;letter-spacing:0;
       background:linear-gradient(135deg,var(--btn-a),var(--brand-red-dark));
       box-shadow:0 6px 18px rgba(179,36,28,.32);
       transition:transform .12s ease, box-shadow .2s ease, opacity .2s ease}
.m-btn:not(:disabled):active{transform:translateY(1px) scale(.985);
                             box-shadow:0 3px 10px rgba(179,36,28,.28)}
.m-btn:disabled{opacity:.42;cursor:not-allowed;box-shadow:none}

/* Ralat: maroon + ikon + perkataan. Bukan merah, sebab merah ialah brand. */
.m-err{display:flex;gap:9px;align-items:flex-start;margin:13px 0 0;padding:12px 13px;
       font-size:13px;line-height:1.5;border-radius:var(--r-md);
       border:1px solid var(--brand-maroon);
       background:var(--surface-2);
       background:color-mix(in srgb, var(--brand-maroon) 9%, var(--surface));
       color:var(--brand-maroon)}
.m-err svg{flex:0 0 auto;margin-top:1px}
.m-link{display:inline-block;margin-top:7px;color:var(--brand-maroon);
        font-weight:800;text-decoration:underline;transition:opacity .18s ease}
.m-link:active{opacity:.6}

/* ── Baris senarai am ─────────────────────────────────────────────────────── */
.m-row{display:flex;align-items:flex-start;justify-content:space-between;gap:11px;
       padding:12px 0;border-top:1px solid var(--line)}
.m-row:first-of-type{border-top:0;padding-top:5px}
.m-ico{flex:0 0 auto;width:36px;height:36px;border-radius:12px;display:grid;
       place-items:center;background:var(--surface-3);color:var(--wood-deep)}
.m-ico svg{width:18px;height:18px}

/* ── Akaun ────────────────────────────────────────────────────────────────── */
.m-facts{display:grid;grid-template-columns:96px 1fr;gap:6px 9px;margin:8px 0 0}
.m-facts dt{font-family:var(--fd);font-size:9.5px;font-weight:800;color:var(--muted);
            text-transform:uppercase;letter-spacing:.14em;align-self:center;margin:0}
.m-facts dd{margin:0;font-size:13px;font-weight:600;color:var(--ink)}
.m-device-row{display:flex;align-items:center;justify-content:space-between;gap:10px;
              padding:9px 0;border-top:1px solid var(--line)}
.m-device-row:first-of-type{border-top:0;padding-top:4px}
.m-device-label{font-size:14px;font-weight:700;color:var(--ink)}
.m-device-seen{font-size:11.5px;color:var(--muted);margin-top:3px;
               font-variant-numeric:tabular-nums}
.m-device-tag{display:inline-block;margin-left:7px;padding:2px 8px;border-radius:999px;
              font-family:var(--fd);font-size:8.5px;font-weight:800;letter-spacing:.13em;
              text-transform:uppercase;background:var(--ok-soft);color:var(--ok)}
.m-btn-sm{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
          min-height:44px;padding:9px 15px;font-family:var(--fd);font-size:12.5px;
          font-weight:600;letter-spacing:0;
          border-radius:999px;border:1.5px solid var(--line);background:var(--surface-2);
          color:var(--brand-maroon);cursor:pointer;
          transition:transform .12s ease, background .18s ease, opacity .18s ease}
.m-btn-sm:active{transform:scale(.95)}
.m-btn-sm:disabled{opacity:.5;cursor:not-allowed}

/* Logout disorokkan di bawah tapi masih boleh dijumpai — bukan dalam menu tersirat. */
.m-collapse-btn{display:flex;align-items:center;justify-content:space-between;
                width:100%;min-height:44px;background:none;border:0;padding:8px 0;margin:0;
                font-family:var(--fd);font-size:12.5px;font-weight:600;
                letter-spacing:0;color:var(--muted);cursor:pointer}
.m-collapse-btn svg{transition:transform .22s cubic-bezier(.2,.8,.2,1)}
.m-collapse-btn[aria-expanded="true"] svg{transform:rotate(180deg)}
.m-collapse-body{max-height:0;opacity:0;overflow:hidden;padding-top:0;
                 transition:max-height .32s ease, opacity .24s ease, padding .32s ease}
/* max-height ialah SILING, bukan ukuran: 140px pecah senyap saat baris kedua
 * ditambah, dan itu memang berlaku. Halaman menetapkan tinggi yang DIUKUR
 * (el.scrollHeight) sebagai gaya inline supaya peralihan tepat; siling ini
 * cuma jaring bila gaya itu tiada. */
.m-collapse-body.open{max-height:1200px;opacity:1;padding-top:11px}

/* ── Senarai points ───────────────────────────────────────────────────────── */
/* `p.m-pts-empty`, not `.m-pts-empty` — same `.m-card p` specificity trap as
 * `.m-lead` above: every "nothing to show" empty-state sentence app-wide
 * (index/points/purchase.php, 6+ call sites) was silently rendering at
 * 12.5px/no-top-margin instead of the intended 13.5px/10px. */
p.m-pts-empty{margin:10px 0 0;font-size:13.5px;color:var(--muted)}
.m-pts-list,.m-buy-list{list-style:none;margin:8px 0 0;padding:0}
.m-pts-row{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;
           padding:9px 0;border-top:1px solid var(--line)}
.m-pts-row:first-child{border-top:0;padding-top:5px}
.m-pts-main{min-width:0}
.m-pts-kind{font-family:var(--fd);font-size:13.5px;font-weight:600;letter-spacing:0;
            color:var(--ink)}
.m-pts-reason{margin-top:3px;font-size:13px;line-height:1.45;color:var(--muted)}
.m-pts-when{margin-top:4px;font-size:11px;color:var(--muted);
            font-variant-numeric:tabular-nums}
.m-pts-delta{flex:0 0 auto;display:inline-flex;align-items:center;gap:4px;
             padding:4px 9px;border-radius:999px;
             font-family:var(--fd);font-size:12.5px;font-weight:800;
             font-variant-numeric:tabular-nums}
.m-pts-delta.is-up{color:var(--ok);background:var(--ok-soft)}
.m-pts-delta.is-down{color:var(--brand-maroon);
                     background:color-mix(in srgb, var(--brand-maroon) 11%, var(--surface))}

/* ── Senarai belian ───────────────────────────────────────────────────────── */
.m-buy-row{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;
           padding:9px 0;border-top:1px solid var(--line)}
.m-buy-row:first-child{border-top:0;padding-top:5px}
/* NO OPACITY ON THE TEXT AT ALL, and that took two goes to get right.
 *
 * It was .m-buy-row.is-void{opacity:.6}, which faded the row's own --muted
 * date and branch to 2.96:1 dark and 2.40:1 light against 5.67 and 5.19
 * unfaded. My first fix only softened it to .75 - and measuring that showed
 * 3.82:1 dark and 3.15:1 light, still under the 4.5:1 this size needs. Even
 * .85 fails in light mode at 3.83:1. There is no alpha that both reads as
 * "faded" and stays legible, so the text keeps its full strength.
 *
 * A cancelled row is marked by SHAPE and WORDS instead: the total is struck
 * through, and .m-buy-void spells it out beside the amount. Same rule this
 * file already applies to discounts a few hundred lines up - never colour
 * alone, because red here is brand chrome rather than meaning. */
.m-buy-row.is-void .m-buy-total{text-decoration:line-through;
                                text-decoration-thickness:1.5px;
                                text-decoration-color:var(--brand-maroon)}
.m-buy-main{min-width:0}
.m-buy-doc{font-family:var(--fd);font-size:12.5px;font-weight:800;letter-spacing:.08em;
           color:var(--ink)}
.m-buy-meta{margin-top:3px;font-size:12.5px;color:var(--muted)}
.m-buy-when{margin-top:4px;font-size:11px;color:var(--muted);
            font-variant-numeric:tabular-nums}
.m-buy-side{flex:0 0 auto;text-align:right}
.m-buy-total{font-family:var(--fd);font-size:14px;font-weight:800;color:var(--ink);
             font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.m-buy-void{display:inline-block;margin-top:4px;padding:2px 8px;border-radius:999px;
            font-family:var(--fd);font-size:8.5px;font-weight:800;letter-spacing:.13em;
            text-transform:uppercase;border:1px solid var(--brand-maroon);
            color:var(--brand-maroon)}
.m-buy-note{font-size:12.5px;line-height:1.55;color:var(--muted)}

/* ── QR ────────────────────────────────────────────────────────────────────
 * Pentas SENTIASA putih, walaupun dalam dark mode. Keperluan FUNGSI, bukan gaya:
 * member membukanya di kaunter, kadang di bawah cahaya matahari, dan QR gelap
 * atas permukaan gelap ialah satu-satunya kes di mana tema akan merosakkan kerja
 * sebenar app ini. Nilai di sini sengaja berkod tegar. */
/* ══ KAD MEMBER ═════════════════════════════════════════════════════════════
 * Kad fizikal, bukan QR terapung atas kad kosong. Jaluran brand di atas dengan
 * tanda V, garis kayu sebagai pemisah, pentas QR putih, kemudian kaki kad.
 * Member menghulurkannya kepada staf di kaunter — ia patut kelihatan seperti
 * sesuatu yang dikeluarkan kepadanya, bukan skrin utiliti. */
.m-vbcard{margin:10px;border-radius:20px;overflow:hidden;
          background:var(--surface);border:1.5px solid var(--line-strong);
          box-shadow:var(--rim),var(--sh-2)}
.m-vbcard-top{position:relative;display:flex;align-items:center;gap:9px;
              padding:11px 13px;color:#fff;overflow:hidden;
              background:linear-gradient(135deg,#8A1A14 0%,#C42A21 46%,var(--brand-pure) 100%);
              box-shadow:inset 0 1px 0 rgba(255,255,255,.16)}
.m-vbcard-top::after{content:"";position:absolute;inset:0;pointer-events:none;
              background:repeating-linear-gradient(90deg,
                 rgba(255,255,255,.055) 0 1px, transparent 1px 7px)}
.m-vbcard-top > *{position:relative}
/* Same reasoning as .m-bar-logo. This one sits on the member QR card. */
.m-vbcard-mark{flex:0 0 auto;display:grid;place-items:center;width:28px;height:28px;
               border-radius:50%;background:#fff;
               box-shadow:0 1px 2px rgba(0,0,0,.18),inset 0 -1px 0 rgba(0,0,0,.06)}
.m-vbcard-mark .m-brand-mark{width:24px;height:24px;display:block}
.m-vbcard-word{flex:1 1 auto;font-family:var(--fd);font-size:12px;font-weight:700;
               letter-spacing:.14em;text-transform:uppercase}
.m-vbcard-type{flex:0 0 auto;padding:4px 9px;border-radius:999px;
               background:rgba(255,255,255,.17);
               font-family:var(--fd);font-size:9px;font-weight:700;
               letter-spacing:.13em;text-transform:uppercase;white-space:nowrap}
.m-vbcard-rule{height:3px;background:linear-gradient(90deg,var(--wood),var(--wood-soft))}
.m-vbcard-body{padding:15px 14px 13px}
.m-vbcard-foot{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
               padding:10px 14px 12px;border-top:1px solid var(--line);
               font-size:11px;color:var(--muted)}

/* Pentas QR SENTIASA putih, walaupun dalam gelap. Keperluan FUNGSI, bukan gaya:
 * member membukanya di kaunter, kadang di bawah cahaya matahari, dan QR gelap
 * atas permukaan gelap ialah satu-satunya kes di mana tema akan merosakkan kerja
 * sebenar app ini. Nilai di sini sengaja berkod tegar. */
.m-qr-stage{background:#FFFFFF;border-radius:14px;padding:14px 14px 11px;
            text-align:center;border:1px solid #EADFCF;
            box-shadow:inset 0 1px 0 #fff, 0 4px 14px rgba(74,48,26,.10)}
.m-qr-code{width:min(196px,60vw);margin:0 auto;line-height:0}
.m-qr-code img{display:block;image-rendering:pixelated;border-radius:6px}
.m-qr-number{margin-top:11px;font-family:var(--fd);font-weight:800;font-size:18px;
             letter-spacing:.05em;font-variant-numeric:tabular-nums;color:#221812}
.m-qr-who{margin-top:4px;font-size:13px;font-weight:600;color:#7A6A5C}
.m-qr-nick{color:#A08A6A}
.m-qr-nick::before{content:" · "}

/* ── Tab bar terapung ──────────────────────────────────────────────────────
 * Kelima-lima label pada SATU garis dasar. Cakera QR terangkat dengan margin
 * negatif yang sama tingginya dengan ikon adik-beradiknya, jadi labelnya jatuh
 * di tempat yang sama dan bukan tergantung di bawah. */
.m-tabs{position:fixed;left:0;right:0;bottom:0;max-width:520px;margin:0 auto;
        display:grid;grid-template-columns:repeat(5,1fr);
        /* KACA. Salah satu daripada dua permukaan sahaja dalam app ini yang
         * mendapat backdrop-filter — kandungan benar-benar bergerak di belakang
         * tab bar semasa menatal, jadi kesannya sebenar dan bukan hiasan.
         * Latar separa-lut ialah SANDARAN dan mesti kekal: kalau
         * backdrop-filter tidak disokong, tab bar mesti tetap boleh dibaca. */
        background:color-mix(in srgb, var(--surface) 78%, transparent);
        border-top:1px solid var(--line-strong);
        box-shadow:inset 0 1px 0 rgba(255,255,255,.5),0 -4px 22px rgba(74,48,26,.13);
        padding:6px 4px calc(6px + env(safe-area-inset-bottom,0px));
        border-radius:22px 22px 0 0}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)){
  .m-tabs{-webkit-backdrop-filter:var(--glass-blur);backdrop-filter:var(--glass-blur);
          background:color-mix(in srgb, var(--surface) 62%, transparent)}
}
.m-tab{display:flex;flex-direction:column;align-items:center;gap:5px;position:relative;
       min-width:0;padding:4px 2px;font-family:var(--fd);font-size:11px;line-height:1;
       font-weight:600;letter-spacing:0;
       color:var(--muted);text-decoration:none;text-align:center;
       transition:color .18s ease}
.m-tab svg{width:21px;height:21px;flex:0 0 auto;
           transition:transform .24s cubic-bezier(.2,.8,.2,1)}
.m-tab[aria-current="page"]{color:var(--brand-red)}
.m-tab[aria-current="page"] svg{transform:translateY(-1px) scale(1.06)}
/* Pil bertona di belakang ikon aktif — bukan titik generik. */
.m-tab[aria-current="page"]::before{content:"";position:absolute;top:-3px;
       width:40px;height:28px;border-radius:999px;
       background:color-mix(in srgb, var(--brand-red) 13%, transparent)}
.m-tab:active{transform:translateY(1px)}
.m-tab-qr{position:relative}
.m-tab-qr::before{display:none}
.m-tab-qr-disc{width:42px;height:42px;margin-top:-21px;border-radius:50%;
               display:grid;place-items:center;color:#fff;
               background:linear-gradient(140deg,#C4302A,var(--brand-red-dark));
               box-shadow:0 8px 20px rgba(138,26,20,.42);
               border:3.5px solid var(--surface);
               transition:transform .22s cubic-bezier(.2,.8,.2,1)}
.m-tab-qr-disc svg{width:19px;height:19px}
.m-tab-qr:active .m-tab-qr-disc{transform:scale(.92)}
.m-tab-label{display:block}

/* ── Splash ────────────────────────────────────────────────────────────────
 * Dibuang oleh CSS sahaja. Kalau ia bergantung pada JS dan JS gagal, member
 * terperangkap memandang skrin penuh tanpa jalan keluar. */
.m-splash{position:fixed;inset:0;z-index:99;display:grid;place-items:center;color:#fff;
          background:linear-gradient(155deg,#6E120E 0%,#A8201A 55%,#C4302A 100%);
          animation:mSplashOut .55s cubic-bezier(.4,0,.2,1) .85s forwards}
@keyframes mSplashOut{to{opacity:0;visibility:hidden;pointer-events:none}}
.m-splash-mark{display:flex;flex-direction:column;align-items:center;gap:15px;
               animation:mSplashIn .5s cubic-bezier(.2,.8,.2,1) both}
/* Transform SAHAJA. Kebolehlihatan tidak boleh bergantung pada animasi selesai —
 * pada WebView lama itu bermakna app KOSONG, kegagalan senyap yang paling sukar
 * didiagnos. */
@keyframes mSplashIn{from{transform:scale(.9) translateY(8px)}to{transform:none}}
/* THE MARK ON THE SPLASH. A white plate behind the artwork, and it is not
 * decoration: the splash background is a red gradient and the logo IS a red
 * disc, so without separation the mark sinks into it. The plate is 8px wider
 * than the image on each side, which reads as the ring the brand already
 * carries on the sign-in screen rather than as a box. */
.m-splash-mark .m-brand-mark{width:68px;height:68px;display:block;
                             background:#fff;border-radius:50%;
                             box-shadow:0 0 0 8px #fff,0 6px 18px rgba(0,0,0,.22)}
.m-splash-name{font-family:var(--fd);font-size:15px;font-weight:800;
               letter-spacing:.26em;text-transform:uppercase}
.m-splash-rule{width:48px;height:3px;border-radius:2px;background:var(--wood);
               transform-origin:left;animation:mRule .7s cubic-bezier(.4,0,.2,1) .25s both}
@keyframes mRule{from{transform:scaleX(0)}to{transform:scaleX(1)}}
html.m-splash-seen .m-splash{display:none}

/* ── Animasi masuk — OPT-IN, transform sahaja ─────────────────────────────── */
@keyframes mBarIn{from{transform:translateY(-100%)}to{transform:none}}
@keyframes mRise{from{transform:translateY(16px)}to{transform:none}}
@keyframes mTabsIn{from{transform:translateY(100%)}to{transform:none}}
@keyframes mErrIn{from{transform:translateY(-6px)}to{transform:none}}
@media (prefers-reduced-motion: no-preference){
  .m-bar{animation:mBarIn .42s cubic-bezier(.2,.8,.2,1) both}
  .m-strip{animation:mRule .55s cubic-bezier(.4,0,.2,1) .18s both}
  .m-hero{animation:mRise .46s cubic-bezier(.2,.8,.2,1) both}
  .m-card{animation:mRise .44s cubic-bezier(.2,.8,.2,1) both}
  .m-card:nth-of-type(2){animation-delay:.07s}
  .m-card:nth-of-type(3){animation-delay:.14s}
  .m-tabs{animation:mTabsIn .46s cubic-bezier(.2,.8,.2,1) .1s both}
  .m-err{animation:mErrIn .28s cubic-bezier(.2,.8,.2,1) both}
}

/* ── Skrin lebar ───────────────────────────────────────────────────────────
 * App ini mudah alih dahulu, tetapi ia dibuka pada desktop semasa pembangunan
 * dan oleh staf. Tanpa ini, shell 520px terapung dalam ruang kosong dan nampak
 * seperti halaman pecah, bukan pilihan reka bentuk. */
@media (min-width:640px){
  body{background:
       radial-gradient(1200px 560px at 50% -10%, rgba(201,168,124,.32), transparent 62%),
       var(--surface-2)}
  .m-shell{min-height:0;margin:22px auto 30px;border:1px solid var(--line);
           border-radius:28px;overflow:hidden;box-shadow:0 18px 50px rgba(74,48,26,.22)}
  .m-tabs{position:sticky;bottom:0;border-radius:0}
  .m-shell.has-tabs{padding-bottom:0}
}

@media (prefers-reduced-motion: reduce){
  .m-splash{animation:mSplashOut .01s linear .3s forwards}
  .m-splash-mark,.m-splash-rule{animation:none}
  .m-btn,.m-tab,.m-tab svg,.m-tab-qr-disc,.m-input,.m-link,.m-btn-sm,
  .m-collapse-btn svg,.m-collapse-body,.m-seg a{transition:none}
}

/* ══ PERALIHAN HALAMAN ══════════════════════════════════════════════════════
 * View Transitions API merentas dokumen. Tiada JS, tiada SPA, tiada router —
 * pelayar meleraikan peralihan antara dua muat halaman penuh sendiri. Ini yang
 * memberi app berbilang-halaman rasa lancar seperti app native.
 *
 * Pelayar yang tidak menyokongnya mengabaikan blok ini sepenuhnya dan halaman
 * bertukar serta-merta seperti biasa. Tiada apa yang rosak. */
@view-transition { navigation: auto; }
::view-transition-old(root){animation:vtOut .2s cubic-bezier(.4,0,1,1) both}
::view-transition-new(root){animation:vtIn .28s cubic-bezier(0,0,.2,1) both}
@keyframes vtOut{to{opacity:0;transform:translateY(-6px) scale(.995)}}
@keyframes vtIn{from{opacity:0;transform:translateY(10px) scale(.995)}}
/* Header dan tab bar kekal di tempatnya semasa peralihan — hanya kandungan
 * yang bergerak. Itu yang membezakan "app" daripada "laman web". */
.m-bar{view-transition-name:m-bar}
.m-tabs{view-transition-name:m-tabs}
@media (prefers-reduced-motion: reduce){
  ::view-transition-old(root),::view-transition-new(root){animation:none}
}

/* ══ HEADER MELEKAT + SALAM ═════════════════════════════════════════════════ */
.m-top{position:sticky;top:0;z-index:20}
/* Header kekal PEJAL, bukan kaca. Ia kromium brand, dan merah separa-lut atas
 * kandungan krim membaca sebagai berlumpur, bukan premium. Kedalamannya datang
 * dari sorotan inset di tepi atas dan bayang jatuh yang meletakkannya DI ATAS
 * kandungan yang menatal di bawahnya. */
.m-hi{display:flex;align-items:center;gap:11px;
      padding:10px 14px 9px;color:#fff;
      background:linear-gradient(135deg,#8A1A14 0%,#C42A21 46%,var(--brand-pure) 100%);
      box-shadow:inset 0 1px 0 rgba(255,255,255,.16),0 4px 16px -4px rgba(74,48,26,.45);
      position:relative;overflow:hidden}
/* Tanda Valoobuy, pada setiap skrin. Sebelum ini ia hanya ada pada splash —
 * kelihatan sesaat, kemudian brand hilang sepenuhnya dari app. */
/* Same reasoning as .m-bar-logo. Only reached on Home when no cust_id was
 * passed, so it is the fallback for the avatar rather than the usual case. */
.m-hi-logo{flex:0 0 auto;display:grid;place-items:center;width:32px;height:32px;
           border-radius:50%;background:#fff;
           box-shadow:0 1px 2px rgba(0,0,0,.18),inset 0 -1px 0 rgba(0,0,0,.06)}
.m-hi-logo .m-brand-mark{width:28px;height:28px;display:block}
.m-hi-who{min-width:0;flex:1 1 auto}
.m-hi::after{content:"";position:absolute;inset:0;pointer-events:none;
             background:repeating-linear-gradient(90deg,
                rgba(255,255,255,.055) 0 1px, transparent 1px 7px)}
.m-hi > *{position:relative}
.m-hi-greet{font-size:10.5px;color:rgba(255,255,255,.75);font-weight:500}
.m-hi-name{font-size:14.5px;font-weight:800;letter-spacing:-.01em;margin-top:1px}
.m-hi-badge{flex:0 0 auto;display:inline-flex;align-items:center;gap:6px;
            padding:5px 9px;border-radius:999px;
            background:rgba(255,255,255,.16);backdrop-filter:blur(6px);
            font-family:var(--fd);font-size:9.5px;font-weight:800;letter-spacing:.14em;
            text-transform:uppercase;color:#fff}
.m-hi-badge svg{width:13px;height:13px}

/* ══ BARIS TINDAKAN PANTAS ══════════════════════════════════════════════════
 * Empat destinasi yang member paling kerap tuju, sebaris, ikon dalam bulatan
 * bertona. Corak yang sama yang setiap app dompet Malaysia guna, sebab ia
 * berkesan: satu tekan dari skrin utama. */
.m-quick{display:grid;grid-template-columns:repeat(4,1fr);gap:7px;margin:10px}
.m-quick a{display:flex;flex-direction:column;align-items:center;gap:5px;
           padding:9px 3px 8px;border-radius:var(--r-md);text-decoration:none;
           background:var(--surface);border:1px solid var(--line);
           box-shadow:var(--rim),var(--sh-1);
           transition:transform .14s ease, box-shadow .2s ease}
.m-quick a:active{transform:translateY(2px) scale(.97);box-shadow:var(--rim)}
/* Jubin sebagai CIP FIZIKAL, bukan segi empat berwarna: sorotan inset di atas,
 * bayang inset di bawah, dan satu bayang jatuh yang cukup kuat untuk mengangkat
 * ia dari kad. Itu yang memberi rasa tiga dimensi tanpa satu imej pun. */
.m-quick .m-qi{width:44px;height:44px;border-radius:14px;display:grid;place-items:center;
               color:#fff;
               box-shadow:var(--rim-chip),
                          0 2px 4px rgba(74,48,26,.18),
                          0 6px 14px -2px rgba(74,48,26,.22)}
.m-quick .m-qi svg{width:22px;height:22px;stroke-width:2.1}
.m-quick span{font-size:11px;font-weight:600;letter-spacing:0;text-transform:none;
              color:var(--ink);text-align:center;line-height:1.25}
/* Setiap tindakan warna sendiri, PEJAL dengan glif putih — corak e-Kupon UC Tati,
 * yang memberi setiap tindakan warnanya sendiri dan bukan empat jubin sedondon.
 * Jubin bertona lembut hilang pada skrin telefon di bawah cahaya kedai. */
.m-qi-red{background:linear-gradient(140deg,#C4302A,var(--brand-red-dark))}
/* Kayu gelap, bukan emas pucat: glif putih di atas emas pucat hilang sepenuhnya
 * pada skrin telefon, dan keempat-empat jubin mesti sekuat satu sama lain. */
.m-qi-wood{background:linear-gradient(140deg,#B08F63,#7E6848)}
.m-qi-ok{background:linear-gradient(140deg,#2E9C63,var(--ok))}
/* Berkod tegar, TIDAK bertema. --brand-maroon dipetakan cerah dalam gelap kerana
 * ia mesti boleh dibaca sebagai TEKS atas kad gelap; memakai token yang sama
 * sebagai LATAR di sini memberi jubin merah jambu pucat dengan glif putih yang
 * hampir hilang. Jubin ialah cip brand pejal, seperti hero dan cakera QR yang
 * juga sengaja berkod tegar dalam kedua-dua tema. */
.m-qi-mar{background:linear-gradient(140deg,#A8433A,#7E2A23)}

/* ══ TAJUK BAHAGIAN DENGAN PAUTAN ═══════════════════════════════════════════ */
.m-sec{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
.m-sec a{font-family:var(--fd);font-size:12px;font-weight:600;letter-spacing:0;
         color:var(--brand-red);text-decoration:none}

/* ══ SCANNER ════════════════════════════════════════════════════════════════ */
.m-scan-stage{position:relative;border-radius:var(--r-lg);overflow:hidden;
              background:#000;aspect-ratio:1;box-shadow:var(--sh-2)}
.m-scan-stage video,.m-scan-stage canvas{width:100%!important;height:100%!important;
                                          object-fit:cover;display:block}
.m-scan-frame{position:absolute;inset:14%;border-radius:18px;pointer-events:none;
              box-shadow:0 0 0 100vmax rgba(0,0,0,.42)}
.m-scan-frame::before,.m-scan-frame::after{content:"";position:absolute;width:30px;height:30px;
              border:3px solid var(--wood)}
.m-scan-frame::before{top:-2px;left:-2px;border-right:0;border-bottom:0;border-radius:14px 0 0 0}
.m-scan-frame::after{bottom:-2px;right:-2px;border-left:0;border-top:0;border-radius:0 0 14px 0}
.m-scan-result{margin-top:12px;padding:12px 13px;border-radius:var(--r-md);
               border:1px solid var(--ok);
               background:var(--ok-soft);color:var(--ok);font-weight:700;font-size:13.5px}

/* ── Bar offline ────────────────────────────────────────────────────────────
 * Melekat di atas segala-galanya. Member yang kehilangan talian patut tahu
 * sebabnya sebelum dia menyalahkan app. */
.m-offline{position:sticky;top:0;z-index:30;padding:7px 12px;text-align:center;
           font-family:var(--fd);font-size:10px;font-weight:800;letter-spacing:.13em;
           text-transform:uppercase;background:var(--warn);color:#fff}
.m-offline[hidden]{display:none}
.m-more{display:inline-block;margin-top:10px;padding:8px 14px;border-radius:999px;
        border:1.5px solid var(--line);background:var(--surface-2);
        font-family:var(--fd);font-size:12.5px;font-weight:600;letter-spacing:0;
        color:var(--brand-red);text-decoration:none;
        transition:transform .12s ease}
.m-more:active{transform:scale(.96)}

/* ══ SETTINGS ═══════════════════════════════════════════════════════════════
 * Ditambah 3 Sep 2026 bersama skrin Settings. Tiada rupa baharu direka di sini:
 * kad, eyebrow, .m-row dan .m-ico ialah kelas yang sudah ada, dan blok ini cuma
 * menambah apa yang memang belum ada — segmen tiga-keadaan, baris tetapan,
 * suis on/off, dan pautan log keluar yang senyap. Padding 12, radius 16, tajuk
 * 13px: sama seperti seluruh app, sebab "KECIKKAN SIKIT" sudah dijawab sekali. */

/* Segmen pilihan-tunggal (tema tiga keadaan, bahasa dua). flex + flex:1 supaya
 * DUA atau TIGA pilihan kedua-duanya mengisi lebar penuh tanpa lebar ajaib. */
.m-seg3{display:flex;gap:3px;margin-top:9px;padding:3px;
        background:var(--surface-3);border:1px solid var(--line);border-radius:999px}
.m-seg3 button{flex:1 1 0;min-width:0;display:flex;flex-direction:column;
               align-items:center;justify-content:center;gap:4px;
               padding:7px 4px 6px;border:0;border-radius:999px;background:none;
               color:var(--muted);cursor:pointer;
               font-family:var(--fd);font-size:11px;font-weight:600;letter-spacing:0;
               transition:background .2s ease,color .2s ease,transform .12s ease}
.m-seg3 button svg{width:17px;height:17px;flex:0 0 auto}
.m-seg3 button:active{transform:scale(.96)}
.m-seg3 button[aria-checked="true"]{background:var(--surface);color:var(--brand-red);
                                    box-shadow:var(--sh-1)}

/* Baris tetapan. Boleh jadi <a> (ada .m-set-go), <div> (ada suis atau nilai)
 * atau <button>. Tinggi minimum 46px = sasaran tekan yang cukup pada telefon. */
.m-set{display:flex;align-items:center;gap:11px;width:100%;min-height:46px;
       padding:9px 0;margin:0;border:0;border-top:1px solid var(--line);
       background:none;text-align:left;text-decoration:none;color:inherit;
       font-family:var(--fb)}
/* JANGAN guna :first-of-type di sini. Baris tetapan ialah <a>, <div> DAN
 * <button> bercampur, dan :first-of-type dikira per JENIS elemen — jadi setiap
 * satu daripada tiga jenis itu menjadi "yang pertama" dan kehilangan pemisahnya.
 * Diukur pada skrin ini sebelum baris ini ditulis: 5 daripada 6 baris tiada
 * pemisah, termasuk baris di tengah kad. Baris pertama sebenarnya sentiasa
 * baris yang mengikut .m-eyebrow, atau anak pertama kad yang tiada tajuk. */
.m-card > .m-set:first-child,
.m-card > .m-eyebrow + .m-set{border-top:0}
.m-set-main{flex:1 1 auto;min-width:0}
.m-set-t{display:block;font-family:var(--fd);font-size:13px;font-weight:600;
         letter-spacing:0;color:var(--ink)}
.m-set-s{display:block;margin:2px 0 0;font-size:11.5px;line-height:1.45;
         color:var(--muted)}
/* Same class also renders as a standalone <p> hint (settings_appearance.php,
 * settings_language.php), not just the <span> inside .m-set rows above — and
 * those two <p> usages sit inside .m-card, where the bare class above loses
 * the font-size tie to `.m-card p` (0,1,0 vs 0,1,1), silently rendering at
 * 12.5px instead of 11.5px. This tag-qualified rule only matches the <p>
 * case, so the <span> usages are untouched and keep using the rule above. */
p.m-set-s{font-size:11.5px}
.m-set-val{flex:0 0 auto;max-width:52%;font-size:12px;font-weight:600;
           color:var(--muted);text-align:right;overflow-wrap:anywhere;
           font-variant-numeric:tabular-nums}
.m-set-go{flex:0 0 auto;width:18px;height:18px;color:var(--muted);
          transition:transform .16s ease}
a.m-set:active .m-set-go{transform:translateX(2px)}
a.m-set:active,button.m-set:active{opacity:.7}

/* Suis on/off. Butang asli + role="switch"; knob bergerak dengan TRANSFORM
 * sahaja, jadi ia betul walaupun peralihan tidak pernah berjalan. */
.m-sw{flex:0 0 auto;position:relative;width:44px;height:26px;padding:0;border:0;
      border-radius:999px;background:var(--surface-3);
      box-shadow:inset 0 0 0 1.5px var(--line);cursor:pointer;
      transition:background .22s ease,box-shadow .22s ease}
.m-sw::after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;
             border-radius:50%;background:var(--surface);
             box-shadow:0 1px 3px rgba(0,0,0,.28);
             transition:transform .22s cubic-bezier(.2,.8,.2,1)}
.m-sw[aria-checked="true"]{background:var(--ok);box-shadow:inset 0 0 0 1.5px var(--ok)}
.m-sw[aria-checked="true"]::after{transform:translateX(18px)}
.m-sw:disabled{opacity:.5;cursor:not-allowed}

/* Nota jujur di bawah satu kawalan — "digunakan di sini, akaun belum simpan".
 * Bukan ralat: tiada merah, tiada ikon amaran.
 * `p.m-note`, not `.m-note` — same `.m-card p` specificity trap as `.m-lead`
 * above: all 3 Settings save-confirmation notes were silently rendering at
 * 12.5px/no-top-margin instead of the intended 11.5px/8px. */
p.m-note{margin:8px 0 0;font-size:11.5px;line-height:1.45;color:var(--muted)}

/* Log keluar: teks bergaris bawah --muted, bukan butang merah penuh-lebar.
 * Butang merah penuh-lebar ialah kawalan paling menonjol di halaman sebaik
 * sahaja "More" dibuka, dan itu membatalkan seluruh maksud menyorokkannya. */
.m-danger-link{display:inline-flex;align-items:center;min-height:44px;padding:8px 0;
               border:0;background:none;
               font-family:var(--fd);font-size:12.5px;font-weight:600;
               color:var(--muted);text-decoration:underline;text-underline-offset:3px;
               cursor:pointer;transition:opacity .18s ease}
.m-danger-link:active{opacity:.55}
.m-danger-link:disabled{opacity:.45;cursor:not-allowed}

/* Teks panjang (privasi). 12.5px/1.6 — sama seperti .m-card p, cuma bernafas. */
.m-prose p{margin:0 0 8px;font-size:12.5px;line-height:1.6;color:var(--muted)}
.m-prose p:last-child{margin-bottom:0}
.m-prose ul{margin:0 0 8px;padding-left:17px}
.m-prose li{font-size:12.5px;line-height:1.6;color:var(--muted);margin:0 0 3px}
.m-prose strong{color:var(--ink);font-weight:600}

@media (prefers-reduced-motion: reduce){
  .m-seg3 button,.m-sw,.m-sw::after,.m-set-go,.m-danger-link{transition:none}
}


/* Butang Download Invoice, purchase.php. Sama tapak seperti .m-btn (gradient
 * brand, bayang jatuh) tapi lebar auto dan bukan blok penuh — ia satu tindakan
 * pilihan dalam kad kecil sendiri, bukan langkah wajib borang. */
.m-inv-btn{display:flex;align-items:center;justify-content:center;gap:8px;
           width:100%;padding:13px;border:0;border-radius:var(--r-md);
           color:#fff;cursor:pointer;font-family:var(--fd);font-size:13.5px;
           font-weight:700;letter-spacing:0;
           background:linear-gradient(135deg,var(--btn-a),var(--brand-red-dark));
           box-shadow:0 6px 18px rgba(179,36,28,.28);
           transition:transform .12s ease, box-shadow .2s ease, opacity .2s ease}
.m-inv-btn:not(:disabled):active{transform:translateY(1px) scale(.985);
                                 box-shadow:0 3px 10px rgba(179,36,28,.24)}
.m-inv-btn:disabled{opacity:.75;cursor:wait}
.m-inv-icon{flex:0 0 auto}
/* Titik "Generating…" — tiga titik melantun bergilir, bukan spinner generik,
 * supaya ia sepadan dengan animasi lembut yang lain dalam app ni (WAJIB). */
.m-inv-dots{display:inline-flex;gap:3px;margin-left:2px}
.m-inv-dots i{width:4px;height:4px;border-radius:50%;background:#fff;
              display:inline-block;animation:mInvBounce 1.1s ease-in-out infinite}
.m-inv-dots i:nth-child(2){animation-delay:.15s}
.m-inv-dots i:nth-child(3){animation-delay:.3s}
@keyframes mInvBounce{
  0%,60%,100%{transform:translateY(0);opacity:.5}
  30%{transform:translateY(-3.5px);opacity:1}
}

.m-mono{font-family:var(--fd);font-variant-numeric:tabular-nums;letter-spacing:.02em}

/* Baris senarai belian yang boleh ditekan, cip julat tarikh dan pager TIDAK ada
 * di sini — ia dalam includes/purchases.css, dimuatkan oleh purchases.php sahaja.
 * Satu blok yang aku tulis di sini pada mulanya bertindih dengan .m-buy-go di
 * sana dan menetapkan flex pada <svg> mereka; ia dibuang. Satu kelas, satu
 * pemilik. */

/* ══ AVATAR ═════════════════════════════════════════════════════════════════
 * Inisial, bukan bulatan kelabu kosong. Lihat includes/avatar.php untuk sebab
 * warnanya diterbitkan dari cust_id dan bukan dari nama: nama boleh disunting,
 * dan avatar yang bertukar warna hari seseorang membetulkan ejaannya sendiri
 * membaca sebagai app yang hilang jejak siapa dia.
 *
 * Empat tona ini ialah tona yang jubin tindakan pantas sudah guna. Hue rawak
 * (hash % 360) akan menghasilkan cakera sian dan limau yang tidak menjadi milik
 * mana-mana bahagian lain app ini. */
.m-av{flex:0 0 auto;display:grid;place-items:center;border-radius:50%;
      font-family:var(--fd);font-weight:700;letter-spacing:.01em;line-height:1;
      color:#fff;overflow:hidden;
      box-shadow:var(--rim-chip)}
.m-av svg{width:56%;height:56%}

.m-av.is-t0{background:linear-gradient(140deg,#C4302A,var(--brand-red-dark))}
.m-av.is-t1{background:linear-gradient(140deg,#B08F63,#7E6848)}
.m-av.is-t2{background:linear-gradient(140deg,#2E9C63,var(--ok))}
.m-av.is-t3{background:linear-gradient(140deg,#A8433A,var(--brand-maroon))}

/* Pada bar merah, cakera bertona brand tidak kelihatan. Basuhan putih lut
 * bertindak sebagai kaca terhadap gradien di belakangnya. */
.m-av.is-onbrand{background:rgba(255,255,255,.18);color:#fff;
                 box-shadow:inset 0 1px 0 rgba(255,255,255,.28)}

/* Tiada nama pada rekod — 12,610 customer. Glif orang, bukan cakera kosong. */
.m-av.is-anon{font-size:0}

/* ── AVATAR PHOTO ────────────────────────────────────────────────────────────
 * The member's own picture, in the SAME circle the initials use, so adding a
 * photo moves nothing on the screen.
 *
 * The wrapper is `position:relative` and the fallback disc is absolutely
 * positioned UNDER the image, not hidden. That is what makes the fallback need
 * no script: the img sits on top when it loads, and `onerror="this.remove()"`
 * uncovers the disc when it does not. A `display:none` fallback toggled by JS
 * would leave a member with a blank circle on any WebView where that script
 * did not run.
 *
 * object-fit:cover with a square source is belt and braces - api/avatar.php
 * already centre-crops to a square before saving, so there is nothing left to
 * crop, but a row written by hand would otherwise stretch. */
/* == THE MEMBER, ON THEIR OWN ACCOUNT SCREEN ==============================
 * Avatar left, name and the photo controls right. The avatar LEADS because it
 * is what makes this read as "my account" rather than as another settings
 * list, and Account is the one place a member can change it.
 *
 * The controls are text links, not buttons: they sit inside a card that is
 * already full of chevron rows, and two filled buttons here would compete
 * with the card's real actions. They are still 44px tall via the padding. */
/* == LATEST ORDER, WAITING TO BE COLLECTED ================================
 * The one card on Account that is an ANNOUNCEMENT rather than a row, so it
 * gets the wood tone rather than the plain surface: it is the only thing on
 * the screen that is news, and it disappears entirely once nothing is
 * outstanding.
 *
 * NOT the brand red. Red on this app means "this is the brand" or "this is
 * money" (the points figure, the redeem cost), and a red card here would read
 * as a warning about an order that is perfectly fine. Wood is the second
 * brand tone and is used for exactly this kind of "look at me" surface. */
.m-wait{display:block;margin:10px;padding:12px 13px;border-radius:var(--r-lg);
        text-decoration:none;color:inherit;
        background:linear-gradient(140deg,var(--wood-soft),var(--surface-2));
        border:1.5px solid var(--wood);
        box-shadow:var(--rim),var(--sh-2);
        -webkit-tap-highlight-color:transparent}
.m-wait:active{transform:translateY(1px) scale(.995)}
.m-wait-top{display:flex;align-items:center;justify-content:space-between;gap:9px}
.m-wait-eyebrow{font-family:var(--fd);font-size:10px;font-weight:700;
                letter-spacing:.1em;text-transform:uppercase;color:var(--wood-deep)}
/* A WORD, not only a colour - the brand is red, so colour alone can never
 * carry a status in this app. Ink on wood measures well past AA. */
.m-wait-chip{flex:0 0 auto;padding:3px 9px;border-radius:999px;
             font-family:var(--fd);font-size:9.5px;font-weight:700;
             letter-spacing:.06em;text-transform:uppercase;white-space:nowrap;
             background:var(--wood);color:#2A1F16}
/* NOT bare classes: `.m-card p` is (0,1,1) and this card sits outside a
 * .m-card, but the same specificity trap has bitten .m-lead, .m-pur-note,
 * .m-adm-text, .m-faq p, .m-rw-b and .m-me-msg, so the tag stays. */
p.m-wait-line{margin:7px 0 0;font-family:var(--fd);font-size:13.5px;
              font-weight:700;letter-spacing:-.01em;color:var(--ink);line-height:1.35}
p.m-wait-sub{margin:3px 0 0;font-size:11.5px;line-height:1.45;color:var(--wood-deep)}

.m-me{display:flex;align-items:flex-start;gap:13px;margin-bottom:11px}
.m-me-av{flex:0 0 auto;display:block}
.m-me-who{min-width:0;flex:1 1 auto}

/* A LABEL wrapping a hidden input, so it opens the picker with no script and
 * is reachable by keyboard. Inline-flex with real padding gives it the 44px
 * this app settled on after the sizing audit without looking like a button. */
.m-me-edit{display:inline-flex;align-items:center;min-height:34px;
           margin-top:5px;padding:6px 0;cursor:pointer;
           font-family:var(--fd);font-size:12px;font-weight:700;
           color:var(--brand-red);-webkit-tap-highlight-color:transparent}
.m-me-edit:active{opacity:.7}
.m-me-rm{display:inline-flex;align-items:center;min-height:34px;
         margin:5px 0 0 14px;padding:6px 0;border:0;background:none;cursor:pointer;
         font-family:var(--fd);font-size:12px;font-weight:600;color:var(--muted);
         -webkit-tap-highlight-color:transparent}
.m-me-rm[hidden]{display:none}
.m-me-rm:active{opacity:.7}
/* NOT a bare class: `.m-card p` is (0,1,1) and would win. Same trap already
 * recorded for .m-lead, .m-pur-note, .m-adm-text, .m-faq p and .m-rw-b. */
p.m-me-msg{margin:5px 0 0;font-size:11.5px;line-height:1.45}
p.m-me-msg.is-ok{color:var(--ok)}
p.m-me-msg.is-bad{color:var(--brand-red)}
p.m-me-msg[hidden]{display:none}

.m-av-wrap{flex:0 0 auto;position:relative;display:block;border-radius:50%;
           overflow:hidden;isolation:isolate}
.m-av-img{position:relative;z-index:1;display:block;width:100%;height:100%;
          object-fit:cover;border-radius:50%}
.m-av.is-fb{position:absolute;inset:0;width:100%!important;height:100%!important}

/* Tanda dalam bar tajuk. Sebelum 4 Sep 2026 tiada slot logo dalam .m-bar sama
 * sekali, jadi tanda itu hanya hidup pada splash dan ikon app — sesaat semasa
 * lancar, kemudian brand hilang dari seluruh app. Home ialah pengecualian: slot
 * yang sama di sana membawa avatar member. */
.m-bar{display:flex;align-items:center;gap:9px}
/* THE MARK IN THE TITLE BAR.
 *
 * A SOLID WHITE CIRCLE, not the translucent rounded square this used to be.
 * That square worked while the mark was an SVG whose V took the bar's own white
 * via currentColor. The mark is now the owner's artwork, which is a RED disc,
 * and a red disc inside a 16%-white square on a red bar is mud. White plate,
 * circle to match the artwork's own shape, and 2px of it showing all round so
 * the disc has an edge to sit against. */
.m-bar-logo{flex:0 0 auto;display:grid;place-items:center;width:26px;height:26px;
            border-radius:50%;background:#fff;
            box-shadow:0 1px 2px rgba(0,0,0,.18),inset 0 -1px 0 rgba(0,0,0,.06)}
.m-bar-logo .m-brand-mark{width:22px;height:22px;display:block}

/* THE WAY OUT of a page that has no tab bar (Our stores, and anything else
 * reached by a link that names no tab). It sits in the logo's slot because
 * they are mutually exclusive: a page either offers the brand or offers the
 * way back, never both, and swapping them keeps the title on the same
 * baseline either way.
 *
 * 44x44 with a NEGATIVE left margin. The icon is 20px and would be a 20px
 * target; the box is padded out to the minimum the sizing audit set for this
 * app, and then pulled back so the arrow still lines up with where the logo
 * was rather than sitting 9px further in. */
.m-bar-back{flex:0 0 auto;display:grid;place-items:center;width:44px;height:44px;
            margin:-9px 0 -9px -10px;color:#fff;text-decoration:none;
            -webkit-tap-highlight-color:transparent}
.m-bar-back svg{width:20px;height:20px;display:block}
.m-bar-back:active svg{transform:translateX(-2px)}

/* ══ PERALIHAN GELONGSOR — APK ANDROID SAHAJA ═══════════════════════════════
 * Pemilik menguji APK pada S24 Ultra dan membandingkannya dengan app ZUS:
 * halaman patut MENGGELONGSOR kiri/kanan seperti Android asli, bukan pudar-
 * silang. Dia minta ini khusus untuk APK — web dan iOS kekal pudar-silang, dan
 * itu sebabnya setiap peraturan di bawah dipagar dengan [data-app="android"].
 * Tanpa pagar itu, paparan desktop yang staf guna akan berubah juga.
 *
 * ARAH ITU PENTING. Gelongsor yang sentiasa ke satu arah membuat butang BACK
 * terasa seperti melangkah ke hadapan, dan itu lebih buruk daripada pudar-
 * silang — ia bercanggah dengan apa yang tangan sudah tahu. Arah datang dari
 * data-nav yang ditulis oleh pagereveal (lihat layout.php); tanpa JS, atau pada
 * WebView yang tidak menyokongnya, tiada data-nav dan blok "push" di bawah
 * berkuat kuasa sebagai lalai — masih menggelongsor, cuma tidak tahu arah.
 *
 * Transform SAHAJA pada elemen kandungan. Peraturan rumah: tiada animasi yang
 * kebolehlihatannya bergantung pada opacity-dari-0, sebab versi lama app ini
 * pernah menghantar splash yang kekal tidak kelihatan selama-lamanya kerana
 * animasi masuknya tidak pernah berjalan. */
@media (prefers-reduced-motion: no-preference){
  :root[data-app="android"]::view-transition-old(root),
  :root[data-app="android"]::view-transition-new(root){
    /* Ganti animasi pudar-silang lalai sepenuhnya. */
    animation:none;
    /* Dua snapshot mesti bertindan, bukan berkongsi ruang. */
    mix-blend-mode:normal;
  }

  /* KE HADAPAN (lalai): yang baharu masuk dari kanan, yang lama keluar ke kiri. */
  :root[data-app="android"]::view-transition-old(root){
    animation:vbSlideOutL .26s cubic-bezier(.32,.72,0,1) both
  }
  :root[data-app="android"]::view-transition-new(root){
    animation:vbSlideInR .26s cubic-bezier(.32,.72,0,1) both
  }

  /* KE BELAKANG: cermin. */
  :root[data-app="android"][data-nav="back"]::view-transition-old(root){
    animation:vbSlideOutR .24s cubic-bezier(.32,.72,0,1) both
  }
  :root[data-app="android"][data-nav="back"]::view-transition-new(root){
    animation:vbSlideInL .24s cubic-bezier(.32,.72,0,1) both
  }
}

/* Yang keluar bergerak SEPARUH jarak yang masuk. Android menggerakkan lapisan
 * di bawah lebih perlahan daripada yang di atas; menggerakkan kedua-duanya
 * 100% terasa seperti dua slaid bertukar, bukan satu helaian di atas yang lain. */
@keyframes vbSlideInR  { from { transform:translateX(100%) } to { transform:none } }
@keyframes vbSlideOutL { to   { transform:translateX(-32%) } }
@keyframes vbSlideInL  { from { transform:translateX(-100%) } to { transform:none } }
@keyframes vbSlideOutR { to   { transform:translateX(32%) } }

/* Kromium tetap tidak menggelongsor. Bar tajuk dan tab bar sudah membawa
 * view-transition-name sendiri, jadi ia kekal di tempatnya sementara hanya
 * kandungan bergerak — itu yang membezakan "app" daripada "laman web". */

/* ══ SKELETON ═══════════════════════════════════════════════════════════════
 * Placeholder shapes for the three Home sections Android fetches AFTER first
 * paint (index.php + api/home_data.php) — the owner's own ZUS reference for
 * "what a genuine fetch should look like while it loads": grey blocks sized
 * and positioned like the real content, not a spinner on a blank screen.
 *
 * Own classes, not the real .m-nw-fig/.m-nw-card etc reused with a grey fill —
 * those already carry their own `background` from news.css/theme.css, loaded
 * AFTER this file, which would silently win the cascade and erase the shimmer.
 * A skeleton needs the real components' SIZE (aspect-ratio, radius, padding),
 * borrowed by using those classes alongside these for layout only, but its own
 * fill colour, never fighting the real one for the same property. */
.m-sk-line{height:12px;border-radius:6px;background:var(--surface-3)}
.m-sk-line.m-sk-w40{width:40%}
.m-sk-line.m-sk-w60{width:60%}
.m-sk-line.m-sk-w80{width:80%}
.m-sk-block{border-radius:var(--r-md);background:var(--surface-3)}
.m-sk-fig{aspect-ratio:16/9;border-radius:0}

@media (prefers-reduced-motion: no-preference){
  .m-sk-line,.m-sk-block{
    background:linear-gradient(100deg,var(--surface-3) 30%,var(--surface-2) 50%,var(--surface-3) 70%);
    background-size:200% 100%;
    animation:mSkShimmer 1.5s ease-in-out infinite}
}
@keyframes mSkShimmer{ 0%{background-position:200% 0} 100%{background-position:-200% 0} }

/* .m-nw-card is normally an <a> with :active/:focus-visible rules from
 * news.css — the skeleton reuses it as a plain <div> for the same box shape
 * (radius, shadow, overflow) with none of those interactive rules mattering
 * since nothing here is clickable yet. */
.m-sk .m-nw-card{cursor:default}

/* ══ SKELETON PERALIHAN HALAMAN ═════════════════════════════════════════════
 * Masalah yang ini selesaikan, dalam kata pemiliknya: "kalau contoh kita tekan
 * apa2 button untuk masuk ke page sub tu..macam mane? tak boleh ke kalau dia
 * terus instant". Menekan resit atau Settings meninggalkan halaman LAMA di atas
 * skrin sampai dokumen baharu tiba, jadi tekanan pertama terasa seperti tiada
 * apa berlaku.
 *
 * Prapaparan TIDAK menyelesaikannya, dan itu sudah dibuktikan pada tab dahulu:
 * rangkaian bukan bahagian yang MATA nampak, lukisan semula dokumen itulah.
 * Sub-page juga tidak boleh dipramuat kerana bilangannya tidak terhad, satu
 * resit satu halaman.
 *
 * Jadi ini memberi jawapan serta-merta pada bingkai ketukan itu sendiri:
 * bentuk kelabu bersaiz kad sebenar, dipasang oleh kelas .is-navving pada
 * <html> sebelum penyemak imbas mula menavigasi. Bila dokumen baharu tiba ia
 * menggantikan semuanya, jadi tiada apa perlu dipadam.
 *
 * Z-INDEX 16 dengan sengaja: .m-top ialah 20 dan .m-offline 30, jadi bar tajuk
 * dan jalur luar talian kekal di atas ini. .m-tabs pula tiada z-index sendiri,
 * jadi ia diberi 21 di sini supaya bar bawah tidak ditutup — di dalam APK
 * kedua-duanya dilukis oleh Flutter dan tidak hadir sama sekali, jadi hamparan
 * ini menutup keseluruhan tetingkap di sana, iaitu betul. */
.m-nav-sk{position:fixed;inset:0;z-index:16;display:none;overflow:hidden;
          background:var(--surface-2);padding:10px}
html.is-navving .m-nav-sk{display:block}
.m-tabs{z-index:21}
/* Kad tiruan: bingkai dan bayang kad betul, isinya baris kelabu. Saiz diambil
 * dari .m-card supaya peralihan ke halaman sebenar tidak melompat. */
.m-nav-sk-card{background:var(--surface);border:1.5px solid var(--line-strong);
               border-radius:var(--r-lg);box-shadow:var(--rim),var(--sh-2);
               padding:12px;margin-bottom:10px}
.m-nav-sk-row{display:flex;align-items:center;gap:11px;margin-bottom:11px}
.m-nav-sk-av{flex:0 0 auto;width:34px;height:34px;border-radius:50%;
             background:var(--surface-3)}
.m-nav-sk-col{flex:1 1 auto;min-width:0;display:grid;gap:7px}

/* ══ TAP FEEDBACK — OURS, NOT THE BROWSER'S ════════════════════════════════
 *
 * THE BLUE BOX. Android's WebView paints its own translucent highlight over
 * whatever you touch, and iOS Safari paints a grey one. It is the wrong shape
 * (the element's box, ignoring border-radius), the wrong colour (a system blue
 * that belongs to no part of this brand) and the wrong timing (it lingers after
 * the finger leaves). On the tab bar it was the ONLY thing that responded to a
 * tap on Home, which is exactly the complaint: the app's own feedback was the
 * platform default.
 *
 * Four components had already switched it off one at a time. That is how it
 * ends up switched off on four things and on for the other sixteen, so it is
 * now off once, here, for everything that can be touched.
 *
 * KEYBOARD FOCUS IS PUT BACK, DELIBERATELY. Killing the highlight is not a
 * licence to kill the focus ring: someone navigating this app with a keyboard,
 * a switch or a Bluetooth remote needs to see where they are. :focus-visible is
 * what separates the two — the browser only matches it when focus did NOT come
 * from a touch or a mouse press, so a finger gets our press state and a Tab key
 * gets a real ring. Doing this with :focus would put the ring back on every tap
 * and undo the whole change.
 */
a, button, summary, label,
input, select, textarea,
[role="button"], [role="tab"], [role="link"] {
  -webkit-tap-highlight-color: transparent;
}

a:focus-visible, button:focus-visible, summary:focus-visible,
[role="button"]:focus-visible, [role="tab"]:focus-visible {
  outline: 2px solid var(--brand-red);
  outline-offset: 2px;
  border-radius: 6px;
}

/* ══ AND SOMETHING HAS TO TAKE ITS PLACE ═══════════════════════════════════
 *
 * Removing the highlight without giving the element its own press state does
 * not make the app feel faster, it makes it feel BROKEN — the finger lands and
 * nothing at all happens until the next page paints. These are the elements
 * that had no :active of their own; the rest already had one.
 *
 * transform, not colour: a scale is composited on the GPU, so it appears on the
 * same frame as the touch even while the WebView is busy fetching the next
 * page. A background change waits for a paint, which is the frame we are trying
 * not to lose. 60ms in, 180ms back out — fast enough to feel like a physical
 * button, slow enough on release not to look like a glitch.
 */
/* .m-btn, .m-auth-btn and .m-inv-btn are NOT in this list, and leaving them in
 * was a regression I had to back out. All three already carried
 * translateY(1px) scale(.985) plus a shadow change - a button visibly pressing
 * INTO the page, which is better than a flat shrink. My blanket rule sat later
 * in the file at equal specificity, so it overrode their transform and threw
 * the translateY away.
 *
 * What put them on the list was a bad grep: I searched for "\.m-btn:active"
 * and their rules are written ".m-btn:not(:disabled):active", so the detection
 * reported "no press state" for three elements that had a better one than the
 * one I was about to add. Re-detecting with the class anywhere in the selector
 * found them. .m-set went in and came straight back out for the same reason:
 * it already had a.m-set:active{opacity:.7} plus a chevron nudge, written with
 * an element-qualified selector my second grep also missed. Three failed greps
 * in a row is the lesson - the reliable way to ask "does a press state already
 * exist" is to ask the BROWSER which rules match, not to pattern-match CSS
 * text. */
.m-collapse-btn:active,
.m-pur-back:active,
.m-auth-link:active:not(:disabled) {
  transform: scale(.972);
  transition-duration: .06s;
}
.m-collapse-btn, .m-pur-back, .m-auth-link {
  transition: transform .18s cubic-bezier(.32,.72,0,1);
}

/* The tab bar gets a firmer one than the rest. It is the control that gets hit
 * most, usually while walking, and it is the one the owner named. */
.m-tab:active svg { transform: scale(.88) }
.m-tab[aria-current="page"]:active svg { transform: translateY(-1px) scale(.94) }

@media (prefers-reduced-motion: reduce) {
  .m-collapse-btn, .m-pur-back, .m-auth-link { transition: none }
  .m-collapse-btn:active, .m-pur-back:active,
  .m-auth-link:active, .m-tab:active svg { transform: none }
}
