#include <tt_app.h>
#include <tt_lvgl_toolbar.h>
#include <tt_app_alertdialog.h>

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <sys/stat.h>
#include <dirent.h>

#include "freertos/FreeRTOS.h"
#include "esp_log.h"
#include "tactility/lvgl_fonts.h"

// Embedded pretty serif fonts - Georgia Italic like reference white card
LV_FONT_DECLARE(georgia_italic_26)
LV_FONT_DECLARE(georgia_italic_22)
LV_FONT_DECLARE(georgia_italic_20)
LV_FONT_DECLARE(georgia_italic_18)
LV_FONT_DECLARE(georgia_italic_16)
LV_FONT_DECLARE(georgia_regular_24)

#define TAG "BibleVerse"
#define AUTO_ADV_MS 60000 // 1 minute

// Per-book binary format (produced by split_bible.py):
// <bnum>.bin : concatenated NUL-terminated UTF8 verse strings
// <bnum>.idx : header 8 bytes: magic 'BIBK' 4s, version u16, count u32
//              then per entry: offset u32, cnum u16, vnum u16  (8 bytes)
// books.json : array of {bnumber, bname, verses, chapters}

#define MAX_BIBLE_BOOKS 66
#define MAX_BOOK_NAME 40
#define MAX_VERSE_TEXT 2048
#define MAX_PATH 128

typedef struct {
    uint32_t offset;
    uint16_t cnum;
    uint16_t vnum;
} VerseIndex;

typedef struct {
    int bnumber;
    char bname[MAX_BOOK_NAME];
    int verse_count;
    int chapter_count;
} BookInfo;

typedef struct {
    // App
    AppHandle app;

    // Bible metadata
    BookInfo books[MAX_BIBLE_BOOKS];
    int book_count;
    int total_verses;

    // Current position (global index 0..total-1 AND book/chap/verse breakdown)
    int cur_global; // 0-based global verse index
    int cur_book_idx; // 0..book_count-1 index into books[]
    int cur_verse_in_book; // 0..book.verse_count-1

    // Loaded book data (only one book at a time)
    uint8_t* book_bin; // malloc'd content of XX.bin
    size_t book_bin_size;
    VerseIndex* book_idx; // malloc'd parsed index
    int book_idx_count;
    int loaded_bnumber; // which bnumber is loaded, -1 = none

    // Settings / favorites
    char progress_path[MAX_PATH];
    char favorites_path[MAX_PATH];
    char books_json_path[MAX_PATH];
    char bible_dir[MAX_PATH]; // assets/bible/ path prefix
    // SD fallback paths
    char sd_bible_dir[MAX_PATH];
    bool use_sd_bible;

    // Highlight
    bool is_highlighted;

    // UI
    lv_obj_t* root;
    lv_obj_t* verse_wrapper; // full screen, clickable to toggle chrome
    lv_obj_t* center_col; // editorial column for fade
    lv_obj_t* lbl_verse;
    lv_obj_t* lbl_ref; // small ref line below verse
    lv_obj_t* header_bar;
    lv_obj_t* ctrl_bar;
    lv_obj_t* lbl_position;
    lv_obj_t* lbl_book_title;
    lv_obj_t* btn_prev;
    lv_obj_t* btn_next;
    lv_obj_t* btn_play_pause;
    lv_obj_t* btn_highlight;
    lv_obj_t* btn_close;
    lv_obj_t* btn_books;

    // Pretty serif fonts (Georgia Italic .fnt bin)
    // Will be linked as C arrays from assets/fonts via binfont -> replaced with embedded C now
    // Keep pointers for compatibility
    lv_font_t* font_serif_26;
    lv_font_t* font_serif_22;
    lv_font_t* font_serif_20;
    lv_font_t* font_serif_18;
    lv_font_t* font_serif_16;
    lv_font_t* font_book_24;
    bool fonts_loaded;

    // Book browser - horizontal slide (0.7.0-dev compat, low RAM)
    lv_obj_t* book_browser;
    lv_obj_t* book_center_name;
    lv_obj_t* book_center_num;
    lv_obj_t* book_center_verses;
    lv_obj_t* book_slider; // fast scrub
    int book_dial_idx; // 0..book_count-1 currently selected
    int book_drag_start_x;
    bool book_dragging;
    bool book_browser_visible;

    // Timer
    lv_timer_t* auto_timer;
    lv_timer_t* chrome_auto_hide_timer; // menu auto-hide after 10s on start
    bool auto_paused;
    bool ui_visible; // chrome visible?
} AppCtx;

static AppCtx g_ctx;

/* ── Forward ── */
static bool load_books_manifest(AppCtx* ctx);
static bool load_book(AppCtx* ctx, int bnumber);
static void unload_book(AppCtx* ctx);
static void show_current_verse(AppCtx* ctx);
static void advance_verse(AppCtx* ctx, int delta);
static void jump_to_book(AppCtx* ctx, int book_idx);
static void jump_to_global(AppCtx* ctx, int global_idx);
static void save_progress(AppCtx* ctx);
static bool load_progress(AppCtx* ctx);
static void toggle_ui(AppCtx* ctx);
static void set_chrome_visible(AppCtx* ctx, bool visible);
static bool is_favorite(AppCtx* ctx, int global_idx);
static void toggle_favorite(AppCtx* ctx);
static void ensure_user_dir(AppCtx* ctx);
static void set_book_browser_visible(AppCtx* ctx, bool visible);
static void toggle_book_browser(AppCtx* ctx);
static void build_book_browser(AppCtx* ctx, lv_obj_t* parent);

/* ── Utilities ── */
static void ensure_user_dir(AppCtx* ctx) {
    // mkdir -p for progress dir: parent of progress_path
    char dir[MAX_PATH];
    strncpy(dir, ctx->progress_path, sizeof(dir)-1);
    dir[sizeof(dir)-1]='\0';
    char* last = strrchr(dir, '/');
    if (last) {
        *last='\0';
        // create intermediate
        char tmp[MAX_PATH];
        size_t len = strlen(dir);
        for (size_t i=1;i<=len;i++) {
            if (dir[i]=='/' || dir[i]=='\0') {
                char c=dir[i];
                dir[i]='\0';
                strncpy(tmp, dir, sizeof(tmp)-1);
                tmp[sizeof(tmp)-1]='\0';
                mkdir(tmp, 0755);
                dir[i]=c;
            }
        }
    }
}

static bool file_exists(const char* path) {
    struct stat st;
    return stat(path, &st)==0;
}

/* ── Bible asset discovery: try assets/bible then SD fallback ── */
static void resolve_bible_paths(AppCtx* ctx) {
    // First try app assets: tt_app_get_assets_child_path("bible")
    char assets_path[MAX_PATH]; size_t sz=sizeof(assets_path);
    tt_app_get_assets_path(ctx->app, assets_path, &sz);
    // assets_path already is the assets folder itself
    snprintf(ctx->bible_dir, sizeof(ctx->bible_dir), "%s/bible", assets_path);
    snprintf(ctx->books_json_path, sizeof(ctx->books_json_path), "%s/books.json", ctx->bible_dir);

    // SD fallback: /sdcard/bibles or /sdcard/bible
    ctx->use_sd_bible = false;
    const char* candidates[] = {"/sdcard/bibles", "/sdcard/bible", "/data/bibles", NULL};
    for (int i=0;candidates[i];i++) {
        char cand_books[MAX_PATH];
        snprintf(cand_books, sizeof(cand_books), "%s/books.json", candidates[i]);
        if (file_exists(cand_books)) {
            strncpy(ctx->sd_bible_dir, candidates[i], sizeof(ctx->sd_bible_dir)-1);
            // prefer SD if assets not found
            if (!file_exists(ctx->books_json_path)) {
                strncpy(ctx->bible_dir, candidates[i], sizeof(ctx->bible_dir)-1);
                strncpy(ctx->books_json_path, cand_books, sizeof(ctx->books_json_path)-1);
                ctx->use_sd_bible = true;
            }
            ESP_LOGI(TAG, "Found SD bible at %s (assets bible exists=%d)", candidates[i], file_exists(assets_path));
            break;
        }
        // also check if XX.bin exists without manifest - if so accept
        if (!ctx->use_sd_bible) {
            char sample[MAX_PATH];
            snprintf(sample, sizeof(sample), "%s/01.bin", candidates[i]);
            if (file_exists(sample)) {
                // synthesize if no json - will still attempt scan
                strncpy(ctx->sd_bible_dir, candidates[i], sizeof(ctx->sd_bible_dir)-1);
                if (!file_exists(ctx->books_json_path)) {
                    strncpy(ctx->bible_dir, candidates[i], sizeof(ctx->bible_dir)-1);
                    snprintf(ctx->books_json_path, sizeof(ctx->books_json_path), "%s/books.json", ctx->bible_dir);
                    ctx->use_sd_bible = true;
                }
                break;
            }
        }
    }
    ESP_LOGI(TAG, "Bible dir resolved: %s use_sd=%d", ctx->bible_dir, ctx->use_sd_bible);
}

