/* ==========================================================================
   Risos AI — Colors & Type Tokens
   PT Riset Sinergi Sosial
   Source: design_handoff_app_redesign/designs/colors_and_type.css

   NOTE: Google Fonts loaded via <link rel="stylesheet"> in base.html
   (preconnect + parallel load). DO NOT @import here — it would parse-block.
   ========================================================================== */

:root {
  /* ── BACKGROUND / SURFACE ─────────────────────────────────── */
  --c-bg-dark:        #0A0F1E;
  --c-bg-deeper:      #060914;
  --c-bg-light:       #F8F9FC;
  --c-bg-paper:       #FFFFFF;
  --c-bg-cream:       #F2F4F9;

  /* App-mode dark surfaces (legacy — used by old shell) */
  --c-app-bg-1:       #0a0a14;
  --c-app-bg-2:       #14141f;
  --c-app-bg-3:       #1a1a2e;

  /* ── PRIMARY (cyan-blue) ─────────────────────────────────── */
  --c-primary:        #3B82F6;
  --c-primary-deep:   #2563EB;
  --c-accent:         #06B6D4;
  --c-accent-bright:  #22D3EE;

  /* ── BRAND IDENTITY (PT Riset Sinergi Sosial — dark blue / navy) ──
     Rio 20 Mei 2026: Risos AI tema biru kegelap-gelapan, bukan ungu/indigo.
     Migrated from indigo (#1e1b4b/#4f46e5/#60A5FA) to navy/blue-800/blue-700.
     ──────────────────────────────────────────────────────────────── */
  --c-brand-deep:     #0c1c3d;
  --c-brand-mid:      #1e3a8a;
  --c-brand:          #1e40af;
  --c-brand-soft:     #60a5fa;

  /* App-mode legacy accent — shifted from indigo→violet to dark blue.
     Kept variable names for backward compat with non-migrated pages. */
  --c-app-accent:        #1d4ed8;
  --c-app-accent-hover:  #1e40af;
  --c-app-accent-soft:   rgba(29,78,216,0.15);

  /* ── TEXT ─────────────────────────────────────────────────── */
  --c-text:                #0F172A;
  --c-text-muted:          #334155;
  --c-text-soft:           #475569;
  --c-text-faint:          #64748B;

  --c-text-on-dark:        #F1F5F9;
  --c-text-on-dark-muted:  #CBD5E1;
  --c-text-on-dark-faint:  #94A3B8;

  /* ── LINES / BORDERS ──────────────────────────────────────── */
  --c-line:           #E2E8F0;
  --c-line-strong:    #CBD5E1;
  --c-line-dark:      rgba(255,255,255,0.08);
  --c-line-dark-2:    rgba(255,255,255,0.16);

  /* ── SEMANTIC ─────────────────────────────────────────────── */
  --c-success:        #22c55e;
  --c-success-soft:   rgba(34,197,94,0.15);
  --c-warning:        #eab308;
  --c-warning-soft:   rgba(234,179,8,0.15);
  --c-danger:         #ef4444;
  --c-danger-soft:    rgba(239,68,68,0.15);

  /* ── GRADIENTS ────────────────────────────────────────────── */
  --grad-primary:       linear-gradient(135deg, #3B82F6 0%, #06B6D4 100%);
  --grad-primary-soft:  linear-gradient(135deg, rgba(59,130,246,0.18) 0%, rgba(6,182,212,0.18) 100%);
  --grad-text:          linear-gradient(120deg, #3B82F6 0%, #06B6D4 60%, #22D3EE 100%);
  --grad-text-deep:     linear-gradient(120deg, #0c1c3d 0%, #1e40af 50%, #2563EB 100%);
  --grad-app-accent:    linear-gradient(135deg, #1e40af 0%, #1d4ed8 100%);

  /* ── TYPE FAMILIES ────────────────────────────────────────── */
  --ff-display: 'Playfair Display', 'Times New Roman', serif;
  --ff-body:    'DM Sans', system-ui, sans-serif;
  --ff-mono:    'JetBrains Mono', ui-monospace, monospace;
  --ff-app:     'Inter', system-ui, -apple-system, sans-serif;

  /* ── FLUID TYPE SCALE ─────────────────────────────────────── */
  --fs-xs:      clamp(0.74rem, 0.72rem + 0.08vw, 0.8rem);
  --fs-sm:      clamp(0.85rem, 0.83rem + 0.1vw, 0.92rem);
  --fs-base:    clamp(0.96rem, 0.92rem + 0.2vw, 1.04rem);
  --fs-lede:    clamp(1.05rem, 1.0rem + 0.4vw, 1.2rem);
  --fs-h3:      clamp(1.3rem, 1.15rem + 0.6vw, 1.55rem);
  --fs-h2:      clamp(2.0rem, 1.4rem + 2.4vw, 3.6rem);
  --fs-h1:      clamp(2.6rem, 1.5rem + 4.5vw, 5.8rem);
  --fs-display: clamp(2.4rem, 1.4rem + 3.5vw, 4.6rem);

  /* ── SPACING ──────────────────────────────────────────────── */
  --space-1: 0.25rem;  --space-2: 0.5rem;   --space-3: 0.75rem;
  --space-4: 1rem;     --space-5: 1.5rem;   --space-6: 2rem;
  --space-7: 3rem;     --space-8: 4.5rem;   --space-9: 6rem;   --space-10: 8rem;

  /* ── LAYOUT ──────────────────────────────────────────────── */
  --container:        1240px;
  --container-narrow: 880px;
  --container-pad:    clamp(1.25rem, 4vw, 2.5rem);

  /* ── RADII — SATU tangga untuk seluruh app ────────────────────
     Sampai 23 Agu 2026 ada DUA tangga radius yang hidup berdampingan,
     dan keduanya menang di tempat berbeda:

       tokens.css:102  --r-*      8 · 14 · 22 · 32 · 44 · 999
       tokens.css:413  --r-*     10 · 13 · 18 ·  –  ·  – · 999   ← menang
       style.css:54    --radius-* 8 · 12 · 16 · 24 ·  – · 100

     Dua deklarasi `--r-*` di berkas YANG SAMA, sama-sama di `:root`,
     dibaca berurutan — yang bawah menang. Grep menemukan tangga
     pertama, peramban memakai yang kedua: kelas kegagalan
     "sudah diperbaiki tapi tidak berubah" yang paling sulit didiagnosa.

     Akibatnya terukur di peramban (1440px, 9 rute, elemen ≥8×8px):
     13 nilai radius berbeda dirender, dengan tiga rumpun yang
     berjarak ≤2px dan karena itu tidak terbaca sebagai pilihan
     rancangan melainkan sebagai kelalaian — 8px×628 / 9px×40 /
     10px×370, lalu 12px×112 / 13px×305 / 14px×52, lalu 16px×120 /
     18px×376.

     Sekarang: satu tangga, di satu tempat. `--radius-*` di style.css
     menjadi ALIAS ke tangga ini (bukan angka kedua), jadi 459
     pemakaian lama ikut tanpa disentuh satu per satu.
     Angkanya dipilih dari nilai yang PALING BANYAK sudah dirender
     supaya perpindahannya sekecil mungkin: 6 · 10 · 14 · 18 · 26 · 36.
     Penjaga: web/tests/test_fondasi_visual.py::test_satu_tangga_radius
     ───────────────────────────────────────────────────────────── */
  --r-xs:   6px;
  --r-sm:   10px;
  --r-md:   14px;
  --r-lg:   18px;
  --r-xl:   26px;
  --r-2xl:  36px;
  --r-pill: 999px;

  /* ── SHADOWS ─────────────────────────────────────────────── */
  --sh-sm:   0 1px 2px rgba(15,23,42,0.05);
  --sh-md:   0 4px 18px -4px rgba(15,23,42,0.08), 0 2px 6px -2px rgba(15,23,42,0.04);
  --sh-lg:   0 14px 50px -16px rgba(15,23,42,0.18), 0 6px 18px -10px rgba(15,23,42,0.08);
  --sh-xl:   0 30px 90px -32px rgba(15,23,42,0.28), 0 12px 36px -16px rgba(15,23,42,0.12);
  --sh-glow: 0 0 0 1px rgba(59,130,246,0.18), 0 16px 60px -16px rgba(59,130,246,0.30), 0 0 80px -8px rgba(6,182,212,0.18);

  /* ── MOTION ──────────────────────────────────────────────── */
  --ease-out:    cubic-bezier(0.2, 0.7, 0.25, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --t-fast: 180ms;
  --t-med:  320ms;
  --t-slow: 600ms;

  /* ── QDA v4 · PAKET TOKEN VISUALISASI (BAB 12 §12.5) ───────────
     Nilai TERANG di sini; timpaan gelap ada di SATU blok gelap di
     bawah (§12.4.2). Berkas komponen `qual-*` berpagu NOL warna
     literal — ia MEMAKAI token ini, tidak pernah mendefinisikannya.
     Perender D3/canvas membacanya lewat
     getComputedStyle(document.documentElement).getPropertyValue(…);
     token yang tidak lahir mengembalikan string KOSONG, dan D3 yang
     menerima string kosong menggambar hitam di kedua tema tanpa satu
     pun galat. Itu alasan blok ini ada.

     Nilainya diambil APA ADANYA dari BAB 12 §12.5 — palet itu sudah
     lolos enam pemeriksaan (pita lightness OKLCH, lantai chroma,
     pemisahan CVD protanopia/deuteranopia, lantai penglihatan normal,
     kontras tanda vs permukaan). JANGAN mengarang nilai baru di sini:
     tiap perubahan wajib menjalankan ulang validasi palet dan
     memperbarui berkas bekunya (P-15 / F2-39).
     Diturunkan dari jangkar navy dan aksen cyan Risos — TANPA
     ungu/indigo (aturan brand).
     ───────────────────────────────────────────────────────────── */

  /* Kategorikal — identitas seri (kode, tema, informan). Urutan slot
     TETAP dan tidak pernah diputar ulang: warna mengikuti ENTITAS,
     bukan peringkatnya. Seri ke-9+ dilipat ke "Lainnya", tidak pernah
     lahir warna baru. */
  --qual-viz-cat-1: #1d4ed8;   /* biru navy (brand) */
  --qual-viz-cat-2: #ea580c;   /* oranye */
  --qual-viz-cat-3: #0d9488;   /* teal */
  --qual-viz-cat-4: #d97706;   /* amber */
  --qual-viz-cat-5: #db2777;   /* magenta */
  --qual-viz-cat-6: #15803d;   /* hijau */
  --qual-viz-cat-7: #0891b2;   /* cyan (keluarga aksen) */
  --qual-viz-cat-8: #b91c1c;   /* merah/rose */

  /* ── Sorotan kode di atas TRANSKRIP ────────────────────────────────
     Diturunkan dari ramp kategorikal di atas, dan SENGAJA bukan dari
     `qda_codings_v2.color_hex`. Alasannya bukan selera: color_hex adalah
     heks mentah yang dipilih peneliti di tema mana pun ia kebetulan
     sedang berada, lalu dipakai kembali di tema satunya tanpa satu pun
     pemeriksaan kontras. Ramp kategorikal sudah punya timpaan gelap
     (§12.5), jadi sorotan ikut bertukar sendiri.

     Isian sengaja LEMAH. Yang dibaca di halaman itu kata-kata informan;
     sorotan hanya penunjuk, dan penunjuk yang lebih kuat dari isinya
     membuat transkrip tidak terbaca — persis keberatan yang dulu dipakai
     untuk menolak warna per-kode sama sekali. 16% menjawab keberatan itu
     tanpa membuang keragamannya.

     Garis bawahnya PENUH: kanal pembeda yang tidak bergantung warna
     (WCAG 1.4.1). Sekitar 8% laki-laki tidak bisa memisahkan teal dari
     amber, dan bagi mereka delapan tint lemah adalah satu warna. */
  --qual-kode-isi-1: color-mix(in srgb, var(--qual-viz-cat-1) 16%, transparent);
  --qual-kode-isi-2: color-mix(in srgb, var(--qual-viz-cat-2) 16%, transparent);
  --qual-kode-isi-3: color-mix(in srgb, var(--qual-viz-cat-3) 16%, transparent);
  --qual-kode-isi-4: color-mix(in srgb, var(--qual-viz-cat-4) 16%, transparent);
  --qual-kode-isi-5: color-mix(in srgb, var(--qual-viz-cat-5) 16%, transparent);
  --qual-kode-isi-6: color-mix(in srgb, var(--qual-viz-cat-6) 16%, transparent);
  --qual-kode-isi-7: color-mix(in srgb, var(--qual-viz-cat-7) 16%, transparent);
  --qual-kode-isi-8: color-mix(in srgb, var(--qual-viz-cat-8) 16%, transparent);

  /* Slot ke-9+ dan slot CAMPURAN (satu kalimat membawa dua kode berbeda).
     Netral dengan sengaja: memberi warna ke-9 melanggar aturan ramp di
     atas, dan memilih salah satu dari dua kode yang menumpuk berarti
     halaman berbohong tentang kode mana yang menyorotinya. */
  --qual-kode-isi-lain: color-mix(in srgb, var(--txt-muted) 14%, transparent);
  --qual-kode-garis-lain: var(--txt-faint);

  /* Sekuensial — magnitudo (heatmap kerapatan, frekuensi). Satu rona
     navy, terang→tua; jangkarnya DIBALIK di tema gelap. Sel "tidak ada
     data" BUKAN langkah-1: ia permukaan + garis --qual-viz-grid. */
  --qual-viz-seq-1: #dbeafe;
  --qual-viz-seq-2: #bfdbfe;
  --qual-viz-seq-3: #93c5fd;
  --qual-viz-seq-4: #60a5fa;
  --qual-viz-seq-5: #3b82f6;
  --qual-viz-seq-6: #2563eb;
  --qual-viz-seq-7: #1e40af;

  /* Divergen — polaritas terhadap garis dasar. Dua rona + titik tengah
     abu NETRAL (bukan rona ketiga): kutub sejuk navy, kutub hangat amber. */
  --qual-viz-div-n2: #92400e;
  --qual-viz-div-n1: #d97706;
  --qual-viz-div-0:  #cbd5e1;
  --qual-viz-div-p1: #2563eb;
  --qual-viz-div-p2: #1e3a8a;

  /* Positif/negatif (sentimen, delta antar gelombang). BUKAN slot
     kategorikal dan BUKAN token status UI — --c-success/--c-danger tetap
     milik banner/ikon status supaya status tidak pernah menyamar jadi
     seri. Selalu berpasangan ikon/label, tidak pernah warna sendirian. */
  --qual-viz-pos: #166534;
  --qual-viz-neg: #991b1b;

  /* Netral, kisi, label. Kisi resesif; label memakai token teks —
     teks di visualisasi TIDAK PERNAH memakai warna serinya. */
  --qual-viz-grid:  var(--c-line);
  --qual-viz-label: var(--txt-muted);
  --qual-viz-muted: #94a3b8;   /* seri "Lainnya", data dikecualikan */

  /* Tirai modal — SATU token untuk seluruh permukaan melayang v4.
     Sembilan resep tirai yang beredar di berkas warisan pensiun ke sini
     supaya gelapnya bisa disetel di satu tempat. */
  --qual-scrim: rgba(15, 23, 42, 0.48);

  /* Tangga z-index QDA v4 — sama di kedua tema, jadi ia lahir SEKALI di
     sini dan tidak pernah ditimpa blok tema. Angka telanjang di berkas
     `qual-*` = merah (P-14 asersi 7); pakai token ini. */
  --z-qual-laci:    900;
  --z-qual-modal:   1000;
  --z-qual-tooltip: 1200;
  --z-qual-toast:   1300;
}

/* ══════════════════════════════════════════════════════════════════════
   SATU BLOK GELAP + SATU BLOK TERANG  (F1-24b · A-11)

   Sampai 22 Agu 2026 berkas ini punya TIGA blok gelap yang terpisah —
   permukaan shell app, cermin token warisan --c-*, dan lapis alias
   "unified brief" — ditambah satu blok body.theme-dark yatim. Tiga blok
   untuk satu selektor berarti satu token bisa didefinisikan tiga kali
   dengan tiga nilai berbeda, dan yang menang adalah yang TERAKHIR dibaca:
   persis mekanisme "sudah diperbaiki tapi tidak berubah" yang paling
   sulit didiagnosa, karena grep menemukan perbaikannya sementara peramban
   memakai yang lain. Ketiganya dilebur di sini.

   Selektor warisan body.theme-dark / body.theme-light IKUT DILEBUR, bukan
   dibuang: theme.js masih memasang kelas itu di <body> (theme.js:172-173)
   dan base.html masih merendernya dari sisi server, jadi menghapusnya akan
   mematikan tema untuk komponen warisan yang menempel ke body.

   Konsekuensi yang disengaja: lengan body.* kini ikut membawa token shell
   (--bg/--txt/--surface-*), bukan hanya token warisan --c-*. Di jendela
   sempit sebelum theme.js berjalan — saat kelas <body> dari peladen bisa
   berbeda dari data-theme yang baru disetel localStorage — <body> kini
   konsisten satu tema, bukan campuran gelap-terang seperti sebelumnya.
   Nilai per-palet tetap milik themes.css: ia di-@import SESUDAH berkas ini
   (style.css:4-5) sehingga menang lewat urutan, dan di lengan body ia juga
   lebih khusus (body.theme-dark[data-palette=…]). Blok ini tidak merebutnya.

   Selektornya menargetkan <html data-theme="…"> yang disetel theme.js
   (theme.js:175) — bukan <body>. Penjaga: P-14 asersi 9
   (tools/pindai_hex_qual.py) mengunci "tepat SATU blok gelap di berkas
   token"; P-15 (web/tests/test_qual_kontras.py) menghitung kontras
   pasangan token di KEDUA tema dari berkas ini.
   ══════════════════════════════════════════════════════════════════════ */
:root[data-theme="dark"], body.theme-dark {
  /* ── Permukaan shell app (header, sidebar, kartu dasbor) ───────── */
  /* ⚠️ Nilai ini WAJIB sama persis dengan `--bg` palet samudra di themes.css.
     themes.css:36 menyatakan "base tokens in style.css/tokens.css already
     equal samudra" — dan untuk --bg pernyataan itu TIDAK benar: selisihnya
     1-4 satuan per kanal. Tak terlihat kalau ditaruh berdampingan, tetapi
     inilah warna yang dicat SEBELUM atribut data-palette terpasang. Halaman
     yang tidak pernah mendapat atribut itu (dokumen legal, halaman tanpa JS)
     karena itu berlatar berbeda dari seluruh app — terukur 23 Agu 2026:
     /legal/privacy rgb(10,15,30) sementara 8 rute lain rgb(10,14,26). */
  --bg:        #0A0E1A;
  --bg-elev:   #0F1729;
  --bg-card:   rgba(255,255,255,0.025);
  --bg-input:  rgba(255,255,255,0.04);
  --line:      rgba(255,255,255,0.07);
  --line-2:    rgba(255,255,255,0.14);
  --txt:       #F1F5F9;
  --txt-muted: #94A3B8;
  --txt-faint: #64748B;
  --hairline-grad: linear-gradient(90deg, transparent, rgba(255,255,255,.08) 30%, rgba(255,255,255,.08) 70%, transparent);

  /* ── Token warisan --c-* ────────────────────────────────────────
     Banyak komponen QDA memakai `color: var(--c-text)` /
     `background: var(--c-bg-paper)`; tokennya didefinisikan di :root
     dengan nilai TERANG, jadi tanpa timpaan ini teksnya tak terbaca di
     tema gelap. --c-bg-dark dan --c-bg-deeper SENGAJA tidak ditulis
     ulang di sini: nilainya sama persis dengan :root, dan token yang
     ditulis dua kali dengan nilai sama adalah dua tempat yang bisa
     berbeda diam-diam nanti. */
  --c-text:                #F1F5F9;   /* slate-100 — terbaca di latar gelap */
  --c-text-muted:          #CBD5E1;   /* slate-300 */
  --c-text-soft:           #94A3B8;   /* slate-400 */
  --c-text-faint:          #64748B;   /* slate-500 — batas bawah */
  --c-bg-paper:            #1E293B;   /* slate-800 — permukaan kartu */
  --c-bg-light:            #0F172A;   /* slate-900 — latar halaman */
  --c-bg-cream:            #0F172A;
  --c-line:                #334155;   /* slate-700 */
  --c-line-strong:         #475569;   /* slate-600 */
  --c-app-accent-soft:     rgba(96, 165, 250, 0.18);

  /* ── Lapis alias "unified brief" ────────────────────────────────── */
  --surface-1: #0C1122; --surface-2: #131A2E; --surface-3: #182138;
  --fg: #EAF0FB; --fg-muted: #98A6C0; --fg-faint: #5E6E88; --text-faint: #5E6E88;
  --accent: #38BDF8; --on-accent: #0A0E1A; --accent-2: #22D3EE; --accent-soft: rgba(56,189,248,0.10);
  --shadow: 0 16px 40px -20px rgba(0,0,0,0.70); --hl: inset 0 1px 0 rgba(255,255,255,0.05);

  /* ── WARNA DUNIA (cadangan statis) ───────────────────────────────
     Rel lima dunia (Riset · Pasar · Kelas · Kampus · Saya) diberi warna, dan
     warnanya DITURUNKAN dari tema yang dipilih pengguna — bukan dipatok.
     Penurunannya dikerjakan theme.js (`worldHues()`); nilai di bawah ini hanya
     CADANGAN untuk kunjungan pertama, sebelum satu baris JS pun jalan, supaya
     tidak ada kedip "dunia tanpa warna".

     Angkanya BUKAN pilihan tangan: ia keluaran `worldHues()` untuk aksen palet
     samudra (tema bawaan) di atas permukaan kartu samudra. Penjaga
     web/tests/test_warna_dunia.py menghitung ulang lewat node dan MERAH kalau
     berkas ini menyimpang — jadi ini cadangan yang terikat, bukan salinan yang
     bisa basi diam-diam.

     Yang lembut & garis SENGAJA tidak ditulis sebagai angka: keduanya ATURAN
     (campur permukaan ke warna dunia), jadi ia ditulis sebagai aturan. Efek
     sampingnya bagus — di palet non-samudra, latar & garis dunia sudah ikut
     permukaan palet itu bahkan sebelum JS memasang warnanya. */
  --dunia-riset:  #3DBBF3;
  --dunia-pasar:  #3DF357;
  --dunia-kelas:  #806DF6;
  --dunia-kampus: #F3573D;
  --dunia-saya:   #F33DD9;
  --dunia-riset-lembut:  color-mix(in srgb, var(--dunia-riset) 20%, var(--surface-1));
  --dunia-pasar-lembut:  color-mix(in srgb, var(--dunia-pasar) 20%, var(--surface-1));
  --dunia-kelas-lembut:  color-mix(in srgb, var(--dunia-kelas) 20%, var(--surface-1));
  --dunia-kampus-lembut: color-mix(in srgb, var(--dunia-kampus) 20%, var(--surface-1));
  --dunia-saya-lembut:   color-mix(in srgb, var(--dunia-saya) 20%, var(--surface-1));
  --dunia-riset-garis:   color-mix(in srgb, var(--dunia-riset) 38%, var(--surface-1));
  --dunia-pasar-garis:   color-mix(in srgb, var(--dunia-pasar) 38%, var(--surface-1));
  --dunia-kelas-garis:   color-mix(in srgb, var(--dunia-kelas) 38%, var(--surface-1));
  --dunia-kampus-garis:  color-mix(in srgb, var(--dunia-kampus) 38%, var(--surface-1));
  --dunia-saya-garis:    color-mix(in srgb, var(--dunia-saya) 38%, var(--surface-1));

  /* ── QDA v4 · timpaan gelap paket visualisasi (BAB 12 §12.5) ─────
     HANYA token yang nilainya BERBEDA dari blok terang yang ditulis di
     sini. --qual-viz-cat-2/-cat-3/-cat-4/-cat-7 dan --qual-viz-div-n1
     sama di kedua tema (§12.5.1, §12.5.3) — menuliskannya lagi cuma
     melahirkan tempat kedua yang bisa menyimpang. --qual-viz-label
     memakai var(--txt-muted) yang sudah bertukar di blok ini, jadi ia
     pun tidak perlu ditimpa.
     Permukaan acuan validasi gelap = --bg-elev di blok ini. */
  --qual-viz-cat-1: #3b82f6;
  --qual-viz-cat-5: #ec4899;
  --qual-viz-cat-6: #16a34a;
  --qual-viz-cat-8: #f43f5e;

  /* Ramp sekuensial DIBALIK: di tema gelap, ekstrem = paling terang. */
  --qual-viz-seq-1: #1e3a8a;
  --qual-viz-seq-2: #1e40af;
  --qual-viz-seq-3: #2563eb;
  --qual-viz-seq-4: #3b82f6;
  --qual-viz-seq-5: #60a5fa;
  --qual-viz-seq-6: #93c5fd;
  --qual-viz-seq-7: #bfdbfe;

  --qual-viz-div-n2: #fbbf24;
  --qual-viz-div-0:  #475569;
  --qual-viz-div-p1: #3b82f6;
  --qual-viz-div-p2: #93c5fd;

  --qual-viz-pos: #4ade80;
  --qual-viz-neg: #f87171;

  --qual-viz-grid:  var(--line);
  --qual-viz-muted: #64748b;

  --qual-scrim: rgba(2, 6, 23, 0.66);
}

:root[data-theme="light"], body.theme-light {
  /* ── Permukaan shell app ────────────────────────────────────────── */
  /* Sama alasannya dengan blok gelap: nilai ini = `--bg` samudra terang,
     bukan angka ketiga. Pasangan rgb(246,247,251) vs rgb(255,255,255) yang
     dicatat T10 rencana induk lahir dari perbedaan itu. */
  --bg:        #D6E6FB;
  --bg-elev:   #FFFFFF;
  --bg-card:   #FFFFFF;
  --bg-input:  #FFFFFF;
  --line:      #DDE3EC;
  --line-2:    #C2CCDA;
  /* Kontras lebih kuat di mode terang — dinaikkan dari slate-900/600/400
     ke slate-950/700/500 supaya terbaca di permukaan putih. */
  --txt:       #020617;
  --txt-muted: #334155;
  --txt-faint: #64748B;
  --hairline-grad: linear-gradient(90deg, transparent, rgba(15,23,42,.10) 30%, rgba(15,23,42,.10) 70%, transparent);

  /* ── Lapis alias "unified brief" ─────────────────────────────────
     --on-accent DITULIS EKSPLISIT (12 Agu 2026). Sebelumnya hanya blok
     gelap yang mendefinisikannya, jadi di mode terang token ini TIDAK ADA
     dan seluruh app bergantung pada fallback `var(--on-accent, putih)` di
     82 tempat. Nilainya kebetulan benar (putih murni di atas aksen terang
     = 4,77:1), tapi satu pemakaian baru dengan fallback lain akan
     diam-diam salah warna. Nilai ini TIDAK mengubah tampilan apa pun — ia
     cuma menghentikan token yang hilang.
     Nilai terang paket --qual-viz-* TIDAK diulang di sini: rumahnya blok
     :root dasar di atas (§12.5), dan mengulangnya = palet kedua. */
  --surface-1: #FFFFFF; --surface-2: #FFFFFF; --surface-3: #F7F9FD;
  --fg: #0F1A2E; --fg-muted: #51607A; --fg-faint: #8593AB; --text-faint: #8593AB;
  --accent: #1D6FE0; --on-accent: #FFFFFF; --accent-2: #0891B2; --accent-soft: rgba(29,111,224,0.07);
  --shadow: 0 14px 38px -22px rgba(15,30,60,0.28), 0 4px 12px -8px rgba(15,30,60,0.10); --hl: none;

  /* ── WARNA DUNIA (cadangan statis) ───────────────────────────────
     Rel lima dunia (Riset · Pasar · Kelas · Kampus · Saya) diberi warna, dan
     warnanya DITURUNKAN dari tema yang dipilih pengguna — bukan dipatok.
     Penurunannya dikerjakan theme.js (`worldHues()`); nilai di bawah ini hanya
     CADANGAN untuk kunjungan pertama, sebelum satu baris JS pun jalan, supaya
     tidak ada kedip "dunia tanpa warna".

     Angkanya BUKAN pilihan tangan: ia keluaran `worldHues()` untuk aksen palet
     samudra (tema bawaan) di atas permukaan kartu samudra. Penjaga
     web/tests/test_warna_dunia.py menghitung ulang lewat node dan MERAH kalau
     berkas ini menyimpang — jadi ini cadangan yang terikat, bukan salinan yang
     bisa basi diam-diam.

     Yang lembut & garis SENGAJA tidak ditulis sebagai angka: keduanya ATURAN
     (campur permukaan ke warna dunia), jadi ia ditulis sebagai aturan. Efek
     sampingnya bagus — di palet non-samudra, latar & garis dunia sudah ikut
     permukaan palet itu bahkan sebelum JS memasang warnanya. */
  --dunia-riset:  #1B66CE;
  --dunia-pasar:  #107D3D;
  --dunia-kelas:  #6D1DE0;
  --dunia-kampus: #AA5316;
  --dunia-saya:   #C5197E;
  --dunia-riset-lembut:  color-mix(in srgb, var(--dunia-riset) 12%, var(--surface-1));
  --dunia-pasar-lembut:  color-mix(in srgb, var(--dunia-pasar) 12%, var(--surface-1));
  --dunia-kelas-lembut:  color-mix(in srgb, var(--dunia-kelas) 12%, var(--surface-1));
  --dunia-kampus-lembut: color-mix(in srgb, var(--dunia-kampus) 12%, var(--surface-1));
  --dunia-saya-lembut:   color-mix(in srgb, var(--dunia-saya) 12%, var(--surface-1));
  --dunia-riset-garis:   color-mix(in srgb, var(--dunia-riset) 30%, var(--surface-1));
  --dunia-pasar-garis:   color-mix(in srgb, var(--dunia-pasar) 30%, var(--surface-1));
  --dunia-kelas-garis:   color-mix(in srgb, var(--dunia-kelas) 30%, var(--surface-1));
  --dunia-kampus-garis:  color-mix(in srgb, var(--dunia-kampus) 30%, var(--surface-1));
  --dunia-saya-garis:    color-mix(in srgb, var(--dunia-saya) 30%, var(--surface-1));
}

/* ══════════════════════════════════════════════════════════════════════
   UNIFIED BRIEF TOKENS (alias layer) — names from
   design_handoff_full_redesign/tokens.css. Mapped onto the existing
   cyan-blue system so templates can use the brief's vocabulary
   (var(--grad), var(--surface-2), var(--fg), var(--ok)…). Single source.
   ══════════════════════════════════════════════════════════════════════ */
:root {
  --grad:       linear-gradient(135deg, #3B82F6 0%, #06B6D4 100%);
  --grad-text-unified: linear-gradient(120deg, #3B82F6 0%, #06B6D4 60%, #22D3EE 100%);
  --logo-indigo-1: #4f46e5;   /* logo mark only */
  --logo-indigo-2: #1e1b4b;   /* logo mark only */
  --ok:   #22C55E;  --ok-soft:   rgba(34,197,94,0.14);
  --warn: #EAB308;  --warn-soft: rgba(234,179,8,0.14);
  --bad:  #EF4444;  --bad-soft:  rgba(239,68,68,0.14);
  /* Radius TIDAK diulang di sini. Blok ini dulu mendeklarasikan ulang
     --r-sm/--r-md/--r-lg/--r-pill dengan angka BERBEDA dari :102, dan karena
     ia dibaca belakangan ia diam-diam menang. Satu tangga sekarang hidup di
     satu tempat saja (:102). */
}

/* ══════════════════════════════════════════════════════════════════════
   DUNIA AKTIF → SATU token yang boleh dipakai ISI halaman  (FASE 5)

   Rio, 23 Agu 2026: *"kenapa saya merasa perbedaannya hanya pada sidebar
   saja?"* — dan itu benar secara harfiah, bukan sebagai kesan. Terukur:
   token `--dunia-*` dipakai di TIGA berkas saja, dan seluruh pemakaiannya
   di luar tokens.css adalah EMPAT deklarasi di shell.css (butir rel) —
   keempatnya melukis titik 62px di dalam rel. Warna dunia sudah lahir,
   sudah lolos uji kontrasnya, lalu berhenti di tepi kiri layar.

   Yang menghalangi bukan warnanya, melainkan tidak adanya CARA bagi isi
   halaman untuk bertanya "aku sedang di dunia mana?". Dunia aktif hanya
   tertulis sebagai kelas pada satu tautan di dalam rel
   (`.rel__d--riset.is-on`), dan tidak ada atribut apa pun di <html>/<body>
   yang menyatakannya.

   `:has()` menjawabnya TANPA satu baris JS dan tanpa menyentuh base.html:
   body membaca rel-nya sendiri, lalu menerbitkan satu token generik yang
   boleh dipakai berkas mana pun. Kalau JS-nya mati, warnanya tetap benar;
   kalau halaman tidak bercangkang (tanpa rel), ia jatuh ke aksen tema —
   bukan ke warna dunia yang salah.

   ⚠️ Dunia KELIMA "Saya" DULU memakai `var(--accent)` mentah, dengan
   alasan "setelan akun bukan tempat kerja, ia tidak butuh identitas
   dunia". Alasan itu gugur begitu diukur: dunia Riset digeser 0°, jadi
   `--accent` BUKAN warna netral — ia PERSIS rona Riset. Jarak Saya↔Riset
   terukur 0,0° di seluruh 12 tema × mode, dan di 5 di antaranya heksnya
   identik byte per byte (grafit gelap, hutan gelap, mawar terang,
   senja terang — heksnya sengaja tidak disalin ke sini; ia turunan tema,
   dan angka turunan yang disalin ke berkas lain basi diam-diam). Dua
   butir rel berwarna
   sama persis adalah cacat yang sama dengan yang melahirkan seluruh
   mesin ini — hanya saja kali ini tabrakannya di dalam sistem sendiri,
   bukan antara palet dan warna patokan. Tambahan: `--accent` tidak pernah
   melewati pencarian terang di theme.js, jadi Saya menembus WCAG AA di
   palet bawaan (samudra terang 4,19:1 — di bawah 4,5:1).

   Sekarang Saya punya sudutnya sendiri, +110°, dan mesinnya tidak
   dibongkar: sudut itu hasil sapuan 95°–260° yang mengukur jarak rona
   TERKECIL antar semua pasang dari LIMA dunia di 12 tema × mode. Dua
   dataran tertinggi (100°–120° dan 225°–240°) sama-sama 49,6°, artinya
   dunia kelima tidak menyempitkan jarak yang sudah ada sedikit pun —
   yang mengikat tetap Riset↔Kelas. 110° dipilih karena jurangnya paling
   jauh di kedua sisi; dataran satunya menempel tebing (turun 5° dari
   225° dan jaraknya terjun ke 20,1° karena Saya menabrak Kampus).
   Terukur untuk lima dunia: jarak rona min 49,6° · kontras min 4,50:1.
   ══════════════════════════════════════════════════════════════════════ */
:root {
  /* Cadangan untuk halaman tanpa rel (mode fokus, dokumen mandiri, tamu). */
  --dunia:        var(--accent);
  --dunia-lembut: transparent;
  --dunia-garis:  var(--line-2, var(--line));
}
body:has(.rel__d--riset.is-on) {
  --dunia:        var(--dunia-riset);
  --dunia-lembut: var(--dunia-riset-lembut);
  --dunia-garis:  var(--dunia-riset-garis);
}
body:has(.rel__d--pasar.is-on) {
  --dunia:        var(--dunia-pasar);
  --dunia-lembut: var(--dunia-pasar-lembut);
  --dunia-garis:  var(--dunia-pasar-garis);
}
body:has(.rel__d--kelas.is-on) {
  --dunia:        var(--dunia-kelas);
  --dunia-lembut: var(--dunia-kelas-lembut);
  --dunia-garis:  var(--dunia-kelas-garis);
}
body:has(.rel__d--kampus.is-on) {
  --dunia:        var(--dunia-kampus);
  --dunia-lembut: var(--dunia-kampus-lembut);
  --dunia-garis:  var(--dunia-kampus-garis);
}
body:has(.rel__d--saya.is-on) {
  --dunia:        var(--dunia-saya);
  --dunia-lembut: var(--dunia-saya-lembut);
  --dunia-garis:  var(--dunia-saya-garis);
}

/* Inline SVG icon sizing — global (icons replace emoji across all shells) */
.ic { width: 1.1em; height: 1.1em; display: inline-block; vertical-align: -0.18em; flex-shrink: 0; }
.ic-lg { width: 1.4em; height: 1.4em; }
.ic-sm { width: 0.95em; height: 0.95em; }
