#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Generator carousel Threads @ukyhandoko — gaya template Bos "White & Black Simple Carousel".

Aturan (Bos 6 Okt 2026):
  - "2 hari sekali aja carousel" · "10 slides per carousel" (maks) · "disitu tertulis 8 kebiasaan.
    ya generate per halamannya" · "ganti jangan angka 8 semua .. ganti random, mulai 5 hingga 10.
    lalu tiap halaman menjelaskan 1 per satu. 1. apa, 2. apa hingga 10"
  - Jadi: **jumlah poin tiap carousel BERBEDA, di antara 5-10** (dict `JUMLAH`), halaman pertama = JUDUL,
    lalu satu halaman per poin bernomor urut (01..N), halaman terakhir = ajakan.
    Total slide = 2 + N (maks 12).

Struktur data tiap seri (dict `SERI`):
  jumlah : N (5-10)
  judul  : daftar baris cover; `{n}` diganti jumlah
  items  : daftar (baris_judul, body) — satu halaman per item, diambil N pertama
  tutup  : daftar baris halaman penutup
  caption: {"pembuka", "cta", "tag"} -> caption Threads dirakit `siapkan_caption_uky.py`

Token desain: latar #F2F2F2 + bentuk putih #FFFFFF; teks hitam #000000; headline KAPITAL kontras berat
(Poppins-Light vs Poppins-Bold, satu ukuran per slide); body miring (shear); nomor halaman raksasa putih
kanan-bawah; kiri atas @ukyhandoko; kanan atas #2026; kiri bawah www.studiosoft.id; kanan bawah SWIPE
(slide terakhir: ikon bookmark).