/* ── Simple JSON parse for books.json (no cJSON to keep deps low) ── */
static bool parse_books_json(AppCtx* ctx, const char* json_str) {
    // Extremely simple: look for "bnumber": N and "bname": "...", "verses": N, "chapters": N
    // We'll do a state scan: search for { ... } objects inside "books": [
    const char* p = strstr(json_str, "\"books\"");
    if (!p) return false;
    p = strchr(p, '[');
    if (!p) return false;
    p++;
    ctx->book_count = 0;
    while (ctx->book_count < MAX_BIBLE_BOOKS) {
        const char* obj_start = strchr(p, '{');
        if (!obj_start) break;
        const char* obj_end = strchr(obj_start, '}');
        if (!obj_end) break;
        // parse fields within obj_start..obj_end
        char temp[256];
        size_t len = (size_t)(obj_end - obj_start + 1);
        if (len >= sizeof(temp)) len = sizeof(temp)-1;
        memcpy(temp, obj_start, len);
        temp[len]='\0';

        int bnum=0, verses=0, chapters=0;
        char bname[MAX_BOOK_NAME]={0};

        const char* pn = strstr(temp, "\"bnumber\"");
        if (pn) sscanf(pn, "\"bnumber\"%*[: ]%d", &bnum);
        const char* pb = strstr(temp, "\"bname\"");
        if (pb) {
            const char* colon = strchr(pb, ':');
            if (colon) {
                const char* q1 = strchr(colon, '\"');
                if (q1) {
                    q1++;
                    const char* q2 = strchr(q1, '\"');
                    if (q2 && (q2-q1)< (int)sizeof(bname)-1) {
                        memcpy(bname, q1, q2-q1);
                        bname[q2-q1]='\0';
                    }
                }
            }
        }
        const char* pv = strstr(temp, "\"verses\"");
        if (pv) sscanf(pv, "\"verses\"%*[: ]%d", &verses);
        const char* pc = strstr(temp, "\"chapters\"");
        if (pc) sscanf(pc, "\"chapters\"%*[: ]%d", &chapters);

        if (bnum>0 && bname[0]) {
            ctx->books[ctx->book_count].bnumber = bnum;
            strncpy(ctx->books[ctx->book_count].bname, bname, MAX_BOOK_NAME-1);
            ctx->books[ctx->book_count].verse_count = verses;
            ctx->books[ctx->book_count].chapter_count = chapters;
            ctx->book_count++;
        }
        p = obj_end+1;
        // stop if ] seen before next {
        const char* next_brace = strchr(p, '{');
        const char* next_bracket = strchr(p, ']');
        if (next_bracket && (!next_brace || next_bracket < next_brace)) break;
    }

    // total_verses
    const char* pt = strstr(json_str, "\"total_verses\"");
    if (pt) {
        sscanf(pt, "\"total_verses\"%*[: ]%d", &ctx->total_verses);
    } else {
        ctx->total_verses=0;
        for (int i=0;i<ctx->book_count;i++) ctx->total_verses+=ctx->books[i].verse_count;
    }
    return ctx->book_count>0;
}

static char* read_file_to_str(const char* path, size_t* out_len) {
    FILE* f = fopen(path, "rb");
    if (!f) return NULL;
    fseek(f,0,SEEK_END);
    long sz = ftell(f);
    if (sz<=0 || sz>500000) { fclose(f); return NULL; }
    fseek(f,0,SEEK_SET);
    char* buf = malloc(sz+1);
    if (!buf) { fclose(f); return NULL; }
    size_t n = fread(buf,1,sz,f);
    fclose(f);
    buf[n]='\0';
    if (out_len) *out_len=n;
    return buf;
}

static bool load_books_manifest(AppCtx* ctx) {
    char* json = read_file_to_str(ctx->books_json_path, NULL);
    if (!json) {
        // fallback: scan directory for *.bin and create synthetic list if books.json missing
        ESP_LOGW(TAG, "books.json not found at %s, scanning dir", ctx->books_json_path);
        DIR* d = opendir(ctx->bible_dir);
        if (!d) {
            ESP_LOGE(TAG, "Cannot open bible dir %s", ctx->bible_dir);
            return false;
        }
        ctx->book_count=0;
        ctx->total_verses=0;
        struct dirent* ent;
        while ((ent=readdir(d))!=NULL && ctx->book_count<MAX_BIBLE_BOOKS) {
            if (ent->d_name[0]=='.') continue;
            size_t l=strlen(ent->d_name);
            if (l<5) continue;
            if (strcmp(ent->d_name+l-4, ".bin")!=0) continue;
            int bnum=0;
            if (sscanf(ent->d_name, "%d", &bnum)!=1) continue;
            // try read idx to get count
            char idx_path[MAX_PATH];
            snprintf(idx_path, sizeof(idx_path), "%s/%02d.idx", ctx->bible_dir, bnum);
            FILE* idxf = fopen(idx_path, "rb");
            int vcount=0;
            if (idxf) {
                uint8_t hdr[10]={0};
                if (fread(hdr,1,10,idxf)==10) {
                    if (memcmp(hdr, "BIBK",4)==0) {
                        // format <4sHI
                        uint16_t ver; uint32_t cnt;
                        memcpy(&ver, hdr+4,2);
                        memcpy(&cnt, hdr+6,4);
                        vcount = (int)cnt;
                    } else {
                        // maybe old header <4sHI as earlier: try
                        // Actually we wrote <4sHI -> magic 4, H 2, I 4 =10 bytes
                        // So above.
                    }
                }
                fclose(idxf);
            }
            // generic name from bnumber if can't find
            ctx->books[ctx->book_count].bnumber = bnum;
            snprintf(ctx->books[ctx->book_count].bname, MAX_BOOK_NAME, "Book %02d", bnum);
            ctx->books[ctx->book_count].verse_count = vcount;
            ctx->books[ctx->book_count].chapter_count = 0;
            ctx->book_count++;
            ctx->total_verses+=vcount;
        }
        closedir(d);
        // sort by bnumber
        for (int i=0;i<ctx->book_count-1;i++) for (int j=0;j<ctx->book_count-i-1;j++) if (ctx->books[j].bnumber > ctx->books[j+1].bnumber) {
            BookInfo tmp = ctx->books[j];
            ctx->books[j]=ctx->books[j+1];
            ctx->books[j+1]=tmp;
        }
        return ctx->book_count>0;
    }
    bool ok = parse_books_json(ctx, json);
    free(json);
    ESP_LOGI(TAG, "Loaded %d books, total verses %d from %s", ctx->book_count, ctx->total_verses, ctx->books_json_path);
    return ok;
}

/* ── Book loading ── */
static bool load_book(AppCtx* ctx, int bnumber) {
    if (ctx->loaded_bnumber == bnumber && ctx->book_bin && ctx->book_idx) return true;
    unload_book(ctx);

    char bin_path[MAX_PATH];
    char idx_path[MAX_PATH];
    snprintf(bin_path, sizeof(bin_path), "%s/%02d.bin", ctx->bible_dir, bnumber);
    snprintf(idx_path, sizeof(idx_path), "%s/%02d.idx", ctx->bible_dir, bnumber);

    if (!file_exists(bin_path) || !file_exists(idx_path)) {
        ESP_LOGE(TAG, "Book files missing bnum=%d bin=%s idx=%s", bnumber, bin_path, idx_path);
        return false;
    }

    // load idx first
    FILE* f = fopen(idx_path, "rb");
    if (!f) return false;
    uint8_t hdr[10];
    if (fread(hdr,1,10,f)!=10) { fclose(f); return false; }
    if (memcmp(hdr, "BIBK",4)!=0) { fclose(f); return false; }
    uint16_t ver=0; uint32_t count=0;
    memcpy(&ver, hdr+4, 2);
    memcpy(&count, hdr+6, 4);
    if (count==0 || count>10000) { fclose(f); return false; }

    ctx->book_idx = malloc(sizeof(VerseIndex)*count);
    if (!ctx->book_idx) { fclose(f); return false; }
    for (uint32_t i=0;i<count;i++) {
        uint8_t entry[8];
        if (fread(entry,1,8,f)!=8) { free(ctx->book_idx); ctx->book_idx=NULL; fclose(f); return false; }
        uint32_t off; uint16_t cnum, vnum;
        memcpy(&off, entry,4);
        memcpy(&cnum, entry+4,2);
        memcpy(&vnum, entry+6,2);
        ctx->book_idx[i].offset=off;
        ctx->book_idx[i].cnum=cnum;
        ctx->book_idx[i].vnum=vnum;
    }
    fclose(f);
    ctx->book_idx_count = (int)count;

    // load bin
    f = fopen(bin_path, "rb");
    if (!f) { free(ctx->book_idx); ctx->book_idx=NULL; return false; }
    fseek(f,0,SEEK_END);
    long sz = ftell(f);
    fseek(f,0,SEEK_SET);
    if (sz<=0 || sz>1024*1024) { fclose(f); free(ctx->book_idx); ctx->book_idx=NULL; return false; }
    ctx->book_bin = malloc(sz+1);
    if (!ctx->book_bin) { fclose(f); free(ctx->book_idx); ctx->book_idx=NULL; return false; }
    size_t n = fread(ctx->book_bin,1,sz,f);
    fclose(f);
    ctx->book_bin[sz]='\0';
    ctx->book_bin_size = n;
    ctx->loaded_bnumber = bnumber;
    ESP_LOGI(TAG, "Loaded book %02d (%d verses, %ld bytes)", bnumber, ctx->book_idx_count, sz);
    return true;
}

static void unload_book(AppCtx* ctx) {
    if (ctx->book_bin) { free(ctx->book_bin); ctx->book_bin=NULL; }
    if (ctx->book_idx) { free(ctx->book_idx); ctx->book_idx=NULL; }
    ctx->book_bin_size=0;
    ctx->book_idx_count=0;
    ctx->loaded_bnumber=-1;
}

/* ── Progress persistence ── */
static void save_progress(AppCtx* ctx) {
    ensure_user_dir(ctx);
    FILE* f = fopen(ctx->progress_path, "w");
    if (!f) { ESP_LOGE(TAG,"save_progress fail %s", ctx->progress_path); return; }
    // Store global index and also b/c/v for human debug
    int bnum = 0; int cnum=1; int vnum=1;
    if (ctx->cur_book_idx>=0 && ctx->cur_book_idx < ctx->book_count) {
        bnum = ctx->books[ctx->cur_book_idx].bnumber;
        if (ctx->book_idx && ctx->cur_verse_in_book < ctx->book_idx_count) {
            cnum = ctx->book_idx[ctx->cur_verse_in_book].cnum;
            vnum = ctx->book_idx[ctx->cur_verse_in_book].vnum;
        }
    }
    fprintf(f, "%d\n%d %d %d\n%d %d\n", ctx->cur_global, bnum, cnum, vnum, ctx->cur_book_idx, ctx->cur_verse_in_book);
    fclose(f);
    ESP_LOGI(TAG,"Saved progress global=%d b=%d c=%d v=%d", ctx->cur_global, bnum, cnum, vnum);
}

static bool load_progress(AppCtx* ctx) {
    FILE* f = fopen(ctx->progress_path, "r");
    if (!f) return false;
    int global_=0;
    int bnum=0,cnum=0,vnum=0;
    int book_idx_=0, verse_in_book_=0;
    int r = fscanf(f, "%d", &global_);
    if (r==1) {
        // try second line
        fscanf(f, "%d %d %d", &bnum, &cnum, &vnum);
        fscanf(f, "%d %d", &book_idx_, &verse_in_book_);
    }
    fclose(f);
    if (global_<0) global_=0;
    if (global_>=ctx->total_verses) global_=0;
    ctx->cur_global = global_;
    // We will resolve book_idx / verse_in_book from global later in global_to_book()
    (void)bnum;(void)cnum;(void)vnum;(void)book_idx_;(void)verse_in_book_;
    return true;
}

// Convert global idx -> book_idx and verse_in_book
static void global_to_book(AppCtx* ctx, int global) {
    if (ctx->book_count==0) return;
    int remaining = global;
    for (int i=0;i<ctx->book_count;i++) {
        int vc = ctx->books[i].verse_count;
        if (vc<=0) continue;
        if (remaining < vc) {
            ctx->cur_book_idx = i;
            ctx->cur_verse_in_book = remaining;
            return;
        }
        remaining -= vc;
    }
    // clamp to last
    ctx->cur_book_idx = ctx->book_count-1;
    ctx->cur_verse_in_book = ctx->books[ctx->book_count-1].verse_count-1;
    if (ctx->cur_verse_in_book<0) ctx->cur_verse_in_book=0;
}

static void book_to_global(AppCtx* ctx) {
    int g=0;
    for (int i=0;i<ctx->cur_book_idx;i++) g+=ctx->books[i].verse_count;
    g+=ctx->cur_verse_in_book;
    ctx->cur_global=g;
}

/* ── Favorites ── */
static bool is_favorite(AppCtx* ctx, int global_idx) {
    if (global_idx<0) return false;
    FILE* f = fopen(ctx->favorites_path, "r");
    if (!f) return false;
    int v;
    while (fscanf(f, "%d", &v)==1) {
        if (v==global_idx) { fclose(f); return true; }
    }
    fclose(f);
    return false;
}

static void toggle_favorite(AppCtx* ctx) {
    // load all
    int vals[512];
    int cnt=0;
    FILE* f = fopen(ctx->favorites_path, "r");
    if (f) {
        while (cnt<512 && fscanf(f, "%d", &vals[cnt])==1) cnt++;
        fclose(f);
    }
    // check existence
    int idx=-1;
    for (int i=0;i<cnt;i++) if (vals[i]==ctx->cur_global) idx=i;
    if (idx>=0) {
        // remove
        for (int i=idx;i<cnt-1;i++) vals[i]=vals[i+1];
        cnt--;
        ctx->is_highlighted=false;
    } else {
        if (cnt<512) {
            vals[cnt++]=ctx->cur_global;
            ctx->is_highlighted=true;
        }
    }
    ensure_user_dir(ctx);
    f = fopen(ctx->favorites_path, "w");
    if (!f) return;
    for (int i=0;i<cnt;i++) fprintf(f, "%d\n", vals[i]);
    fclose(f);
}

/* ── UI logic ── */
// Font resolution: pretty serif if loaded, else fallback to system
// Georgia Italic is wider/taller than Montserrat system -> map down one tier to avoid crop
static const lv_font_t* resolve_verse_font(AppCtx* ctx, enum LvglFontSize fallback_sz) {
    if (!ctx->fonts_loaded) return lvgl_get_text_font(fallback_sz);
    // Georgia 26 is visually larger than system LARGE, so use 22 for LARGE tier, 20 for DEFAULT, 16 for SMALL
    if (fallback_sz == FONT_SIZE_LARGE && ctx->font_serif_22) return ctx->font_serif_22; // was 26, too big
    if (fallback_sz == FONT_SIZE_DEFAULT && ctx->font_serif_18) return ctx->font_serif_18; // was 20
    if (fallback_sz == FONT_SIZE_SMALL && ctx->font_serif_16) return ctx->font_serif_16;
    // ultra-short special: still allow 26 for very tiny verses <40 chars
    return lvgl_get_text_font(fallback_sz);
}
static const lv_font_t* resolve_verse_font_ultra(AppCtx* ctx) {
    if (ctx->fonts_loaded && ctx->font_serif_26) return ctx->font_serif_26;
    return lvgl_get_text_font(FONT_SIZE_LARGE);
}
static const lv_font_t* resolve_book_font(AppCtx* ctx) {
    if (ctx->font_book_24) return ctx->font_book_24;
    if (ctx->font_serif_22) return ctx->font_serif_22;
    return lvgl_get_text_font(FONT_SIZE_LARGE);
}
static void apply_verse_scaling(AppCtx* ctx, const char* verse) {
    if (!ctx->lbl_verse) return;
    size_t len = strlen(verse);
    enum LvglFontSize font_sz;
    bool is_ultra = false;
    int line_space;
    int pad_v;
    int pad_h;
    int letter_space = 0;

    if (len < 40) {
        font_sz = FONT_SIZE_LARGE;
        is_ultra = true;
        line_space = 12;
        pad_v = 6;
        pad_h = 20;
        letter_space = 0;
    } else if (len < 100) {
        font_sz = FONT_SIZE_LARGE;
        line_space = 8;
        pad_v = 4;
        pad_h = 16;
    } else if (len < 200) {
        font_sz = FONT_SIZE_DEFAULT;
        line_space = 6;
        pad_v = 3;
        pad_h = 14;
    } else if (len < 350) {
        font_sz = FONT_SIZE_SMALL;
        line_space = 5;
        pad_v = 2;
        pad_h = 12;
    } else {
        font_sz = FONT_SIZE_SMALL;
        line_space = 4;
        pad_v = 1;
        pad_h = 8;
    }

    const lv_font_t* f = is_ultra ? resolve_verse_font_ultra(ctx) : resolve_verse_font(ctx, font_sz);
    lv_obj_set_style_text_font(ctx->lbl_verse, f, 0);
    lv_obj_set_style_text_line_space(ctx->lbl_verse, line_space, 0);
    lv_obj_set_style_text_letter_space(ctx->lbl_verse, letter_space, 0);
    lv_obj_set_style_pad_top(ctx->lbl_verse, pad_v, 0);
    lv_obj_set_style_pad_bottom(ctx->lbl_verse, pad_v, 0);
    lv_obj_set_style_pad_left(ctx->lbl_verse, pad_h, 0);
    lv_obj_set_style_pad_right(ctx->lbl_verse, pad_h, 0);
}