Pakai: python3 gen_uky_carousel.py [--seri a]
"""
import os, sys
from PIL import Image, ImageDraw, ImageFont

W, H = 1080, 1350
BG = (242, 242, 242)
PUTIH = (255, 255, 255)
HITAM = (0, 0, 0)
M = 110
NAMA = "@ukyhandoko"
LABEL = "#2026"
SITUS = "www.studiosoft.id"

FP = "/home/mantapman/.fonts/Poppins-%s.ttf"
F_LIGHT, F_REG, F_BOLD = FP % "Light", FP % "Regular", FP % "Bold"

SERI = {
# ── A: kebiasaan bisnis kecil ──
"a": {"jumlah": 6,
 "judul": ["BISNIS KECIL", "TUMBUH KARENA", "{n} KEBIASAAN INI"],
 "tutup": ["JANGAN LUPA", "SIMPAN", "POST INI"],
 "caption": {"pembuka": "Bisnis kecil tumbuh bukan karena iklan besar, tapi karena kebiasaan yang dijalankan terus.",
             "cta": "Dari daftar ini, mana yang paling sering kamu lewatkan? Tulis di komentar 👇",
             "tag": "#bisnisdigital #umkm #personalbranding #studiosoft"},
 "items": [
  (["JUALAN", "PAKAI CERITA"], "Orang membeli karena percaya. Ceritakan klienmu, bukan daftar fitur."),
  (["CATAT", "ANGKANYA"], "Berapa yang datang, berapa yang jadi. Tanpa angka, keputusan cuma perasaan."),
  (["TAMPIL", "TERATUR"], "Nama, tawaran, dan cara menghubungi. Tiga hal itu harus jelas di mana pun orang mencarimu."),
  (["PILIH", "SATU KANAL"], "Satu kanal yang digarap serius lebih baik daripada lima yang setengah jalan."),
  (["MINTA", "UMPAN BALIK"], "Tanya apa yang membingungkan. Jawabannya biasanya bukan yang kita duga."),
  (["BALAS", "CEPAT"], "Yang pertama menjawab biasanya yang dapat. Bukan karena paling murah, tapi paling sigap."),
  (["HITUNG HARGA", "JANGAN NEBAK"], "Hitung waktu, bahan, dan sisihkan untuk perbaikan. Harga yang salah bikin capek sendiri."),
  (["RAWAT", "PELANGGAN LAMA"], "Pelanggan lama jauh lebih murah daripada pelanggan baru. Jangan cuma sibuk mencari."),
 ]},
# ── B: foto jualan ──
"b": {"jumlah": 9,
 "judul": ["FOTO JUALAN", "DIPERCAYA KARENA", "{n} HAL INI"],
 "tutup": ["SIMPAN UNTUK", "KAMU PAKAI", "NANTI"],
 "caption": {"pembuka": "Bisnis kecil punya satu keunggulan yang tidak dimiliki toko besar: bisa menunjukkan orangnya.",
             "cta": "Bagian mana yang paling sering kamu lewatkan waktu memotret produk? Tulis di komentar 👇",
             "tag": "#fotoproduk #umkm #fotografi #studiosoft"},
 "items": [
  (["PAKAI", "CAHAYA ALAMI"], "Dekat jendela, jangan pakai flash keras. Cahaya lembut bikin warna terlihat apa adanya."),
  (["LATAR", "BERSIH"], "Kosongkan meja. Latar yang ramai bikin mata bingung dan produk jadi kabur."),
  (["JANGAN", "ZOOM DIGITAL"], "Dekati barangnya, jangan dipaksa perbesar. Memperbesar gambar cuma membesarkan piksel."),
  (["TUNJUKKAN", "UKURANNYA"], "Taruh benda yang dikenal sebagai pembanding, misalnya pena atau tangan."),
  (["FOTO", "PROSESNYA"], "Orang percaya kalau melihat cara kerjanya, bukan cuma hasil akhirnya."),
  (["MUNCULKAN", "ORANGNYA"], "Wajah dan tangan bikin produk terasa nyata. Bisnis kecil punya ini, toko besar tidak."),
  (["KONSISTEN", "WARNA"], "Semua foto pakai cahaya dan latar yang sama supaya feed terlihat rapi."),
  (["JANGAN", "OVER-EDIT"], "Warna yang terlalu cantik bikin kecewa saat barangnya datang."),
  (["AMBIL", "BEBERAPA SUDUT"], "Depan, samping, dan detail. Pembeli ingin tahu bentuk aslinya, bukan cuma tampak depannya."),
 ]},
# ── C: pekerjaan yang bisa dibantu AI ──
"c": {"jumlah": 10,
 "judul": ["{n} PEKERJAAN", "YANG BISA", "DIBANTU AI"],
 "tutup": ["MULAI DARI", "SATU TUGAS", "DULU"],
 "caption": {"pembuka": "AI paling berguna kalau dipakai untuk pekerjaan yang berulang dan menyita jam kerja.",
             "cta": "Dari daftar ini, mana yang ingin kamu coba dulu? Tulis di komentar 👇",
             "tag": "#ai #produktivitas #bisnisdigital #studiosoft"},
 "items": [
  (["DRAFT", "CAPTION"], "Dapat kerangka cepat, tapi nada dan isinya tetap kamu yang memutuskan."),
  (["RISET", "KATA KUNCI"], "Bagus untuk gambaran awal. Verifikasi jumlah pencarian tetap dilakukan manual."),
  (["RINGKAS", "LAPORAN"], "Laporan panjang jadi poin-poin. Angka aslinya tetap harus dicek sendiri."),
  (["SUSUN JADWAL", "KONTEN"], "Kerangka kalender cepat jadi. Rasa dan pengalaman tetap datang dari kamu."),
  (["JAWAB", "PERTANYAAN BERULANG"], "Sama tapi cepat. Pastikan nadanya tetap terdengar seperti manusia."),
  (["CEK EJAAN", "DAN NADA"], "Bukan pengganti editor, tapi menghemat beberapa putaran revisi."),
  (["OLAH DATA", "SEDERHANA"], "Urutkan, kelompokkan, cari pola. Bukan untuk keputusan besar tanpa dicek."),
  (["IDE DAN", "VARIASI"], "Banyak pilihan dalam waktu singkat. Yang memilih tetap kamu."),
  (["DRAFT BALASAN", "ULASAN"], "Pelanggan dapat jawaban lebih cepat, kamu tinggal menyunting nadanya."),
  (["RINGKAS HAL", "RUMIT"], "Penjelasan panjang diubah jadi bahasa sederhana yang bisa kamu pelajari sendiri."),
 ]},
# ── D: tanda website perlu dibenahi ──
"d": {"jumlah": 5,
 "judul": ["{n} TANDA", "WEBSITE BISNIS", "BUTUH DIBENAHI"],
 "tutup": ["SIMPAN", "CHECKLIST", "INI"],
 "caption": {"pembuka": "Kalau beberapa tanda ini muncul di websitemu, tamu yang datang kemungkinan besar pergi tanpa menghubungi.",
             "cta": "Dari daftar ini, mana yang ada di websitemu? Tulis di komentar 👇",
             "tag": "#website #umkm #bisnisdigital #studiosoft"},
 "items": [
  (["LAMBAT", "DIBUKA"], "Lebih dari tiga detik, separuh pengunjung pergi sebelum sempat melihat tawaranmu."),
  (["TIDAK NYAMAN", "DI HP"], "Lebih dari separuh orang membuka dari HP. Kalau harus di-zoom, mereka menutupnya."),
  (["TIDAK JELAS", "JUALAN APA"], "Pengunjung harus paham tawaranmu dalam lima detik pertama, tanpa menggulir jauh."),
  (["SULIT", "DIHUBUNGI"], "Tombol WhatsApp atau telepon harus terlihat, bukan disembunyikan di bagian paling bawah."),
  (["TIDAK ADA", "BUKTI"], "Testimoni, portofolio, atau foto hasil kerja. Tanpa itu, klaimmu cuma klaim."),
  (["ARTIKEL", "TIDAK TERAWAT"], "Halaman yang tidak pernah diperbarui bikin mesin pencari malas berkunjung."),
  (["ALAMATNYA", "PANJANG"], "Alamat yang rapi mudah diingat dan mudah dibagikan lewat pesan."),
  (["TIDAK", "TERUKUR"], "Tanpa data, tidak ada yang tahu halaman mana yang benar-benar bekerja."),
 ]},
# ── E: konten sosmed rapi ──
"e": {"jumlah": 7,
 "judul": ["{n} HAL YANG BIKIN", "KONTEN SOSMED", "BISNIS RAPI"],
 "tutup": ["SIMPAN UNTUK", "KAMU PAKAI", "NANTI"],
 "caption": {"pembuka": "Konten sosmed bisnis tidak harus ramai, tapi harus rapi. Ini hal-hal kecil yang paling menentukan.",
             "cta": "Dari daftar ini, mana yang paling sering kamu lewatkan? Tulis di komentar 👇",
             "tag": "#kontensosmed #umkm #bisnisdigital #studiosoft"},
 "items": [
  (["SATU GAGASAN", "PER POST"], "Konten yang bicara semua hal tidak diingat siapa pun. Satu post, satu pesan."),
  (["WARNA DAN FONT", "KONSISTEN"], "Supaya orang tahu itu kamu, bahkan sebelum membaca nama akunnya."),
  (["JADWAL YANG", "REALISTIS"], "Tiga kali seminggu yang jalan lebih baik daripada tiap hari yang berhenti di minggu kedua."),
  (["KALIMAT", "PENDEK"], "Satu gagasan per kalimat. Layar kecil tidak sabar membaca kalimat panjang."),
  (["JUDUL YANG", "JELAS"], "Bukan clickbait, tapi yang menjanjikan isi lalu ditepati."),
  (["FOTO YANG", "LAYAK"], "Foto gelap dan miring bikin pesan yang benar terlihat asal-asalan."),
  (["SELALU ADA", "AJAKAN"], "Setiap post menutup dengan satu langkah berikutnya: tulis, simpan, atau hubungi."),
  (["SIMPAN DAN", "ULANG"], "Konten bagus layak tayang lagi dengan sudut baru. Tidak semua harus selalu baru."),
 ]},
# ── F: pertanyaan sebelum bayar jasa digital marketing ──
"f": {"jumlah": 8,
 "judul": ["{n} PERTANYAAN", "SEBELUM BAYAR JASA", "DIGITAL MARKETING"],
 "tutup": ["TANYAKAN", "SEBELUM", "TANDA TANGAN"],
 "caption": {"pembuka": "Menanyakan hal-hal ini sebelum bayar bukan berarti tidak percaya. Justru itu cara menjaga kerja sama tetap sehat.",
             "cta": "Pertanyaan mana yang paling sering terlupa? Tulis di komentar 👇",
             "tag": "#digitalmarketing #umkm #bisnisdigital #studiosoft"},
 "items": [
  (["APA YANG", "DIKERJAKAN"], "Minta rincian pekerjaan bulanan. Bukan janji hasil, tapi daftar kerja nyata."),
  (["SIAPA YANG", "MENGERJAKAN"], "Tahu siapa timnya, bukan cuma nama agensinya. Kalau perlu, tanya nama orangnya."),
  (["LAPORANNYA", "APA"], "Metrik apa yang dilaporkan dan seberapa sering. Tanpa laporan, kamu bayar dalam gelap."),
  (["AKUN MILIK", "SIAPA"], "Semua akses harus atas nama bisnismu sendiri. Ini soal aman atau tidak."),
  (["BERAPA LAMA", "HASILNYA"], "SEO butuh waktu. Kalau dijanjikan instan, itu tanda harus dicurigai."),
  (["BAGAIMANA KALAU", "BERHENTI"], "Apa yang kamu bawa pergi: data, konten, akses, dan dokumentasinya."),
  (["GARANSI ATAU", "JANJI"], "Janji peringkat tertentu biasanya tanda bahaya, bukan layanan bagus."),
  (["BIAYA", "TERSEMBUNYI"], "Tanya biaya iklan, produksi, dan alat tambahan. Semua yang tidak disebut di awal."),
 ]},
# ── G: langkah mulai usaha online ──
"g": {"jumlah": 10,
 "judul": ["{n} LANGKAH MULAI", "USAHA ONLINE", "DARI NOL"],
 "tutup": ["MULAI DARI", "LANGKAH", "PERTAMA"],
 "caption": {"pembuka": "Mulai usaha online dari nol tidak perlu langsung sempurna. Yang penting urutannya benar.",
             "cta": "Kamu sedang di langkah berapa sekarang? Tulis di komentar 👇",
             "tag": "#umkm #bisnisonline #mulaiusaha #studiosoft"},
 "items": [
  (["PILIH SATU", "MASALAH"], "Jual penyelesaian, bukan sekadar produk. Orang membayar untuk masalah yang hilang."),
  (["TENTUKAN", "PEMBELI PERTAMA"], "Sebut nama orangnya, jangan semua orang. Kalau semua orang, tidak ada yang merasa disapa."),
  (["PUNYA SATU", "HALAMAN"], "Cukup satu halaman yang menjelaskan tawaran dan cara menghubungimu."),
  (["KASIH", "BUKTI AWAL"], "Kerjakan satu proyek kecil, lalu minta izin memamerkannya."),
  (["MULAI DARI KANAL", "YANG KAMU KUASAI"], "Jangan langsung sebar ke mana-mana. Kuasai satu tempat dulu."),
  (["HITUNG HARGA", "DENGAN JUJUR"], "Jangan lupa waktu yang kamu pakai di luar produksi: menjawab, merevisi, menagih."),
  (["BUAT CATATAN", "SEDERHANA"], "Siapa datang, dari mana, jadi atau tidak. Tiga kolom saja cukup."),
  (["ULANGI", "YANG BEKERJA"], "Perbesar yang menghasilkan, hentikan yang hanya menghabiskan waktu."),
  (["MINTA TESTIMONI", "SEGERA"], "Minta saat pelanggan masih puas, bukan tiga bulan kemudian."),
  (["NAIKKAN HARGA", "SETELAH TERBUKTI"], "Harga naik setelah hasil terbukti. Bukan karena ingin cepat kaya."),
 ]},
# ── H: mengubah pengikut jadi pelanggan ──
"h": {"jumlah": 6,
 "judul": ["{n} CARA MENGUBAH", "PENGIKUT JADI", "PELANGGAN"],
 "tutup": ["SIMPAN UNTUK", "KAMU PAKAI", "NANTI"],
 "caption": {"pembuka": "Pengikut banyak tidak otomatis jadi pelanggan. Yang menjembatani cuma beberapa kebiasaan kecil.",
             "cta": "Dari daftar ini, mana yang belum kamu lakukan? Tulis di komentar 👇",
             "tag": "#personalbranding #umkm #bisnisdigital #studiosoft"},
 "items": [
  (["KENALI", "DULU"], "Baca komentar dan pesan masuk. Dari situ kelihatan masalah yang paling sering muncul."),
  (["JAWAB DI", "TEMPAT TERBUKA"], "Jawab pertanyaan umum di kolom komentar, bukan cuma lewat pesan pribadi."),
  (["KASIH SATU", "LANGKAH KECIL"], "Bukan langsung menawarkan paket. Beri satu langkah yang bisa dicoba hari itu."),
  (["TUNJUKKAN", "PROSES"], "Ceritakan cara kerjamu, bukan cuma hasil akhirnya. Orang percaya pada proses yang jelas."),
  (["SIMPAN", "BUKTI"], "Testimoni dan foto sebelum-sesudah bikin orang berani menghubungi lebih dulu."),
  (["INGATKAN,", "JANGAN MEMAKSA"], "Ingatkan tawaran sesekali. Yang belum siap hari ini bisa jadi siap bulan depan."),
 ]},
# ── I: yang saya lakukan saat ketemu klien baru ──
"i": {"jumlah": 5,
 "judul": ["{n} HAL YANG SAYA LAKUKAN", "SAAT KETEMU", "KLIEN BARU"],
 "tutup": ["MULAI DARI", "MENDENGAR", "DULU"],
 "caption": {"pembuka": "Pertemuan pertama menentukan arah seluruh proyek. Ini urutan yang saya pakai, sudah bertahun-tahun.",
             "cta": "Bagian mana yang menurutmu paling menentukan? Tulis di komentar 👇",
             "tag": "#bisnisdigital #agensi #personalbranding #studiosoft"},
 "items": [
  (["DENGAR", "DULU"], "Empat puluh menit pertama cuma bertanya. Belum bicara solusi, apalagi harga."),
  (["ULANG MASALAHNYA", "DENGAN BAHASA SAYA"], "Saya tulis ulang masalahnya, lalu minta dikoreksi. Kalau salah, lebih baik ketahuan sekarang."),
  (["SEBUT APA YANG", "TIDAK SAYA KERJAKAN"], "Batasan yang jelas lebih menenangkan daripada janji yang luas."),
  (["TENTUKAN", "UKURAN BERHASIL"], "Angka atau kejadian apa yang bikin proyek dianggap berhasil. Harus disepakati di awal."),
  (["KIRIM RINGKASAN", "TERTULIS"], "Satu halaman: masalah, rencana, tenggat, dan siapa mengerjakan apa."),
 ]},
# ── J: kebiasaan kerja bikin proyek selesai tepat waktu ──
"j": {"jumlah": 9,
 "judul": ["{n} KEBIASAAN KERJA YANG BIKIN", "PROYEK SELESAI", "TEPAT WAKTU"],
 "tutup": ["SIMPAN", "CHECKLIST", "INI"],
 "caption": {"pembuka": "Proyek terlambat jarang karena kurang niat. Biasanya karena tidak dipecah dan tidak dijadwalkan.",
             "cta": "Dari daftar ini, mana yang paling sering kamu lewatkan? Tulis di komentar 👇",
             "tag": "#produktivitas #kerjafokus #bisnisdigital #studiosoft"},
 "items": [
  (["PECAH JADI", "BAGIAN KECIL"], "Proyek besar terasa berat karena tidak dipecah. Potong sampai ukurannya bisa diselesaikan sehari."),
  (["TANGGAL UNTUK", "TIAP BAGIAN"], "Bukan cuma tanggal akhir. Tanggal akhir tanpa tonggak di tengah hampir selalu molor."),
  (["KERJAKAN YANG", "PALING BERAT PAGI"], "Energi terbaik dipakai untuk bagian tersulit, bukan untuk membalas pesan."),
  (["MATIKAN", "NOTIFIKASI"], "Satu jam fokus lebih berharga daripada tiga jam sambil mengecek layar."),
  (["SIMPAN VERSI,", "JANGAN TUNGGU SEMPURNA"], "Versi kasar yang dikirim lebih cepat dikoreksi daripada versi sempurna yang telat."),
  (["MINTA KOREKSI", "DI TENGAH JALAN"], "Koreksi di tengah jauh lebih murah daripada bongkar di akhir."),
  (["CATAT YANG", "MENGHAMBAT"], "Hambatan yang dicatat tidak terulang di proyek berikutnya."),
  (["SISAKAN WAKTU", "UNTUK REVISI"], "Revisi bukan tanda gagal. Itu bagian dari pekerjaan, jadi harus ada waktunya."),
  (["TUTUP DENGAN", "SERAH TERIMA JELAS"], "Kumpulkan hasil di satu tempat dan kirim ringkasannya. Proyek dianggap selesai kalau ini rapi."),
 ]},
# ── K: yang bikin harga jasa susah naik ──
"k": {"jumlah": 7,
 "judul": ["{n} HAL YANG BIKIN", "HARGA JASA", "SUSAH NAIK"],
 "tutup": ["HITUNG ULANG,", "LALU NAIKKAN", "PELAN-PELAN"],
 "caption": {"pembuka": "Harga jasa susah naik bukan karena pasar tidak mau bayar. Biasanya karena ada yang belum disiapkan.",
             "cta": "Dari daftar ini, mana yang sedang kamu alami? Tulis di komentar 👇",
             "tag": "#hargajasa #bisnisdigital #umkm #studiosoft"},
 "items": [
  (["TIDAK ADA", "BUKTI HASIL"], "Tanpa catatan hasil, harga selalu diperdebatkan karena tidak ada yang bisa ditunjuk."),
  (["MENGERJAKAN", "SEMUA PERMINTAAN"], "Terlihat bisa apa saja, tapi tidak diingat karena satu hal. Pilih satu hal yang paling kuat."),
  (["BERSANG", "DI HARGA"], "Kalau yang dijual cuma murah, akan selalu ada yang lebih murah."),
  (["TIDAK MENGHITUNG", "WAKTU TOTAL"], "Waktu menjawab, rapat, dan merevisi sering tidak dihitung padahal ikut termakan."),
  (["HASIL TIDAK", "DIUKUR"], "Kalau klien tidak tahu apa yang berubah, sulit mengharapkannya membayar lebih."),
  (["CEPAT MENURUNKAN", "HARGA"], "Diskon pertama jadi patokan seterusnya. Harga yang sudah turun susah dinaikkan kembali."),
  (["TIDAK PERNAH MENAIKKAN", "UNTUK KLIEN BARU"], "Harga baru berlaku untuk klien baru, sementara klien lama tetap dihormati kesepakatannya."),
 ]},
}


def _baris(daftar, bold_last=True):
    return [(t, "bold" if (bold_last and i == len(daftar) - 1) else "light") for i, t in enumerate(daftar)]


def deck_dari(nama):
    info = SERI[nama]
    n = info["jumlah"]
    items = info["items"][:n]
    if len(items) < n:
        raise SystemExit(f"seri {nama}: butuh {n} item, cuma ada {len(items)}")
    dek = [{"jenis": "cover", "nomor": None, "body": None,
            "baris": _baris([b.replace("{n}", str(n)) for b in info["judul"]])}]
    for i, (judul, body) in enumerate(items, 1):
        # nomor di DEPAN judul halaman, tanpa nol di depan (Bos 6 Okt: "kasih angka di depannya ...
        # gapapa ga usah pakai 0")
        baris = [f"{i}. {judul[0]}"] + list(judul[1:])
        dek.append({"jenis": "isi", "nomor": str(i), "body": body, "baris": _baris(baris)})
    dek.append({"jenis": "penutup", "nomor": None, "body": None, "baris": _baris(info["tutup"])})
    return dek


def font(path, size):
    try:
        return ImageFont.truetype(path, size)
    except Exception:
        return ImageFont.load_default()


def fit(t, path, maxw, start, min_size=30):
    s = start
    while s > min_size:
        if font(path, s).getlength(t) <= maxw:
            return s
        s -= 2
    return min_size


def bungkus(t, f, maxw):
    kata, baris, kini = t.split(), [], ""
    for k in kata:
        coba = (kini + " " + k).strip()
        if f.getlength(coba) <= maxw or not kini:
            kini = coba
        else:
            baris.append(kini)
            kini = k
    if kini:
        baris.append(kini)
    return baris


def bentuk_latar(img):
    lap = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    d = ImageDraw.Draw(lap)
    d.ellipse([-260, -120, 520, 660], fill=PUTIH + (255,))
    d.ellipse([330, -430, 1290, 530], outline=PUTIH + (255,), width=48)
    img.alpha_composite(lap)


def nomor_besar(img, teks):
    lap = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    ImageDraw.Draw(lap).text((470, 880), teks, font=font(F_BOLD, 700), fill=PUTIH + (255,))
    img.alpha_composite(lap)


def teks_miring(img, xy, teks, f, fill=HITAM, geser=0.22):
    lebar = int(f.getlength(teks)) + 40 + int(f.size * geser)
    tinggi = int(f.size * 1.6)
    lap = Image.new("RGBA", (lebar, tinggi), (0, 0, 0, 0))
    ImageDraw.Draw(lap).text((0, 0), teks, font=f, fill=fill + (255,))
    lap = lap.transform((lebar, tinggi), Image.AFFINE, (1, geser, -geser * tinggi * 0.35, 0, 1, 0),
                        resample=Image.BICUBIC)
    img.alpha_composite(lap, (int(xy[0]), int(xy[1])))


def bookmark(d, x, y, s=1.0):
    w_, h_ = int(64 * s), int(84 * s)
    d.polygon([(x, y), (x + w_, y), (x + w_, y + h_), (x + w_ // 2, y + h_ - int(24 * s)), (x, y + h_)], fill=HITAM)


def render(dek, outdir, idx, judul_seri):
    img = Image.new("RGBA", (W, H), BG + (255,))
    bentuk_latar(img)
    if dek.get("nomor"):
        nomor_besar(img, dek["nomor"])
    d = ImageDraw.Draw(img)
    masalah = []

    d.text((M, 92), NAMA, font=font(F_REG, 42), fill=HITAM)
    d.text((W - M, 92), LABEL, font=font(F_BOLD, 42), fill=HITAM, anchor="ra")

    maxw = W - 2 * M
    y = 430 if dek["jenis"] == "cover" else 400
    ukuran = min(fit(t, (F_BOLD if b == "bold" else F_LIGHT), maxw, 122, 48) for t, b in dek["baris"])
    for teks, bobot in dek["baris"]:
        path = F_BOLD if bobot == "bold" else F_LIGHT
        lh = int(ukuran * 0.98)
        if y + lh > H - 250:
            masalah.append(f"headline lewat batas: {teks}")
        d.text((M, y), teks, font=font(path, ukuran), fill=HITAM)
        y += lh

    if dek.get("body"):
        fb = font(F_LIGHT, 46)
        yb = y + 34
        for i, b in enumerate(bungkus(dek["body"], fb, 620)):
            if yb + i * 62 > H - 220:
                masalah.append("body lewat batas")
                break
            teks_miring(img, (M - 6, yb + i * 62), b, fb)

    d = ImageDraw.Draw(img)
    d.text((M, H - 118), SITUS, font=font(F_REG, 34), fill=HITAM)
    if dek["jenis"] == "penutup":
        bookmark(d, W - M - 64, H - 146, 1.0)
    else:
        teks_miring(img, (W - M - 128, H - 122), "SWIPE", font(F_BOLD, 36))

    os.makedirs(outdir, exist_ok=True)
    out = os.path.join(outdir, f"uky_car_{idx:02d}.jpg")
    img.convert("RGB").save(out, quality=93)
    return out, masalah


def main():
    pilih = sys.argv[sys.argv.index("--seri") + 1] if "--seri" in sys.argv else None
    total = 0
    for nama in SERI:
        if pilih and nama != pilih:
            continue
        dek = deck_dari(nama)
        outdir = f"/tmp/uky_car_{nama}"
        for f_ in os.listdir(outdir) if os.path.isdir(outdir) else []:
            if f_.startswith("uky_car_"):
                os.remove(os.path.join(outdir, f_))
        for i, dk in enumerate(dek, 1):
            out, masalah = render(dk, outdir, i, nama)
            total += len(masalah)
            print(f"{out} {'✅' if not masalah else '❌ ' + '; '.join(masalah)}")
    print(f"selesai | seri={pilih or 'semua'} | masalah: {total}")


if __name__ == "__main__":
    main()