// Fade transition for verse change
static void verse_fade_anim_cb(void* var, int32_t v) {
    lv_obj_t* obj = (lv_obj_t*)var;
    lv_obj_set_style_text_opa(obj, v, 0);
}
static void verse_fade_in(AppCtx* ctx) {
    if (!ctx->lbl_verse || !ctx->center_col) return;
    lv_anim_t a;
    lv_anim_init(&a);
    lv_anim_set_var(&a, ctx->lbl_verse);
    lv_anim_set_values(&a, 40, 255);
    lv_anim_set_duration(&a, 220);
    lv_anim_set_exec_cb(&a, verse_fade_anim_cb);
    lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
    lv_anim_start(&a);
    // Also fade ref slightly delayed
    if (ctx->lbl_ref) {
        lv_anim_t b;
        lv_anim_init(&b);
        lv_anim_set_var(&b, ctx->lbl_ref);
        lv_anim_set_values(&b, 30, 200);
        lv_anim_set_duration(&b, 300);
        lv_anim_set_exec_cb(&b, verse_fade_anim_cb);
        lv_anim_set_path_cb(&b, lv_anim_path_ease_out);
        lv_anim_start(&b);
    }
}

static void show_current_verse(AppCtx* ctx) {
    if (ctx->book_count==0) return;
    global_to_book(ctx, ctx->cur_global);
    book_to_global(ctx);
    int bnum = ctx->books[ctx->cur_book_idx].bnumber;

    if (!load_book(ctx, bnum)) {
        lv_label_set_text(ctx->lbl_verse, "Failed to load book");
        lv_label_set_text(ctx->lbl_ref, "");
        return;
    }

    if (ctx->cur_verse_in_book<0) ctx->cur_verse_in_book=0;
    if (ctx->cur_verse_in_book>=ctx->book_idx_count) ctx->cur_verse_in_book=ctx->book_idx_count-1;

    VerseIndex* vi = &ctx->book_idx[ctx->cur_verse_in_book];
    uint32_t off = vi->offset;
    const char* text_ptr = "(empty)";
    if (off < ctx->book_bin_size) text_ptr = (const char*)(ctx->book_bin + off);

    // Fix stack overflow: verse_buf was 2048 bytes on stack, causing gui task (4096) overflow
    // Use heap allocation
    char* verse_buf = (char*)malloc(MAX_VERSE_TEXT);
    if (!verse_buf) {
        ESP_LOGE(TAG, "Failed to malloc verse_buf");
        return;
    }
    strncpy(verse_buf, text_ptr, MAX_VERSE_TEXT-1);
    verse_buf[MAX_VERSE_TEXT-1]='\0';

    char ref_buf[64];
    snprintf(ref_buf, sizeof(ref_buf), "%s %d:%d", ctx->books[ctx->cur_book_idx].bname, vi->cnum, vi->vnum);

    char ref_pretty[80];
    char bname_upper[40];
    strncpy(bname_upper, ctx->books[ctx->cur_book_idx].bname, sizeof(bname_upper)-1);
    bname_upper[sizeof(bname_upper)-1]='\0';
    for (char* p=bname_upper; *p; ++p) *p = toupper((unsigned char)*p);
    snprintf(ref_pretty, sizeof(ref_pretty), "%s %d:%d", bname_upper, vi->cnum, vi->vnum);

    if (ctx->lbl_verse) {
        lv_obj_set_style_text_opa(ctx->lbl_verse, 60, 0);
        lv_label_set_text(ctx->lbl_verse, verse_buf);
        apply_verse_scaling(ctx, verse_buf);
    }
    if (ctx->lbl_ref) {
        lv_obj_set_style_text_opa(ctx->lbl_ref, 40, 0);
        lv_label_set_text(ctx->lbl_ref, ref_pretty);
    }
    if (ctx->lbl_book_title) lv_label_set_text(ctx->lbl_book_title, ref_buf);
    if (ctx->lbl_position) {
        char pos_buf[40];
        snprintf(pos_buf, sizeof(pos_buf), "%d / %d", ctx->cur_global+1, ctx->total_verses);
        lv_label_set_text(ctx->lbl_position, pos_buf);
    }

    ctx->is_highlighted = is_favorite(ctx, ctx->cur_global);
    if (ctx->btn_highlight) {
        lv_obj_t* lbl = lv_obj_get_child(ctx->btn_highlight, 0);
        if (lbl) lv_label_set_text(lbl, ctx->is_highlighted ? LV_SYMBOL_OK : LV_SYMBOL_DUMMY);
        if (ctx->is_highlighted) {
            lv_obj_set_style_bg_color(ctx->btn_highlight, lv_color_hex(0xF5C16C), 0);
            lv_obj_set_style_text_color(ctx->btn_highlight, lv_color_hex(0x1E1E2E), 0);
        } else {
            lv_obj_set_style_bg_color(ctx->btn_highlight, lv_color_hex(0x252538), 0);
            lv_obj_set_style_text_color(ctx->btn_highlight, lv_color_hex(0x8C8CB0), 0);
        }
        if (ctx->lbl_verse) {
            if (ctx->is_highlighted) {
                lv_obj_set_style_bg_color(ctx->lbl_verse, lv_color_hex(0x2E2818), 0);
                lv_obj_set_style_bg_opa(ctx->lbl_verse, LV_OPA_COVER, 0);
                lv_obj_set_style_border_color(ctx->lbl_verse, lv_color_hex(0xF5C16C), 0);
                lv_obj_set_style_border_width(ctx->lbl_verse, 1, 0);
                lv_obj_set_style_border_opa(ctx->lbl_verse, LV_OPA_40, 0);
            } else {
                lv_obj_set_style_bg_opa(ctx->lbl_verse, LV_OPA_TRANSP, 0);
                lv_obj_set_style_border_width(ctx->lbl_verse, 0, 0);
            }
        }
    }

    if (ctx->btn_prev) {
        if (ctx->cur_global<=0) lv_obj_add_state(ctx->btn_prev, LV_STATE_DISABLED);
        else lv_obj_clear_state(ctx->btn_prev, LV_STATE_DISABLED);
    }
    if (ctx->btn_next) {
        if (ctx->cur_global >= ctx->total_verses-1) lv_obj_add_state(ctx->btn_next, LV_STATE_DISABLED);
        else lv_obj_clear_state(ctx->btn_next, LV_STATE_DISABLED);
    }
    free(verse_buf);
    save_progress(ctx);
    verse_fade_in(ctx);
}

static void advance_verse(AppCtx* ctx, int delta) {
    int ng = ctx->cur_global + delta;
    if (ng < 0) ng = 0;
    if (ng >= ctx->total_verses) ng = ctx->total_verses-1;
    if (ng != ctx->cur_global) {
        ctx->cur_global = ng;
        show_current_verse(ctx);
    }
}

static void jump_to_global(AppCtx* ctx, int global_idx) {
    if (global_idx < 0) global_idx = 0;
    if (global_idx >= ctx->total_verses) global_idx = ctx->total_verses-1;
    ctx->cur_global = global_idx;
    set_book_browser_visible(ctx, false);
    set_chrome_visible(ctx, false);
    show_current_verse(ctx);
    // restart auto timer
    if (ctx->auto_timer) lv_timer_reset(ctx->auto_timer);
}

static void jump_to_book(AppCtx* ctx, int book_idx) {
    if (book_idx < 0 || book_idx >= ctx->book_count) return;
    // compute global start of book
    int global = 0;
    for (int i=0;i<book_idx;i++) global += ctx->books[i].verse_count;
    jump_to_global(ctx, global);
}

/* ── Timer ── */
static void on_auto_timer(lv_timer_t* t) {
    AppCtx* ctx = (AppCtx*)lv_timer_get_user_data(t);
    if (ctx->auto_paused) return;
    if (ctx->cur_global < ctx->total_verses-1) {
        advance_verse(ctx, 1);
    } else {
        ctx->auto_paused = true;
        if (ctx->btn_play_pause) {
            lv_obj_t* lbl = lv_obj_get_child(ctx->btn_play_pause, 0);
            if (lbl) lv_label_set_text(lbl, LV_SYMBOL_PLAY);
        }
    }
}

/* ── Book browser: horizontal slide, center name updates (arc-free) ── */
static void book_y_anim_cb(void* var, int32_t v) {
    lv_obj_t* obj = (lv_obj_t*)var;
    // v is y offset
    lv_obj_set_y(obj, v);
}
static void book_fade_anim_cb(void* var, int32_t v) {
    lv_obj_t* obj = (lv_obj_t*)var;
    lv_obj_set_style_text_opa(obj, v, 0);
}

static void book_dial_update_center(AppCtx* ctx) {
    if (!ctx->book_center_name) return;
    if (ctx->book_dial_idx <0) ctx->book_dial_idx=0;
    if (ctx->book_dial_idx >= ctx->book_count) ctx->book_dial_idx = ctx->book_count-1;
    BookInfo* b = &ctx->books[ctx->book_dial_idx];

    // Animate name change: quick fade (40->255)
    lv_obj_set_style_text_opa(ctx->book_center_name, 100, 0);
    {
        lv_anim_t an; lv_anim_init(&an);
        lv_anim_set_var(&an, ctx->book_center_name);
        lv_anim_set_values(&an, 60, 255);
        lv_anim_set_duration(&an, 180);
        lv_anim_set_exec_cb(&an, book_fade_anim_cb);
        lv_anim_set_path_cb(&an, lv_anim_path_ease_out);
        lv_anim_start(&an);
    }

    lv_label_set_text(ctx->book_center_name, b->bname);
    // Use pretty book font if loaded (50% larger already via LARGE vs DEFAULT fallback)
    lv_obj_set_style_text_font(ctx->book_center_name, resolve_book_font(ctx), 0);

    if (ctx->book_center_num) {
        char nb[20]; snprintf(nb, sizeof(nb), "%2d / %d", b->bnumber, ctx->book_count);
        lv_label_set_text(ctx->book_center_num, nb);
    }
    if (ctx->book_center_verses) {
        char vb[24]; snprintf(vb, sizeof(vb), "%d verses", b->verse_count);
        lv_label_set_text(ctx->book_center_verses, vb);
    }
    uint32_t col = (b->bnumber < 40) ? 0xE8E6E0 : 0x89B4FA;
    lv_obj_set_style_text_color(ctx->book_center_name, lv_color_hex(col), 0);

    if (ctx->book_slider) {
        // avoid recursive event loop - set without anim, but we add subtle anim for drag
        lv_slider_set_value(ctx->book_slider, ctx->book_dial_idx, LV_ANIM_OFF);
    }
}

static void set_book_browser_visible(AppCtx* ctx, bool visible) {
    ctx->book_browser_visible = visible;
    if (visible) {
        ctx->book_dial_idx = ctx->cur_book_idx;
        book_dial_update_center(ctx);
        if (ctx->book_browser) {
            lv_obj_remove_flag(ctx->book_browser, LV_OBJ_FLAG_HIDDEN);
            // Anim: pop center mid_wrap y 20 -> 0, ease_out 260ms
            if (ctx->book_center_name) {
                lv_obj_t* parent = lv_obj_get_parent(ctx->book_center_name); // mid_wrap
                lv_obj_set_y(parent, 20);
                lv_anim_t ay; lv_anim_init(&ay);
                lv_anim_set_var(&ay, parent);
                lv_anim_set_values(&ay, 20, 0);
                lv_anim_set_duration(&ay, 260);
                lv_anim_set_exec_cb(&ay, book_y_anim_cb);
                lv_anim_set_path_cb(&ay, lv_anim_path_ease_out);
                lv_anim_start(&ay);
            }
            // Fade name
            if (ctx->book_center_name) {
                lv_obj_set_style_text_opa(ctx->book_center_name, LV_OPA_20, 0);
                lv_anim_t af; lv_anim_init(&af);
                lv_anim_set_var(&af, ctx->book_center_name);
                lv_anim_set_values(&af, LV_OPA_20, LV_OPA_COVER);
                lv_anim_set_duration(&af, 220);
                lv_anim_set_exec_cb(&af, book_fade_anim_cb);
                lv_anim_set_path_cb(&af, lv_anim_path_ease_out);
                lv_anim_start(&af);
            }
        }
    } else {
        if (ctx->book_browser) lv_obj_add_flag(ctx->book_browser, LV_OBJ_FLAG_HIDDEN);
    }
}
static void toggle_book_browser(AppCtx* ctx) {
    // reset chrome auto-hide when opening browser
    if (ctx->chrome_auto_hide_timer) {
        lv_timer_delete(ctx->chrome_auto_hide_timer);
        ctx->chrome_auto_hide_timer = NULL;
    }
    set_book_browser_visible(ctx, !ctx->book_browser_visible);
}

static void on_books_close_click(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    set_book_browser_visible(ctx, false);
}
static void on_book_center_click(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    jump_to_book(ctx, ctx->book_dial_idx);
}
static void on_dial_prev_click(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    if (ctx->book_dial_idx>0) { ctx->book_dial_idx--; book_dial_update_center(ctx); }
}
static void on_dial_next_click(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    if (ctx->book_dial_idx < ctx->book_count-1) { ctx->book_dial_idx++; book_dial_update_center(ctx); }
}
static void on_slider_changed(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    int v = lv_slider_get_value(ctx->book_slider);
    if (v!=ctx->book_dial_idx) { ctx->book_dial_idx=v; book_dial_update_center(ctx); }
}
static void on_swipe_area_event(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    lv_event_code_t code = lv_event_get_code(e);
    lv_indev_t* indev = lv_indev_active();
    if (!indev) return;
    lv_point_t p; lv_indev_get_point(indev, &p);
    if (code==LV_EVENT_PRESSED) {
        ctx->book_drag_start_x = p.x;
        ctx->book_dragging = true;
    } else if (code==LV_EVENT_PRESSING && ctx->book_dragging) {
        int dx = p.x - ctx->book_drag_start_x;
        int thresh = 28; // px per book
        if (abs(dx) >= thresh) {
            int steps = dx / thresh;
            int new_idx = ctx->book_dial_idx - steps; // swipe left -> next
            if (new_idx<0) new_idx=0;
            if (new_idx>=ctx->book_count) new_idx=ctx->book_count-1;
            if (new_idx!=ctx->book_dial_idx) {
                ctx->book_dial_idx = new_idx;
                book_dial_update_center(ctx);
            }
            ctx->book_drag_start_x = p.x; // reset for next step
        }
    } else if (code==LV_EVENT_RELEASED || code==LV_EVENT_PRESS_LOST) {
        ctx->book_dragging=false;
    }
}

static void build_book_browser(AppCtx* ctx, lv_obj_t* parent) {
    ctx->book_browser = lv_obj_create(parent);
    lv_obj_set_size(ctx->book_browser, LV_PCT(100), LV_PCT(100));
    lv_obj_align(ctx->book_browser, LV_ALIGN_CENTER, 0, 0);
    lv_obj_set_style_bg_color(ctx->book_browser, lv_color_hex(0x0E0E14), 0);
    lv_obj_set_style_bg_opa(ctx->book_browser, LV_OPA_COVER, 0);
    lv_obj_set_style_border_width(ctx->book_browser, 0, 0);
    lv_obj_set_style_pad_all(ctx->book_browser, 0, 0);
    lv_obj_set_style_radius(ctx->book_browser, 0, 0);

    // Swipeable full-center area - no top bar
    lv_obj_t* center_wrap = lv_obj_create(ctx->book_browser);
    lv_obj_set_size(center_wrap, LV_PCT(100), LV_PCT(100));
    lv_obj_set_style_pad_top(center_wrap, 0, 0);
    lv_obj_set_style_pad_bottom(center_wrap, 64, 0);
    lv_obj_set_style_bg_opa(center_wrap, LV_OPA_TRANSP, 0);
    lv_obj_set_style_border_width(center_wrap, 0, 0);
    lv_obj_remove_flag(center_wrap, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_align(center_wrap, LV_ALIGN_CENTER, 0, 0);
    lv_obj_add_flag(center_wrap, LV_OBJ_FLAG_CLICKABLE);
    lv_obj_add_event_cb(center_wrap, on_swipe_area_event, LV_EVENT_PRESSED, ctx);
    lv_obj_add_event_cb(center_wrap, on_swipe_area_event, LV_EVENT_PRESSING, ctx);
    lv_obj_add_event_cb(center_wrap, on_swipe_area_event, LV_EVENT_RELEASED, ctx);
    lv_obj_add_event_cb(center_wrap, on_swipe_area_event, LV_EVENT_PRESS_LOST, ctx);

    // Left arrow
    lv_obj_t* btn_l = lv_button_create(center_wrap);
    lv_obj_set_size(btn_l, 44, 44);
    lv_obj_align(btn_l, LV_ALIGN_LEFT_MID, 8, 0);
    lv_obj_set_style_radius(btn_l, 22, 0);
    lv_obj_set_style_bg_color(btn_l, lv_color_hex(0x1E1E2E), 0);
    lv_obj_t* l_l = lv_label_create(btn_l); lv_label_set_text(l_l, LV_SYMBOL_PREV); lv_obj_center(l_l);
    lv_obj_add_event_cb(btn_l, on_dial_prev_click, LV_EVENT_CLICKED, ctx);

    // Center — book name directly, 50% larger, tappable (no card)
    lv_obj_t* mid_wrap = lv_obj_create(center_wrap);
    lv_obj_set_size(mid_wrap, LV_PCT(68), LV_SIZE_CONTENT);
    lv_obj_align(mid_wrap, LV_ALIGN_CENTER, 0, 0);
    lv_obj_set_style_bg_opa(mid_wrap, LV_OPA_TRANSP, 0);
    lv_obj_set_style_border_width(mid_wrap, 0, 0);
    lv_obj_set_style_pad_all(mid_wrap, 0, 0);
    lv_obj_set_style_pad_gap(mid_wrap, 10, 0);
    lv_obj_set_flex_flow(mid_wrap, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_flex_align(mid_wrap, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    lv_obj_remove_flag(mid_wrap, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_add_flag(mid_wrap, LV_OBJ_FLAG_CLICKABLE);
    lv_obj_add_event_cb(mid_wrap, on_book_center_click, LV_EVENT_CLICKED, ctx);

    ctx->book_center_name = lv_label_create(mid_wrap);
    lv_label_set_text(ctx->book_center_name, "Genesis");
    lv_obj_set_style_text_color(ctx->book_center_name, lv_color_hex(0xE8E6E0), 0);
    lv_obj_set_style_text_font(ctx->book_center_name, lvgl_get_text_font(FONT_SIZE_LARGE), 0); // 50% bigger: LARGE vs DEFAULT
    lv_obj_set_style_text_letter_space(ctx->book_center_name, 0, 0);
    lv_label_set_long_mode(ctx->book_center_name, LV_LABEL_LONG_MODE_DOTS);
    lv_obj_set_width(ctx->book_center_name, LV_PCT(100));
    lv_obj_set_style_text_align(ctx->book_center_name, LV_TEXT_ALIGN_CENTER, 0);

    ctx->book_center_verses = lv_label_create(mid_wrap);
    lv_label_set_text(ctx->book_center_verses, "—");
    lv_obj_set_style_text_color(ctx->book_center_verses, lv_color_hex(0x5A5A78), 0);
    lv_obj_set_style_text_font(ctx->book_center_verses, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
    lv_obj_set_style_text_align(ctx->book_center_verses, LV_TEXT_ALIGN_CENTER, 0);

    // Right arrow
    lv_obj_t* btn_r = lv_button_create(center_wrap);
    lv_obj_set_size(btn_r, 44, 44);
    lv_obj_align(btn_r, LV_ALIGN_RIGHT_MID, -8, 0);
    lv_obj_set_style_radius(btn_r, 22, 0);
    lv_obj_set_style_bg_color(btn_r, lv_color_hex(0x1E1E2E), 0);
    lv_obj_t* l_r = lv_label_create(btn_r); lv_label_set_text(l_r, LV_SYMBOL_NEXT); lv_obj_center(l_r);
    lv_obj_add_event_cb(btn_r, on_dial_next_click, LV_EVENT_CLICKED, ctx);

    // Bottom bar: [X cancel] [slider] [N / 66]
    lv_obj_t* bottom = lv_obj_create(ctx->book_browser);
    lv_obj_set_size(bottom, LV_PCT(100), 56);
    lv_obj_align(bottom, LV_ALIGN_BOTTOM_MID, 0, 0);
    lv_obj_set_style_bg_color(bottom, lv_color_hex(0x15151F), 0);
    lv_obj_set_style_bg_opa(bottom, LV_OPA_COVER, 0);
    lv_obj_set_style_border_width(bottom, 1, 0);
    lv_obj_set_style_border_side(bottom, LV_BORDER_SIDE_TOP, 0);
    lv_obj_set_style_border_color(bottom, lv_color_hex(0x232338), 0);
    lv_obj_set_style_radius(bottom, 0, 0);
    lv_obj_set_style_pad_hor(bottom, 10, 0);
    lv_obj_set_style_pad_ver(bottom, 8, 0);
    lv_obj_set_style_pad_gap(bottom, 10, 0);
    lv_obj_set_flex_flow(bottom, LV_FLEX_FLOW_ROW);
    lv_obj_set_flex_align(bottom, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    lv_obj_remove_flag(bottom, LV_OBJ_FLAG_SCROLLABLE);

    lv_obj_t* btn_cancel = lv_button_create(bottom);
    lv_obj_set_size(btn_cancel, 36, 32);
    lv_obj_set_style_radius(btn_cancel, 8, 0);
    lv_obj_set_style_bg_color(btn_cancel, lv_color_hex(0x252538), 0);
    lv_obj_t* l_cancel = lv_label_create(btn_cancel); lv_label_set_text(l_cancel, LV_SYMBOL_CLOSE); lv_obj_center(l_cancel);
    lv_obj_add_event_cb(btn_cancel, on_books_close_click, LV_EVENT_CLICKED, ctx);

    ctx->book_slider = lv_slider_create(bottom);
    lv_obj_set_flex_grow(ctx->book_slider, 1);
    lv_obj_set_height(ctx->book_slider, 8);
    lv_slider_set_range(ctx->book_slider, 0, ctx->book_count>0 ? ctx->book_count-1 : 65);
    lv_obj_set_style_bg_color(ctx->book_slider, lv_color_hex(0x232338), LV_PART_MAIN);
    lv_obj_set_style_bg_color(ctx->book_slider, lv_color_hex(0x4A4A6E), LV_PART_INDICATOR);
    lv_obj_set_style_bg_color(ctx->book_slider, lv_color_hex(0xE8E6E0), LV_PART_KNOB);
    lv_obj_add_event_cb(ctx->book_slider, on_slider_changed, LV_EVENT_VALUE_CHANGED, ctx);

    ctx->book_center_num = lv_label_create(bottom);
    lv_label_set_text(ctx->book_center_num, "1 / 66");
    lv_obj_set_style_text_color(ctx->book_center_num, lv_color_hex(0x8A8AAA), 0);
    lv_obj_set_style_text_font(ctx->book_center_num, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
    lv_obj_set_style_pad_left(ctx->book_center_num, 4, 0);

    lv_obj_add_flag(ctx->book_browser, LV_OBJ_FLAG_HIDDEN);
    ctx->book_browser_visible = false;
    ctx->book_dial_idx = 0;
    ctx->book_drag_start_x = 0;
    ctx->book_dragging = false;
}

/* ── UI callbacks ── */
static void on_verse_tap(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    toggle_ui(ctx);
}

static void chrome_auto_hide_cb(lv_timer_t* t) {
    AppCtx* ctx = (AppCtx*)lv_timer_get_user_data(t);
    if (!ctx) return;
    // Don't hide if book browser open
    if (ctx->book_browser_visible) return;
    if (ctx->ui_visible) set_chrome_visible(ctx, false);
    if (ctx->chrome_auto_hide_timer) {
        lv_timer_delete(ctx->chrome_auto_hide_timer);
        ctx->chrome_auto_hide_timer = NULL;
    }
}

static void schedule_chrome_auto_hide(AppCtx* ctx, uint32_t delay_ms) {
    if (ctx->chrome_auto_hide_timer) {
        lv_timer_delete(ctx->chrome_auto_hide_timer);
        ctx->chrome_auto_hide_timer = NULL;
    }
    ctx->chrome_auto_hide_timer = lv_timer_create(chrome_auto_hide_cb, delay_ms, ctx);
}

static void toggle_ui(AppCtx* ctx) {
    bool new_vis = !ctx->ui_visible;
    set_chrome_visible(ctx, new_vis);
    if (new_vis) {
        schedule_chrome_auto_hide(ctx, 10000); // hide again after 10s if user shows via tap
    } else {
        if (ctx->chrome_auto_hide_timer) {
            lv_timer_delete(ctx->chrome_auto_hide_timer);
            ctx->chrome_auto_hide_timer = NULL;
        }
    }
}

static void set_chrome_visible(AppCtx* ctx, bool visible) {
    ctx->ui_visible = visible;
    if (visible) {
        if (ctx->header_bar) lv_obj_remove_flag(ctx->header_bar, LV_OBJ_FLAG_HIDDEN);
        if (ctx->ctrl_bar) lv_obj_remove_flag(ctx->ctrl_bar, LV_OBJ_FLAG_HIDDEN);
    } else {
        if (ctx->header_bar) lv_obj_add_flag(ctx->header_bar, LV_OBJ_FLAG_HIDDEN);
        if (ctx->ctrl_bar) lv_obj_add_flag(ctx->ctrl_bar, LV_OBJ_FLAG_HIDDEN);
    }
}

static void on_prev_click(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    advance_verse(ctx, -1);
}
static void on_next_click(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    advance_verse(ctx, 1);
}
static void on_play_pause_click(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    ctx->auto_paused = !ctx->auto_paused;
    if (ctx->btn_play_pause) {
        lv_obj_t* lbl = lv_obj_get_child(ctx->btn_play_pause, 0);
        if (lbl) lv_label_set_text(lbl, ctx->auto_paused ? LV_SYMBOL_PLAY : LV_SYMBOL_PAUSE);
    }
}
static void on_highlight_click(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    toggle_favorite(ctx);
    // refresh highlight visuals without reloading
    show_current_verse(ctx);
}
static void on_close_click(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    (void)ctx;
    tt_app_stop();
}
// Future hooks merged into books click
static void on_books_click(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    toggle_book_browser(ctx);
}

static void on_title_click(lv_event_t* e) {
    AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
    // Tapping title when chrome visible opens books
    if (ctx->ui_visible) toggle_book_browser(ctx);
}

/* ── App lifecycle ── */
static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
    memset(&g_ctx, 0, sizeof(g_ctx));
    g_ctx.app = app;
    g_ctx.loaded_bnumber = -1;
    g_ctx.auto_paused = false;
    g_ctx.ui_visible = false;
    g_ctx.root = parent;

    char ud_path[MAX_PATH]; size_t sz=sizeof(ud_path);
    tt_app_get_user_data_path(app, ud_path, &sz);
    snprintf(g_ctx.progress_path, sizeof(g_ctx.progress_path), "%s/progress.txt", ud_path);
    snprintf(g_ctx.favorites_path, sizeof(g_ctx.favorites_path), "%s/favorites.txt", ud_path);

    // ── Load pretty serif fonts (embedded C - Georgia Italic like PSALMS reference) ──
    // Embedded fonts are in PSRAM via ELF LOADER LOAD PSRAM (CONFIG_ELF_LOADER_LOAD_PSRAM=y)
    // No VFS needed - direct pointers to static const lv_font_t
    {
        g_ctx.font_serif_26 = (lv_font_t*)&georgia_italic_26;
        g_ctx.font_serif_22 = (lv_font_t*)&georgia_italic_22;
        g_ctx.font_serif_20 = (lv_font_t*)&georgia_italic_20;
        g_ctx.font_serif_18 = (lv_font_t*)&georgia_italic_18;
        g_ctx.font_serif_16 = (lv_font_t*)&georgia_italic_16;
        g_ctx.font_book_24  = (lv_font_t*)&georgia_regular_24;
        g_ctx.fonts_loaded = true;
        ESP_LOGI(TAG, "Embedded serif fonts loaded: 26=%p 22=%p 20=%p 18=%p 16=%p book24=%p",
            g_ctx.font_serif_26, g_ctx.font_serif_22, g_ctx.font_serif_20, g_ctx.font_serif_18, g_ctx.font_serif_16, g_ctx.font_book_24);
    }

    resolve_bible_paths(&g_ctx);

    if (!load_books_manifest(&g_ctx)) {
        lv_obj_t* lbl = lv_label_create(parent);
        lv_label_set_text(lbl, "Bible data not found.\n\nExpected:\n assets/bible/books.json\n or /sdcard/bibles/\n\nRun tools/convert_bible.py");
        lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
        lv_label_set_long_mode(lbl, LV_LABEL_LONG_MODE_WRAP);
        lv_obj_set_width(lbl, LV_PCT(80));
        return;
    }

    if (!load_progress(&g_ctx)) g_ctx.cur_global = 0;
    if (g_ctx.cur_global <0 || g_ctx.cur_global >= g_ctx.total_verses) g_ctx.cur_global=0;

    // Deep paper dark - eye friendly for meditation
    lv_obj_set_style_bg_color(parent, lv_color_hex(0x0E0E14), 0);
    lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0);
    lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_set_style_pad_all(parent, 0, 0);

    // Full screen tap area
    g_ctx.verse_wrapper = lv_obj_create(parent);
    lv_obj_set_size(g_ctx.verse_wrapper, LV_PCT(100), LV_PCT(100));
    lv_obj_align(g_ctx.verse_wrapper, LV_ALIGN_CENTER, 0, 0);
    lv_obj_set_style_bg_opa(g_ctx.verse_wrapper, LV_OPA_TRANSP, 0);
    lv_obj_set_style_border_width(g_ctx.verse_wrapper, 0, 0);
    lv_obj_set_style_pad_all(g_ctx.verse_wrapper, 0, 0);
    lv_obj_remove_flag(g_ctx.verse_wrapper, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_add_flag(g_ctx.verse_wrapper, LV_OBJ_FLAG_CLICKABLE);
    lv_obj_add_event_cb(g_ctx.verse_wrapper, on_verse_tap, LV_EVENT_CLICKED, &g_ctx);

    // Center reading column with generous breathing room - avoid top chrome crop
    lv_obj_t* center_col = lv_obj_create(g_ctx.verse_wrapper);
    g_ctx.center_col = center_col;
    lv_obj_set_size(center_col, LV_PCT(90), LV_PCT(82)); // 80->82 but with top-safe padding
    lv_obj_align(center_col, LV_ALIGN_CENTER, 0, -7); // up 15px: was +8 base, now -7 (user corrected 30->15)
    lv_obj_set_style_bg_opa(center_col, LV_OPA_TRANSP, 0);
    lv_obj_set_style_border_width(center_col, 0, 0);
    lv_obj_set_flex_flow(center_col, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_flex_align(center_col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    lv_obj_set_style_pad_gap(center_col, 10, 0); // tightened: ref closer to verse (was 14)
    lv_obj_set_style_pad_all(center_col, 0, 0);
    lv_obj_remove_flag(center_col, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_add_flag(center_col, LV_OBJ_FLAG_CLICKABLE);
    lv_obj_add_event_cb(center_col, on_verse_tap, LV_EVENT_CLICKED, &g_ctx);

    g_ctx.lbl_verse = lv_label_create(center_col);
    lv_obj_set_width(g_ctx.lbl_verse, LV_PCT(100));
    lv_label_set_long_mode(g_ctx.lbl_verse, LV_LABEL_LONG_MODE_WRAP);
    lv_obj_set_style_text_color(g_ctx.lbl_verse, lv_color_hex(0xF2F0E8), 0); // warmer, editorial like reference image (was #E8E6E0)
    lv_obj_set_style_text_font(g_ctx.lbl_verse, lvgl_get_text_font(FONT_SIZE_DEFAULT), 0); // reduced base - Georgia italic wider than Montserrat
    lv_obj_set_style_text_align(g_ctx.lbl_verse, LV_TEXT_ALIGN_CENTER, 0);
    lv_obj_set_style_text_line_space(g_ctx.lbl_verse, 8, 0);
    lv_obj_set_style_text_letter_space(g_ctx.lbl_verse, 0, 0);
    lv_obj_set_style_radius(g_ctx.lbl_verse, 0, 0); // no bubble - flat editorial
    lv_obj_set_style_pad_all(g_ctx.lbl_verse, 4, 0); // less padding, avoid crop
    lv_label_set_text(g_ctx.lbl_verse, "Loading...");

    g_ctx.lbl_ref = lv_label_create(center_col);
    lv_obj_set_style_text_color(g_ctx.lbl_ref, lv_color_hex(0x9A9AA8), 0); // softer, like PSALMS citation gray
    lv_obj_set_style_text_font(g_ctx.lbl_ref, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
    lv_obj_set_style_text_align(g_ctx.lbl_ref, LV_TEXT_ALIGN_CENTER, 0);
    lv_obj_set_style_text_letter_space(g_ctx.lbl_ref, 2, 0); // tracking like reference image's PSALMS 23:1
    lv_label_set_text(g_ctx.lbl_ref, "");

    // ── Header bar (frosted dark) - hidden by default to avoid black bar crop ──
    // Fix black bar cropping text: make truly transparent, no border, same bg as root #0E0E14
    g_ctx.header_bar = lv_obj_create(parent);
    lv_obj_t* hb = g_ctx.header_bar;
    lv_obj_set_size(hb, LV_PCT(100), 36); // reduced 44->36 to avoid crop
    lv_obj_align(hb, LV_ALIGN_TOP_MID, 0, 0);
    lv_obj_set_style_radius(hb, 0, 0);
    lv_obj_set_style_bg_color(hb, lv_color_hex(0x0E0E14), 0); // same as root, not #15151F
    lv_obj_set_style_bg_opa(hb, LV_OPA_TRANSP, 0); // transparent to avoid black bar - was COVER
    lv_obj_set_style_border_width(hb, 0, 0);
    lv_obj_set_style_pad_hor(hb, 12, 0);
    lv_obj_set_style_pad_ver(hb, 4, 0);
    lv_obj_remove_flag(hb, LV_OBJ_FLAG_SCROLLABLE);

    g_ctx.lbl_book_title = lv_label_create(hb);
    lv_obj_align(g_ctx.lbl_book_title, LV_ALIGN_LEFT_MID, 0, 0);
    lv_obj_set_style_text_color(g_ctx.lbl_book_title, lv_color_hex(0xE8E6E0), 0);
    lv_obj_set_style_text_font(g_ctx.lbl_book_title, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
    lv_label_set_text(g_ctx.lbl_book_title, "Bible");
    lv_obj_add_flag(g_ctx.lbl_book_title, LV_OBJ_FLAG_CLICKABLE);
    lv_obj_add_event_cb(g_ctx.lbl_book_title, on_title_click, LV_EVENT_CLICKED, &g_ctx);

    g_ctx.lbl_position = lv_label_create(hb);
    lv_obj_align(g_ctx.lbl_position, LV_ALIGN_RIGHT_MID, 0, 0);
    lv_obj_set_style_text_color(g_ctx.lbl_position, lv_color_hex(0x6A6A8A), 0);
    lv_obj_set_style_text_font(g_ctx.lbl_position, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
    lv_label_set_text(g_ctx.lbl_position, "1 / 1");

    // ── Bottom controls (minimal) ──
    g_ctx.ctrl_bar = lv_obj_create(parent);
    lv_obj_t* cb = g_ctx.ctrl_bar;
    lv_obj_set_size(cb, LV_PCT(100), 56);
    lv_obj_align(cb, LV_ALIGN_BOTTOM_MID, 0, 0);
    lv_obj_set_style_radius(cb, 0, 0);
    lv_obj_set_style_bg_color(cb, lv_color_hex(0x0E0E14), 0);
    lv_obj_set_style_bg_opa(cb, LV_OPA_TRANSP, 0); // transparent editorial, was #15151F COVER
    lv_obj_set_style_border_width(cb, 0, 0);
    lv_obj_set_style_pad_all(cb, 6, 0);
    lv_obj_set_style_pad_gap(cb, 4, 0);
    lv_obj_set_flex_flow(cb, LV_FLEX_FLOW_ROW);
    lv_obj_set_flex_align(cb, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    lv_obj_remove_flag(cb, LV_OBJ_FLAG_SCROLLABLE);

    lv_obj_t* lbl;

    g_ctx.btn_close = lv_button_create(cb);
    lv_obj_set_size(g_ctx.btn_close, 38, 38);
    lv_obj_set_style_radius(g_ctx.btn_close, 10, 0);
    lv_obj_set_style_bg_color(g_ctx.btn_close, lv_color_hex(0x1E1E2E), 0);
    lv_obj_set_style_text_color(g_ctx.btn_close, lv_color_hex(0x8C8CB0), 0);
    lbl = lv_label_create(g_ctx.btn_close); lv_label_set_text(lbl, LV_SYMBOL_CLOSE); lv_obj_center(lbl);
    lv_obj_add_event_cb(g_ctx.btn_close, on_close_click, LV_EVENT_CLICKED, &g_ctx);

    g_ctx.btn_books = lv_button_create(cb);
    lv_obj_set_size(g_ctx.btn_books, 44, 38);
    lv_obj_set_style_radius(g_ctx.btn_books, 10, 0);
    lv_obj_set_style_bg_color(g_ctx.btn_books, lv_color_hex(0x252538), 0);
    lv_obj_set_style_text_color(g_ctx.btn_books, lv_color_hex(0xC4C4D4), 0);
    lbl = lv_label_create(g_ctx.btn_books); lv_label_set_text(lbl, LV_SYMBOL_LIST); lv_obj_center(lbl);
    lv_obj_add_event_cb(g_ctx.btn_books, on_books_click, LV_EVENT_CLICKED, &g_ctx);

    g_ctx.btn_prev = lv_button_create(cb);
    lv_obj_set_size(g_ctx.btn_prev, 44, 38);
    lv_obj_set_style_radius(g_ctx.btn_prev, 10, 0);
    lv_obj_set_style_bg_color(g_ctx.btn_prev, lv_color_hex(0x252538), 0);
    lv_obj_set_style_text_color(g_ctx.btn_prev, lv_color_hex(0xC4C4D4), 0);
    lbl = lv_label_create(g_ctx.btn_prev); lv_label_set_text(lbl, LV_SYMBOL_PREV); lv_obj_center(lbl);
    lv_obj_add_event_cb(g_ctx.btn_prev, on_prev_click, LV_EVENT_CLICKED, &g_ctx);

    g_ctx.btn_play_pause = lv_button_create(cb);
    lv_obj_set_size(g_ctx.btn_play_pause, 56, 38);
    lv_obj_set_style_radius(g_ctx.btn_play_pause, 19, 0);
    lv_obj_set_style_bg_color(g_ctx.btn_play_pause, lv_color_hex(0x2A2A4A), 0);
    lv_obj_set_style_text_color(g_ctx.btn_play_pause, lv_color_hex(0xE8E6E0), 0);
    lbl = lv_label_create(g_ctx.btn_play_pause); lv_label_set_text(lbl, LV_SYMBOL_PAUSE); lv_obj_center(lbl);
    lv_obj_add_event_cb(g_ctx.btn_play_pause, on_play_pause_click, LV_EVENT_CLICKED, &g_ctx);

    g_ctx.btn_next = lv_button_create(cb);
    lv_obj_set_size(g_ctx.btn_next, 44, 38);
    lv_obj_set_style_radius(g_ctx.btn_next, 10, 0);
    lv_obj_set_style_bg_color(g_ctx.btn_next, lv_color_hex(0x252538), 0);
    lv_obj_set_style_text_color(g_ctx.btn_next, lv_color_hex(0xC4C4D4), 0);
    lbl = lv_label_create(g_ctx.btn_next); lv_label_set_text(lbl, LV_SYMBOL_NEXT); lv_obj_center(lbl);
    lv_obj_add_event_cb(g_ctx.btn_next, on_next_click, LV_EVENT_CLICKED, &g_ctx);

    g_ctx.btn_highlight = lv_button_create(cb);
    lv_obj_set_size(g_ctx.btn_highlight, 38, 38);
    lv_obj_set_style_radius(g_ctx.btn_highlight, 10, 0);
    lv_obj_set_style_bg_color(g_ctx.btn_highlight, lv_color_hex(0x252538), 0);
    lv_obj_set_style_text_color(g_ctx.btn_highlight, lv_color_hex(0x8C8CB0), 0);
    lbl = lv_label_create(g_ctx.btn_highlight); lv_label_set_text(lbl, LV_SYMBOL_DUMMY); lv_obj_center(lbl);
    lv_obj_add_event_cb(g_ctx.btn_highlight, on_highlight_click, LV_EVENT_CLICKED, &g_ctx);

    set_chrome_visible(&g_ctx, true); // start with menu showing per user request
    schedule_chrome_auto_hide(&g_ctx, 10000); // auto hide after 10s
    show_current_verse(&g_ctx);

    // Build book browser overlay (on top of everything)
    build_book_browser(&g_ctx, parent);

    g_ctx.auto_timer = lv_timer_create(on_auto_timer, AUTO_ADV_MS, &g_ctx);
}

static void onHideApp(AppHandle app, void* data) {
    (void)app; (void)data;
    if (g_ctx.auto_timer) {
        lv_timer_delete(g_ctx.auto_timer);
        g_ctx.auto_timer = NULL;
    }
    if (g_ctx.chrome_auto_hide_timer) {
        lv_timer_delete(g_ctx.chrome_auto_hide_timer);
        g_ctx.chrome_auto_hide_timer = NULL;
    }
    // Embedded fonts are static const - no free needed (live in PSRAM ELF)
    save_progress(&g_ctx);
    unload_book(&g_ctx);
    // UI objects are deleted by LVGL parent cleanup
    memset(&g_ctx, 0, sizeof(g_ctx));
    g_ctx.loaded_bnumber = -1;
}

int main(int argc, char* argv[]) {
    tt_app_register((AppRegistration){
        .onShow = onShowApp,
        .onHide = onHideApp
    });
    return 0;
}
