// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3

#include "balance_tab.h"
#include "balance_address_list.h"
#include "balance_tab_helpers.h"
#include "balance_recent_tx.h"
#include "balance_components.h"
#include "mining_tab_helpers.h"  // FormatHashrate (consistent MH/GH/.. scaling)
#include "key_export_dialog.h"
#include "qr_popup_dialog.h"
#include "address_label_dialog.h"
#include "address_transfer_dialog.h"
#include "send_tab.h"
#include "../../app.h"
#include "../../config/settings.h"
#include "../../config/version.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../theme.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../effects/imgui_acrylic.h"
#include "../sidebar.h"
#include "../notifications.h"
#include "../staleness_badge.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <toml++/toml.hpp>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <ctime>
#include <cmath>
#include "../../util/logger.h"

namespace dragonx {
namespace ui {

// Animated balance state — lerps smoothly toward target. Non-static (declared extern in
// balance_components.h) so the shared components in balance_components.cpp share the same values.
double s_dispTotal       = 0.0;
double s_dispShielded    = 0.0;
double s_dispTransparent = 0.0;
double s_dispUnconfirmed = 0.0;

// Forward declarations for all layout functions
static void RenderBalanceClassic(App* app);
static void RenderBalanceDonut(App* app);
static void RenderBalanceConsolidated(App* app);
static void RenderBalanceDashboard(App* app);
static void RenderBalanceVerticalStack(App* app);
static void RenderBalanceVertical2x2(App* app);
static void RenderBalanceShield(App* app);
static void RenderBalanceTimeline(App* app);
static void RenderBalanceTwoRow(App* app);
static void RenderBalanceMinimal(App* app);

// ============================================================================
// Layout config — parsed from ui.toml [tabs.balance.layouts]
// ============================================================================

// Legacy int→string ID mapping for old settings.json migration
static const char* s_legacyLayoutIds[] = {
    "classic", "donut", "consolidated", "dashboard",
    "vertical-stack", "vertical-2x2", "shield", "timeline", "two-row", "minimal"
};
static constexpr int s_legacyLayoutCount = 10;

static std::vector<BalanceLayoutEntry> s_balanceLayouts;
static std::string s_defaultLayoutId = "classic";
static bool s_layoutConfigLoaded = false;
bool s_generating_z_address = false;  // external linkage — shared with balance_components.cpp

static void LoadBalanceLayoutConfig()
{
    s_balanceLayouts.clear();

    const void* elem = schema::UI().findElement("tabs.balance", "layouts");
    if (elem) {
        const auto& t = *static_cast<const toml::table*>(elem);

        if (auto selected = t["selected"].value<std::string>())
            s_defaultLayoutId = *selected;
        else if (auto def = t["default"].value<std::string>())
            s_defaultLayoutId = *def;

        if (auto* options = t["options"].as_array()) {
            for (auto& item : *options) {
                auto* opt = item.as_table();
                if (!opt) continue;
                auto id = (*opt)["id"].value<std::string>();
                auto name = (*opt)["name"].value<std::string>();
                if (!id || !name) continue;

                BalanceLayoutEntry entry;
                entry.id = *id;
                entry.name = *name;
                entry.enabled = (*opt)["enabled"].value_or(true);
                s_balanceLayouts.push_back(std::move(entry));
            }
        }
    }

    // Fallback if ui.toml had no layouts defined
    if (s_balanceLayouts.empty()) {
        for (int i = 0; i < s_legacyLayoutCount; i++) {
            BalanceLayoutEntry entry;
            entry.id = s_legacyLayoutIds[i];
            // Capitalize first letter for display name
            entry.name = entry.id;
            if (!entry.name.empty())
                entry.name[0] = (char)toupper((unsigned char)entry.name[0]);
            s_balanceLayouts.push_back(std::move(entry));
        }
    }
    s_layoutConfigLoaded = true;
}

const std::vector<BalanceLayoutEntry>& GetBalanceLayouts()
{
    if (!s_layoutConfigLoaded) LoadBalanceLayoutConfig();
    return s_balanceLayouts;
}

const std::string& GetDefaultBalanceLayout()
{
    if (!s_layoutConfigLoaded) LoadBalanceLayoutConfig();
    return s_defaultLayoutId;
}

void RefreshBalanceLayoutConfig()
{
    s_layoutConfigLoaded = false;
}

std::string MigrateBalanceLayoutIndex(int index)
{
    if (index >= 0 && index < s_legacyLayoutCount)
        return s_legacyLayoutIds[index];
    return "classic";
}

// Layout ID → render function dispatch
using LayoutRenderFn = void(*)(App*);
struct LayoutDispatchEntry { const char* id; LayoutRenderFn fn; };
static const LayoutDispatchEntry s_layoutDispatch[] = {
    { "classic",        RenderBalanceClassic },
    { "donut",          RenderBalanceDonut },
    { "consolidated",   RenderBalanceConsolidated },
    { "dashboard",      RenderBalanceDashboard },
    { "vertical-stack", RenderBalanceVerticalStack },
    { "vertical-2x2",   RenderBalanceVertical2x2 },
    { "shield",         RenderBalanceShield },
    { "timeline",       RenderBalanceTimeline },
    { "two-row",        RenderBalanceTwoRow },
    { "minimal",        RenderBalanceMinimal },
};

void RenderBalanceTab(App* app)
{
    std::string layoutId = GetDefaultBalanceLayout();
    if (app->settings()) {
        std::string saved = app->settings()->getBalanceLayout();
        if (!saved.empty()) layoutId = saved;
    }

    // Left/Right arrows: cycle through enabled balance layouts
    // (skip when Ctrl is held — Ctrl+Arrow cycles themes instead)
    if (app->settings() && !ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
        bool cycleUp   = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
        bool cycleDown = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
        if (cycleUp || cycleDown) {
            const auto& layouts = GetBalanceLayouts();
            // Build list of enabled layout IDs
            std::vector<std::string> enabled;
            for (const auto& l : layouts)
                if (l.enabled) enabled.push_back(l.id);
            if (!enabled.empty()) {
                int cur = 0;
                for (int i = 0; i < (int)enabled.size(); i++) {
                    if (enabled[i] == layoutId) { cur = i; break; }
                }
                if (cycleUp)
                    cur = (cur - 1 + (int)enabled.size()) % (int)enabled.size();
                else
                    cur = (cur + 1) % (int)enabled.size();
                layoutId = enabled[cur];
                app->settings()->setBalanceLayout(layoutId);

                // Show toast with layout name
                const auto& allLayouts = GetBalanceLayouts();
                std::string displayName = layoutId;
                for (const auto& l : allLayouts) {
                    if (l.id == layoutId) { displayName = l.name; break; }
                }
                char layoutToast[128];
                snprintf(layoutToast, sizeof(layoutToast), TR("balance_layout_switched"), displayName.c_str());
                Notifications::instance().info(layoutToast);
            }
        }
    }

    // Dispatch by string ID
    for (const auto& entry : s_layoutDispatch) {
        if (layoutId == entry.id) {
            entry.fn(app);
            return;
        }
    }
    // Fallback to Classic
    RenderBalanceClassic(app);
}

// ============================================================================
// Layout 0: Classic (original 3-card layout)
// ============================================================================
static void RenderBalanceClassic(App* app)
{
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const auto syncBar    = S.drawElement("tabs.balance", "sync-bar");

    // Read layout properties from schema
    const float kBalanceLerpSpeed    = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
    const float kHeroPadTop          = S.drawElement("tabs.balance", "hero-pad-top").sizeOr(12.0f);

    const auto& state = app->state();

    // Responsive scale factors (recomputed every frame)
    ImVec2 contentAvail = ImGui::GetContentRegionAvail();
    const float hs = Layout::hScale(contentAvail.x);
    const float vs = Layout::vScale(contentAvail.y);
    const auto tier = Layout::currentTier(contentAvail.x, contentAvail.y);
    const float glassRound = Layout::glassRounding();
    const float cGap       = Layout::cardGap();
    const float dp         = Layout::dpiScale();

    // Lerp displayed balances toward actual values
    {
        float dt = ImGui::GetIO().DeltaTime;
        float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed;
        auto lerp = [](double& disp, double target, float dt, float spd) {
            double diff = target - disp;
            if (std::abs(diff) < 1e-9) { disp = target; return; }
            disp += diff * (double)(dt * spd);
            // Snap when very close
            if (std::abs(target - disp) < 1e-9) disp = target;
        };
        lerp(s_dispTotal,       app->getTotalBalance(),       dt, speed);
        lerp(s_dispShielded,    app->getShieldedBalance(),    dt, speed);
        lerp(s_dispTransparent, app->getTransparentBalance(), dt, speed);
        lerp(s_dispUnconfirmed, state.unconfirmed_balance,    dt, speed);
    }

    // ================================================================
    // Card row — Total Balance | Shielded | Transparent | Market
    // ================================================================
    {
        float topMargin = S.drawElement("tabs.balance.classic", "top-margin").size;
        if (topMargin > 0.0f)
            ImGui::Dummy(ImVec2(0, topMargin));
        else if (topMargin < 0.0f) {
            // auto: use hero-pad-top scaled by vertical factor
            float autoPad = kHeroPadTop * vs;
            if (autoPad > 0.0f)
                ImGui::Dummy(ImVec2(0, autoPad));
        }
        // topMargin == 0 → no spacing at all

        const float cardGap = cGap;
        float availWidth = ImGui::GetContentRegionAvail().x;

        // Responsive card columns: 4 normally, 2 in compact, 1 if very narrow
        int numCols = (int)S.drawElement("tabs.balance.classic", "card-num-cols").sizeOr(4.0f);
        if (tier == Layout::LayoutTier::Compact) {
            if (availWidth < S.drawElement("tabs.balance.classic", "card-narrow-width").sizeOr(400.0f) * Layout::dpiScale())
                numCols = (int)S.drawElement("tabs.balance.classic", "card-narrow-cols").sizeOr(1.0f);
            else
                numCols = (int)S.drawElement("tabs.balance.classic", "card-compact-cols").sizeOr(2.0f);
        }
        float cardWidth  = (availWidth - (float)(numCols - 1) * cardGap) / (float)numCols;

        ImDrawList* dl = ImGui::GetWindowDrawList();
        ImVec2 origin  = ImGui::GetCursorScreenPos();
        GlassPanelSpec cardSpec;
        cardSpec.rounding = glassRound;

        char buf[64];
        ImU32 greenCol = Success();
        ImU32 goldCol  = Warning();
        ImU32 amberCol = Warning();

        ImFont* ovFont  = Type().overline();
        ImFont* sub1    = Type().subtitle1();
        ImFont* capFont = Type().caption();

        float classicPadOverride = S.drawElement("tabs.balance.classic", "card-padding").size;
        float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg();

        // Card height: must fit the Market card's content (overline + price + 24h)
        const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f) * dp;
        const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f) * dp;
        const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f) * dp;
        float marketContentH = cardPadLg
                             + ovFont->LegacySize + ovGap
                             + sub1->LegacySize + 2.0f * dp
                             + capFont->LegacySize
                             + cardPadLg;
        float classicCardH = S.drawElement("tabs.balance.classic", "card-height").size;
        float cardH;
        if (classicCardH >= 0.0f) {
            // TOML override scaled by dp so it grows with DPI + user font scale
            cardH = std::max(classicCardH * dp, marketContentH);
        } else {
            float minH = S.drawElement("tabs.balance.classic", "card-min-height").sizeOr(70.0f) * dp;
            cardH = std::max(StatCardHeight(vs, minH), marketContentH);
        }

        // Helper: draw accent stripe on left edge, clipped to card rounded corners.
        // We draw a full-size rounded rect (left corners only) and clip it to the
        // stripe width so the shape itself follows the card rounding.
        const float accentW      = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
        auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) {
            dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true);
            dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding,
                              ImDrawFlags_RoundCornersLeft);
            dl->PopClipRect();
        };

        // Helper: compute card position given card index (0-3)
        auto cardPos = [&](int idx) -> ImVec2 {
            int col = idx % numCols;
            int row = idx / numCols;
            return ImVec2(origin.x + col * (cardWidth + cardGap),
                          origin.y + row * (cardH + cardGap));
        };

        // ---- Total Balance card ----
        {
            ImVec2 cMin = cardPos(0);
            ImVec2 cMax(cMin.x + cardWidth, cMin.y + cardH);
            DrawGlassPanel(dl, cMin, cMax, cardSpec);
            drawAccent(cMin, cMax, S.resolveColor("var(--accent-total)", OnSurface()));

            float cx = cMin.x + cardPadLg;
            float cy = cMin.y + cardPadLg;

            // Coin logo (small, top-right corner)
            ImTextureID logoTex = app->getCoinLogoTexture();
            if (logoTex != 0) {
                float logoSz = ovFont->LegacySize + sub1->LegacySize + 4.0f * dp;
                float logoX = cMax.x - cardPadLg - logoSz;
                float logoY = cMin.y + cardPadLg;
                dl->AddImage(logoTex,
                    ImVec2(logoX, logoY),
                    ImVec2(logoX + logoSz, logoY + logoSz),
                    ImVec2(0, 0), ImVec2(1, 1),
                    IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.classic", "logo-opacity").sizeOr(180.0f)));
            }

            std::string totalLabelUpper = TR("total_balance_label");
            std::transform(totalLabelUpper.begin(), totalLabelUpper.end(), totalLabelUpper.begin(),
                           [](unsigned char c){ return (char)std::toupper(c); });
            dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy),
                        OnSurfaceMedium(), totalLabelUpper.c_str());
            cy += ovFont->LegacySize + ovGap;

            snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
            dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurface(), buf);
            ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf);
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(cx + balSz.x + tickGap,
                               cy + sub1->LegacySize - capFont->LegacySize),
                        OnSurfaceMedium(), DRAGONX_TICKER);
            cy += sub1->LegacySize + valGap;

            // USD value
            {
                double usd_value = state.getBalanceUSD();
                if (usd_value > 0.0)
                    snprintf(buf, sizeof(buf), "$%.2f USD", usd_value);
                else
                    snprintf(buf, sizeof(buf), "$-.-- USD");
                dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
                            OnSurfaceDisabled(), buf);
            }
            cy += capFont->LegacySize + 2 * dp;

            // Sync progress or mining indicator (whichever fits)
            if (state.sync.syncing && state.sync.headers > 0) {
                float pct = static_cast<float>(state.sync.verification_progress) * 100.0f;
                snprintf(buf, sizeof(buf), TR("balance_syncing_pct"), pct);
                dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
                            Warning(), buf);

                // Thin sync bar at card bottom — clipped to card rounded corners
                float barH = (syncBar.height >= 0) ? syncBar.height : 3.0f;
                float prog = static_cast<float>(state.sync.verification_progress);
                if (prog > 1.0f) prog = 1.0f;
                float barTop = cMax.y - barH;
                // Clip to the bottom strip so the full-card rounded rect
                // curves exactly match the card's own rounded corners
                dl->PushClipRect(ImVec2(cMin.x, barTop), cMax, true);
                // Background track
                dl->AddRectFilled(cMin, cMax,
                    SurfaceOverlay(15), cardSpec.rounding);
                // Progress fill — additional horizontal clip
                float progRight = cMin.x + (cMax.x - cMin.x) * prog;
                dl->PushClipRect(ImVec2(cMin.x, barTop), ImVec2(progRight, cMax.y), true);
                dl->AddRectFilled(cMin, cMax,
                    WithAlpha(Warning(), 200), cardSpec.rounding);
                dl->PopClipRect();
                dl->PopClipRect();
            } else if (state.mining.generate) {
                float pulse = schema::UI().drawElement("animations", "pulse-base-normal").size
                    + schema::UI().drawElement("animations", "pulse-amp-normal").size
                    * (float)std::sin((double)ImGui::GetTime()
                    * schema::UI().drawElement("animations", "pulse-speed-normal").size);
                ImU32 mineCol = WithAlpha(Success(), (int)(255.0f * pulse));
                dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f),
                    S.drawElement("tabs.balance.classic", "mining-dot-radius").sizeOr(3.0f), mineCol);
                double hr = state.mining.localHashrate;
                // Leading indent clears the mining dot drawn at cx+4dp; the text
                // itself starts at cx+12dp so keep the two spaces for spacing parity.
                char mineFmt[64];
                snprintf(mineFmt, sizeof(mineFmt), "  %s", TR("balance_mining_rate"));
                snprintf(buf, sizeof(buf), mineFmt, FormatHashrate(hr).c_str());
                dl->AddText(capFont, capFont->LegacySize,
                            ImVec2(cx + 12 * dp, cy),
                            WithAlpha(Success(), 200), buf);
            } else {
                // Refresh-staleness badge (W6-2): connected, not syncing/mining, but the balance
                // hasn't refreshed in a while (a busy daemon can fail z_gettotalbalance without
                // dropping the whole connection). The node banner only covers full disconnects, so
                // this pill is the sole signal that the shown number may be out of date.
                StalenessBadge badge = evaluateStalenessBadge(
                    state.last_balance_update, std::time(nullptr), state.connected);
                if (badge.show) {
                    const bool err = (badge.severity == StalenessSeverity::Error);
                    ImU32 fg = err ? Error() : Warning();
                    ImU32 bg = WithAlpha(fg, 38);
                    ImU32 bd = WithAlpha(fg, 90);
                    snprintf(buf, sizeof(buf), "%s %s",
                             TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str());
                    ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd, ImVec2(4.0f * dp, 2.0f * dp));
                    // Hover → explain what stale means and how old the data actually is.
                    if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
                        Tooltip("%s", TR("data_stale_tooltip"));
                }
            }

            // Hover glow
            if (material::IsRectHovered(cMin, cMax)) {
                dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
                            cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
            }
        }

        // ---- Shielded card ----
        {
            ImVec2 cMin = cardPos(1);
            ImVec2 cMax(cMin.x + cardWidth, cMin.y + cardH);
            DrawGlassPanel(dl, cMin, cMax, cardSpec);
            drawAccent(cMin, cMax, WithAlpha(S.resolveColor("var(--accent-shielded)", Success()), 200));

            float cx = cMin.x + cardPadLg;
            float cy = cMin.y + cardPadLg;

            dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy),
                        OnSurfaceMedium(), TR("shielded"));
            cy += ovFont->LegacySize + ovGap;

            snprintf(buf, sizeof(buf), "%.8f", s_dispShielded);
            dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), greenCol, buf);
            ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf);
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(cx + balSz.x + tickGap,
                               cy + sub1->LegacySize - capFont->LegacySize),
                        OnSurfaceMedium(), DRAGONX_TICKER);
            cy += sub1->LegacySize + valGap;

            // Privacy ratio + address count
            {
                float privPct = (s_dispTotal > 1e-9)
                    ? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f;
                snprintf(buf, sizeof(buf), TR("baltab_pct_of_total_zaddr"),
                         privPct, (int)state.z_addresses.size());
                dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
                            WithAlpha(Success(), 160), buf);
            }

            // Unconfirmed badge (top-right corner)
            if (state.unconfirmed_balance > 0.0) {
                snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
                ImVec2 ts = capFont->CalcTextSizeA(
                    capFont->LegacySize, 10000, 0, buf);
                float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f) * dp;
                float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f) * dp;
                ImVec2 bMin(cMax.x - ts.x - bp * 3,
                            cMin.y + cardPadLg);
                ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp);
                dl->AddRectFilled(bMin, bMax,
                                  WithAlpha(Warning(), 40), br);
                dl->AddText(capFont, capFont->LegacySize,
                            ImVec2(bMin.x + bp, bMin.y + bp * 0.5f),
                            amberCol, buf);
            }

            // Hover glow + click to Receive
            if (material::IsRectHovered(cMin, cMax)) {
                dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
                            cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                if (ImGui::IsMouseClicked(0))
                    app->setCurrentPage(NavPage::Receive);
            }
        }

        // ---- Transparent card ----
        {
            ImVec2 cMin = cardPos(2);
            ImVec2 cMax(cMin.x + cardWidth, cMin.y + cardH);
            DrawGlassPanel(dl, cMin, cMax, cardSpec);
            drawAccent(cMin, cMax, WithAlpha(S.resolveColor("var(--accent-transparent)", Warning()), 200));

            float cx = cMin.x + cardPadLg;
            float cy = cMin.y + cardPadLg;

            dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy),
                        OnSurfaceMedium(), TR("transparent"));
            cy += ovFont->LegacySize + ovGap;

            snprintf(buf, sizeof(buf), "%.8f", s_dispTransparent);
            dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), goldCol, buf);
            ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf);
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(cx + balSz.x + tickGap,
                               cy + sub1->LegacySize - capFont->LegacySize),
                        OnSurfaceMedium(), DRAGONX_TICKER);
            cy += sub1->LegacySize + valGap;

            snprintf(buf, sizeof(buf), TR("baltab_t_addresses_count"),
                     (int)state.t_addresses.size());
            dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
                        OnSurfaceDisabled(), buf);

            // Hover glow + click to Receive
            if (material::IsRectHovered(cMin, cMax)) {
                dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
                            cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                if (ImGui::IsMouseClicked(0))
                    app->setCurrentPage(NavPage::Receive);
            }
        }

        // ---- Market card ----
        {
            ImVec2 cMin = cardPos(3);
            ImVec2 cMax(cMin.x + cardWidth, cMin.y + cardH);
            DrawGlassPanel(dl, cMin, cMax, cardSpec);
            drawAccent(cMin, cMax, S.resolveColor("var(--accent-action)", Primary()));

            float cx = cMin.x + cardPadLg;
            float cy = cMin.y + cardPadLg;

            // Price string (compute early to measure text width)
            const auto& market = state.market;
            if (market.price_usd > 0) {
                if (market.price_usd >= 0.01)
                    snprintf(buf, sizeof(buf), "$%.4f", market.price_usd);
                else if (market.price_usd >= 0.0001)
                    snprintf(buf, sizeof(buf), "$%.6f", market.price_usd);
                else
                    snprintf(buf, sizeof(buf), "$%.8f", market.price_usd);
            } else {
                snprintf(buf, sizeof(buf), "$--.--");
            }
            ImVec2 pSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf);
            ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, "USD");

            // Measure widest text line to determine sparkline left edge
            std::string marketLabel = TR("market");
            std::transform(marketLabel.begin(), marketLabel.end(), marketLabel.begin(),
                           [](unsigned char c){ return (char)std::toupper(c); });
            float textW = std::max(pSz.x + tickGap + usdSz.x,
                                   ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
            float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f) * dp;
            float sparkLeft = cx + textW + sparkGap;
            float sparkRight = cMax.x - cardPadLg;

            // Left side: label + price + 24h change
            dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy),
                        OnSurfaceMedium(), marketLabel.c_str());
            cy += ovFont->LegacySize + ovGap;

            dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy),
                        OnSurface(), buf);
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(cx + pSz.x + tickGap,
                               cy + sub1->LegacySize - capFont->LegacySize),
                        OnSurfaceMedium(), "USD");
            cy += sub1->LegacySize + valGap;

            // 24h change
            if (market.price_usd > 0) {
                bool pos = market.change_24h >= 0;
                ImU32 chgCol = pos ? Success()
                                   : Error();
                snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"),
                         pos ? "+" : "", market.change_24h);
                dl->AddText(capFont, capFont->LegacySize,
                            ImVec2(cx, cy), chgCol, buf);
            }

            // Right side: sparkline fills remaining card space
            if (market.price_history.size() >= 2 && sparkLeft < sparkRight) {
                float spTop = cMin.y + cardPadLg;
                float spBot = cMax.y - cardPadLg;
                ImVec2 spMin(sparkLeft, spTop);
                ImVec2 spMax(sparkRight, spBot);
                ImU32 lineCol = market.change_24h >= 0
                    ? WithAlpha(Success(), 200)
                    : WithAlpha(Error(), 200);
                DrawSparkline(dl, spMin, spMax,
                              market.price_history, lineCol);
            }

            // Hover glow + click to Market
            if (material::IsRectHovered(cMin, cMax)) {
                dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
                            cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                if (ImGui::IsMouseClicked(0))
                    app->setCurrentPage(NavPage::Market);
            }
        }

        // Advance cursor past the card row(s)
        {
            int totalCards = 4;
            int numRows = (totalCards + numCols - 1) / numCols;
            ImGui::Dummy(ImVec2(availWidth, cardH * numRows + cardGap * (numRows - 1)));
        }
        ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
    }

    // ================================================================
    // Address list + Recent transactions — shared renderers (feature-complete:
    // set-label / add-to-portfolio context items, custom address icons, drag-to-reorder,
    // keyboard nav, copy-flash, i18n). Classic keeps its own address-table-height override
    // (see tabs.balance.classic) so the card sizing is unchanged.
    // ================================================================
    {
        float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
        float classicAddrH = S.drawElement("tabs.balance.classic", "address-table-height").size;
        float addrH = (classicAddrH >= 0.0f) ? classicAddrH * dp
            : ImGui::GetContentRegionAvail().y - recentReserve
                      - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
        RenderSharedAddressList(app, addrH, contentAvail.x, glassRound, hs, vs, recentReserve);
        RenderSharedRecentTx(app, recentReserve, contentAvail.x, hs, vs);
    }
}

// Shared helpers (UpdateBalanceLerp / RenderCompactHero / RenderSharedAddressList /
// RenderSharedRecentTx / RenderSyncBar) now live in balance_components.{h,cpp}.

// ============================================================================
// Layout 1: Donut Chart
// ============================================================================
static void RenderBalanceDonut(App* app) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const auto& state = app->state();
    UpdateBalanceLerp(app);

    ImVec2 contentAvail = ImGui::GetContentRegionAvail();
    float availW = contentAvail.x;
    float hs = Layout::hScale(availW);
    float vs = Layout::vScale(contentAvail.y);
    float glassRound = Layout::glassRounding();
    float cGap = Layout::cardGap();
    const float dp = Layout::dpiScale();
    ImDrawList* dl = ImGui::GetWindowDrawList();
    char buf[64];

    // --- Hero: total balance ---
    float donutTopMargin = S.drawElement("tabs.balance.donut", "top-margin").size;
    if (donutTopMargin >= 0.0f)
        ImGui::Dummy(ImVec2(0, donutTopMargin));
    else
        ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.donut", "hero-pad-ratio").sizeOr(8.0f) * vs));
    {
        Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
        ImGui::Dummy(ImVec2(0, 2 * dp));
        snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
        ImFont* heroFont = Type().h2();
        ImVec2 pos = ImGui::GetCursorScreenPos();
        DrawTextShadow(dl, heroFont, heroFont->LegacySize, pos, OnSurface(), buf);
        ImVec2 heroSize = heroFont->CalcTextSizeA(heroFont->LegacySize, 10000.0f, 0.0f, buf);
        ImGui::Dummy(heroSize);
        ImGui::SameLine();
        ImFont* capFont = Type().caption();
        float tickerY = pos.y + heroSize.y - capFont->LegacySize;
        dl->AddText(capFont, capFont->LegacySize,
                    ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
                    OnSurfaceMedium(), DRAGONX_TICKER);
        ImGui::NewLine();

        // USD value
        double usd_value = state.getBalanceUSD();
        if (usd_value > 0.0) snprintf(buf, sizeof(buf), "$%.2f USD", usd_value);
        else snprintf(buf, sizeof(buf), "$-.-- USD");
        Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
    }
    RenderSyncBar(app, dl, vs);
    ImGui::Dummy(ImVec2(0, cGap));

    // --- Donut + legend panel ---
    {
        ImFont* donutOv = Type().overline();
        ImFont* donutCap = Type().caption();
        // Font-content floor: legend needs overline + 3 caption lines + spacing
        float donutFontFloor = Layout::spacingLg() * 2
            + donutOv->LegacySize + Layout::spacingMd()
            + donutCap->LegacySize * 3 + Layout::spacingSm() * 3;
        float donutCardH = S.drawElement("tabs.balance.donut", "card-height").size;
        float panelH;
        if (donutCardH >= 0.0f) {
            panelH = std::max(donutCardH * dp, donutFontFloor);
        } else {
            panelH = std::max({donutFontFloor,
                S.drawElement("tabs.balance.donut", "panel-min-height").sizeOr(80.0f) * dp,
                contentAvail.y * S.drawElement("tabs.balance.donut", "panel-height-ratio").sizeOr(0.20f)});
        }
        ImVec2 panelMin = ImGui::GetCursorScreenPos();
        ImVec2 panelMax(panelMin.x + availW, panelMin.y + panelH);
        GlassPanelSpec spec;
        spec.rounding = glassRound;
        DrawGlassPanel(dl, panelMin, panelMax, spec);

        float donutPadOverride = S.drawElement("tabs.balance.donut", "card-padding").size;
        float donutPad = (donutPadOverride >= 0.0f) ? donutPadOverride : Layout::spacingLg();

        // Donut ring
        float cx = panelMin.x + panelH * 0.5f + donutPad;
        float cy = panelMin.y + panelH * 0.5f;
        float radius = std::min(
            panelH * S.drawElement("tabs.balance.donut", "outer-radius-ratio").sizeOr(0.40f),
            availW * S.drawElement("tabs.balance.donut", "max-radius-ratio").sizeOr(0.12f));
        float innerRadius = radius * S.drawElement("tabs.balance.donut", "inner-radius-ratio").sizeOr(0.6f);

        float total = (float)s_dispTotal;
        float shielded = (float)s_dispShielded;
        float ratio = (total > 1e-9f) ? shielded / total : 0.5f;

        // Shielded arc (green)
        float startAngle = -IM_PI * 0.5f; // top
        float shieldEnd = startAngle + 2.0f * IM_PI * ratio;
        if (ratio > 0.01f) {
            dl->PathClear();
            dl->PathArcTo(ImVec2(cx, cy), radius, startAngle, shieldEnd, 32);
            dl->PathArcTo(ImVec2(cx, cy), innerRadius, shieldEnd, startAngle, 32);
            dl->PathFillConvex(WithAlpha(Success(), 180));
        }
        // Transparent arc (gold)
        if (ratio < 0.99f) {
            dl->PathClear();
            dl->PathArcTo(ImVec2(cx, cy), radius, shieldEnd, startAngle + 2.0f * IM_PI, 32);
            dl->PathArcTo(ImVec2(cx, cy), innerRadius, startAngle + 2.0f * IM_PI, shieldEnd, 32);
            dl->PathFillConvex(WithAlpha(Warning(), 180));
        }

        // Center text: privacy %
        float privPct = ratio * 100.0f;
        snprintf(buf, sizeof(buf), "%.0f%%", privPct);
        ImFont* sub1 = Type().subtitle1();
        ImVec2 pctSz = sub1->CalcTextSizeA(sub1->LegacySize, 1000, 0, buf);
        dl->AddText(sub1, sub1->LegacySize,
                    ImVec2(cx - pctSz.x * 0.5f, cy - pctSz.y * 0.5f),
                    OnSurface(), buf);

        // Legend (right side)
        float legendX = panelMin.x + panelH + donutPad * 2;
        float legendY = panelMin.y + donutPad;
        ImFont* capFont = Type().caption();
        ImFont* body2 = Type().body2();

        float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f) * dp;
        float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f) * dp;
        float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f) * dp;
        float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f) * dp;

        // Shielded legend
        dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success());
        snprintf(buf, sizeof(buf), TR("baltab_shielded_amount"), s_dispShielded);
        dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf);
        legendY += capFont->LegacySize + legendLineGap;

        // Transparent legend
        dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning());
        snprintf(buf, sizeof(buf), TR("baltab_transparent_amount"), s_dispTransparent);
        dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf);
        legendY += capFont->LegacySize + legendSectionGap;

        // Market price
        const auto& market = state.market;
        if (market.price_usd > 0) {
            if (market.price_usd >= 0.01)
                snprintf(buf, sizeof(buf), TR("baltab_market_price_4dp"), market.price_usd);
            else
                snprintf(buf, sizeof(buf), TR("baltab_market_price_8dp"), market.price_usd);
            dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
                        OnSurfaceMedium(), buf);
            legendY += capFont->LegacySize + 4 * dp;

            bool pos = market.change_24h >= 0;
            snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"), pos ? "+" : "", market.change_24h);
            dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
                        pos ? Success() : Error(), buf);
        }

        ImGui::Dummy(ImVec2(availW, panelH));
    }

    ImGui::Dummy(ImVec2(0, cGap));

    // --- Shared address list + recent tx ---
    float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
    float donutAddrOverride = S.drawElement("tabs.balance.donut", "address-table-height").size;
    float addrH = (donutAddrOverride >= 0.0f) ? donutAddrOverride * dp
        : ImGui::GetContentRegionAvail().y - recentReserve
                  - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
    RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
    RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
}

// ============================================================================
// Layout 2: Consolidated Card
// ============================================================================
static void RenderBalanceConsolidated(App* app) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const auto& state = app->state();
    UpdateBalanceLerp(app);

    ImVec2 contentAvail = ImGui::GetContentRegionAvail();
    float availW = contentAvail.x;
    float hs = Layout::hScale(availW);
    float vs = Layout::vScale(contentAvail.y);
    float glassRound = Layout::glassRounding();
    float cGap = Layout::cardGap();
    const float dp = Layout::dpiScale();
    ImDrawList* dl = ImGui::GetWindowDrawList();
    char buf[64];

    // Single consolidated card
    float consTopMargin = S.drawElement("tabs.balance.consolidated", "top-margin").size;
    if (consTopMargin >= 0.0f)
        ImGui::Dummy(ImVec2(0, consTopMargin));

    ImFont* heroFont = Type().h2();
    ImFont* sub1 = Type().subtitle1();
    ImFont* capFont = Type().caption();
    ImFont* ovFont = Type().overline();
    float consPadOverride = S.drawElement("tabs.balance.consolidated", "card-padding").size;
    float pad = (consPadOverride >= 0.0f) ? consPadOverride : Layout::spacingLg();
    // Font-content floor: pad + overline + hero + caption + subtitle + pad
    float consFontFloor = pad + ovFont->LegacySize + Layout::spacingSm()
        + heroFont->LegacySize + Layout::spacingSm()
        + capFont->LegacySize + Layout::spacingSm()
        + sub1->LegacySize + pad;
    float consCardH = S.drawElement("tabs.balance.consolidated", "card-height").size;
    float cardH;
    if (consCardH >= 0.0f) {
        cardH = std::max(consCardH * dp, consFontFloor);
    } else {
        cardH = std::max({consFontFloor,
            S.drawElement("tabs.balance.consolidated", "card-min-height").sizeOr(90.0f) * dp,
            contentAvail.y * S.drawElement("tabs.balance.consolidated", "card-height-ratio").sizeOr(0.22f)});
    }
    ImVec2 cardMin = ImGui::GetCursorScreenPos();
    ImVec2 cardMax(cardMin.x + availW, cardMin.y + cardH);
    GlassPanelSpec spec;
    spec.rounding = glassRound;
    DrawGlassPanel(dl, cardMin, cardMax, spec);

    float cx = cardMin.x + pad;
    float cy = cardMin.y + pad;

    // Coin logo
    ImTextureID logoTex = app->getCoinLogoTexture();

    float logoSz = heroFont->LegacySize + capFont->LegacySize + 4.0f * dp;
    if (logoTex != 0) {
        dl->AddImage(logoTex,
            ImVec2(cx, cy), ImVec2(cx + logoSz, cy + logoSz),
            ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
        cx += logoSz + Layout::spacingMd();
    }

    // Total balance
    snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
    DrawTextShadow(dl, heroFont, heroFont->LegacySize, ImVec2(cx, cy), OnSurface(), buf);
    ImVec2 heroSz = heroFont->CalcTextSizeA(heroFont->LegacySize, 10000, 0, buf);
    dl->AddText(capFont, capFont->LegacySize,
                ImVec2(cx + heroSz.x + 4 * dp, cy + heroSz.y - capFont->LegacySize),
                OnSurfaceMedium(), DRAGONX_TICKER);
    cy += heroSz.y + 2 * dp;

    // USD value
    double usd_value = state.getBalanceUSD();
    if (usd_value > 0.0) snprintf(buf, sizeof(buf), "$%.2f USD", usd_value);
    else snprintf(buf, sizeof(buf), "$-.-- USD");
    dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);

    // Market badge (top-right)
    {
        const auto& market = state.market;
        if (market.price_usd > 0) {
            float badgeX = cardMax.x - pad;
            float badgeY = cardMin.y + pad;
            if (market.price_usd >= 0.01)
                snprintf(buf, sizeof(buf), "$%.4f", market.price_usd);
            else
                snprintf(buf, sizeof(buf), "$%.8f", market.price_usd);
            ImVec2 pSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(badgeX - pSz.x, badgeY), OnSurfaceMedium(), buf);
            badgeY += capFont->LegacySize + 2 * dp;

            bool pos = market.change_24h >= 0;
            snprintf(buf, sizeof(buf), "%s%.1f%%", pos ? "+" : "", market.change_24h);
            ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(badgeX - chgSz.x, badgeY),
                        pos ? Success() : Error(), buf);
        }
    }

    // Divider
    float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f);
    dl->AddLine(ImVec2(cardMin.x + pad, divY), ImVec2(cardMax.x - pad, divY),
                IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.consolidated", "divider-alpha").sizeOr(20.0f)),
                S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f) * dp);

    // Bottom half: proportion bars
    float barY = divY + Layout::spacingSm();
    float barH = std::max(
        S.drawElement("tabs.balance.consolidated", "bar-min-height").sizeOr(6.0f),
        S.drawElement("tabs.balance.consolidated", "bar-base-height").sizeOr(10.0f) * vs);
    float halfW = (availW - pad * 3) * 0.5f;
    float total = (float)s_dispTotal;
    float shieldRatio = (total > 1e-9f) ? (float)(s_dispShielded / total) : 0.5f;
    float transRatio = 1.0f - shieldRatio;

    // Shielded bar
    float shieldX = cardMin.x + pad;
    std::string shieldLabelUpper = TR("shielded");
    std::transform(shieldLabelUpper.begin(), shieldLabelUpper.end(), shieldLabelUpper.begin(),
                   [](unsigned char c){ return (char)std::toupper(c); });
    dl->AddText(ovFont, ovFont->LegacySize, ImVec2(shieldX, barY), Success(), shieldLabelUpper.c_str());
    barY += ovFont->LegacySize + 4 * dp;
    dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW, barY + barH),
                      SurfaceOverlay(15), barH * 0.5f);
    dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW * shieldRatio, barY + barH),
                      WithAlpha(Success(), 180), barH * 0.5f);
    barY += barH + 2 * dp;
    snprintf(buf, sizeof(buf), "%.8f (%.0f%%)", s_dispShielded, shieldRatio * 100.0f);
    dl->AddText(capFont, capFont->LegacySize, ImVec2(shieldX, barY), OnSurfaceMedium(), buf);

    // Transparent bar
    float transX = cardMin.x + pad * 2 + halfW;
    barY = divY + Layout::spacingSm();
    std::string transLabelUpper = TR("transparent");
    std::transform(transLabelUpper.begin(), transLabelUpper.end(), transLabelUpper.begin(),
                   [](unsigned char c){ return (char)std::toupper(c); });
    dl->AddText(ovFont, ovFont->LegacySize, ImVec2(transX, barY), Warning(), transLabelUpper.c_str());
    barY += ovFont->LegacySize + 4 * dp;
    dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW, barY + barH),
                      SurfaceOverlay(15), barH * 0.5f);
    dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW * transRatio, barY + barH),
                      WithAlpha(Warning(), 180), barH * 0.5f);
    barY += barH + 2 * dp;
    snprintf(buf, sizeof(buf), "%.8f (%.0f%%)", s_dispTransparent, transRatio * 100.0f);
    dl->AddText(capFont, capFont->LegacySize, ImVec2(transX, barY), OnSurfaceMedium(), buf);

    // Sync bar at card bottom — clipped to rounded corners
    if (state.sync.syncing && state.sync.headers > 0) {
        const auto syncBar = S.drawElement("tabs.balance", "sync-bar");
        float syncBarH = (syncBar.height >= 0) ? syncBar.height : 3.0f;
        float prog = static_cast<float>(state.sync.verification_progress);
        if (prog > 1.0f) prog = 1.0f;
        float syncBarTop = cardMax.y - syncBarH;
        // Clip to the bottom strip so the full-card rounded rect
        // curves exactly match the card's own rounded corners
        dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), cardMax, true);
        // Background track
        dl->AddRectFilled(cardMin, cardMax,
            SurfaceOverlay(15), glassRound);
        // Progress fill — additional horizontal clip
        float progRight = cardMin.x + (cardMax.x - cardMin.x) * prog;
        dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), ImVec2(progRight, cardMax.y), true);
        dl->AddRectFilled(cardMin, cardMax,
            WithAlpha(Warning(), 200), glassRound);
        dl->PopClipRect();
        dl->PopClipRect();
    }
    ImGui::Dummy(ImVec2(availW, cardH));
    ImGui::Dummy(ImVec2(0, cGap));

    float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
    float consAddrOverride = S.drawElement("tabs.balance.consolidated", "address-table-height").size;
    float addrH = (consAddrOverride >= 0.0f) ? consAddrOverride * dp
        : ImGui::GetContentRegionAvail().y - recentReserve
                  - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
    RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
    RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
}

// ============================================================================
// Layout 3: Dashboard Tiles
// ============================================================================
static void RenderBalanceDashboard(App* app) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const auto& state = app->state();
    UpdateBalanceLerp(app);

    ImVec2 contentAvail = ImGui::GetContentRegionAvail();
    float availW = contentAvail.x;
    float hs = Layout::hScale(availW);
    float vs = Layout::vScale(contentAvail.y);
    auto tier = Layout::currentTier(availW, contentAvail.y);
    float glassRound = Layout::glassRounding();
    float cGap = Layout::cardGap();
    const float dp = Layout::dpiScale();
    ImDrawList* dl = ImGui::GetWindowDrawList();
    char buf[64];

    // Compact hero line
    float dashTopMargin = S.drawElement("tabs.balance.dashboard", "top-margin").size;
    if (dashTopMargin >= 0.0f)
        ImGui::Dummy(ImVec2(0, dashTopMargin));
    float dashHeroH = S.drawElement("tabs.balance.dashboard", "hero-height").size;
    RenderCompactHero(app, dl, availW, hs, vs, dashHeroH);
    RenderSyncBar(app, dl, vs);
    ImGui::Dummy(ImVec2(0, cGap));

    // 4-tile grid
    int numCols = (tier == Layout::LayoutTier::Compact && availW < S.drawElement("tabs.balance.dashboard", "compact-cutoff").sizeOr(500.0f) * Layout::dpiScale())
        ? (int)S.drawElement("tabs.balance.dashboard", "tile-compact-cols").sizeOr(2.0f)
        : (int)S.drawElement("tabs.balance.dashboard", "tile-num-cols").sizeOr(4.0f);
    int numRows = (numCols == 2) ? 2 : 1;
    float tileW = (availW - (numCols - 1) * cGap) / numCols;
    ImFont* ovFont = Type().overline();
    ImFont* sub1 = Type().subtitle1();
    ImFont* capFont = Type().caption();
    // Font-content floor: pad + overline + subtitle1 + caption + pad
    float dashFontFloor = Layout::spacingLg()
        + ovFont->LegacySize + Layout::spacingSm()
        + sub1->LegacySize + Layout::spacingSm()
        + capFont->LegacySize
        + Layout::spacingLg();
    float dashCardH = S.drawElement("tabs.balance.dashboard", "card-height").size;
    float tileH;
    if (dashCardH >= 0.0f) {
        tileH = std::max(dashCardH * dp, dashFontFloor);
    } else {
        tileH = std::max({dashFontFloor,
            S.drawElement("tabs.balance.dashboard", "tile-min-height").sizeOr(70.0f) * dp,
            contentAvail.y * S.drawElement("tabs.balance.dashboard", "tile-height-ratio").sizeOr(0.16f) / numRows});
    }
    ImVec2 origin = ImGui::GetCursorScreenPos();

    GlassPanelSpec tileSpec;
    tileSpec.rounding = glassRound;

    struct TileInfo {
        const char* label;
        const char* value;
        ImU32 accent;
        const char* icon;
        NavPage nav;
        bool isAction;
    };

    snprintf(buf, sizeof(buf), "%.8f", s_dispShielded);
    static char shBuf[64], trBuf[64];
    snprintf(shBuf, sizeof(shBuf), "%.8f", s_dispShielded);
    snprintf(trBuf, sizeof(trBuf), "%.8f", s_dispTransparent);

    // Localized captions — raw dl->AddText (no auto-uppercase), so uppercase to preserve the caption look.
    auto upperTR = [](const char* key) {
        std::string s = TR(key);
        std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return (char)std::toupper(c); });
        return s;
    };
    std::string lblShielded = upperTR("shielded"), lblTransparent = upperTR("transparent");
    std::string lblQuickSend = upperTR("quick_send"), lblQuickReceive = upperTR("quick_receive");

    TileInfo tiles[4] = {
        {lblShielded.c_str(), shBuf, S.resolveColor("var(--accent-shielded)", Success()), ICON_MD_SHIELD, NavPage::Receive, false},
        {lblTransparent.c_str(), trBuf, S.resolveColor("var(--accent-transparent)", Warning()), ICON_MD_CIRCLE, NavPage::Receive, false},
        {lblQuickSend.c_str(), "Send", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_MADE, NavPage::Send, true},
        {lblQuickReceive.c_str(), "Receive", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_RECEIVED, NavPage::Receive, true},
    };

    for (int i = 0; i < 4; i++) {
        int col = (numCols == 4) ? i : (i % 2);
        int row = (numCols == 4) ? 0 : (i / 2);
        float xOff = col * (tileW + cGap);
        float yOff = row * (tileH + cGap);

        ImVec2 tMin(origin.x + xOff, origin.y + yOff);
        ImVec2 tMax(tMin.x + tileW, tMin.y + tileH);
        DrawGlassPanel(dl, tMin, tMax, tileSpec);

        // Accent stripe — clipped to tile rounded corners
        {
            float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
            dl->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true);
            dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding,
                              ImDrawFlags_RoundCornersLeft);
            dl->PopClipRect();
        }

        float dashPadOverride = S.drawElement("tabs.balance.dashboard", "card-padding").size;
        float tilePad = (dashPadOverride >= 0.0f) ? dashPadOverride : Layout::spacingLg();
        float tilePadV = (dashPadOverride >= 0.0f) ? dashPadOverride : Layout::spacingSm();

        float px = tMin.x + tilePad;
        float py = tMin.y + tilePadV;

        // Icon
        ImFont* iconFont = Type().iconSmall();
        ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, tiles[i].icon);
        dl->AddText(iconFont, iconFont->LegacySize,
                    ImVec2(px, py), tiles[i].accent, tiles[i].icon);
        px += iSz.x + Layout::spacingSm();

        // Label
        dl->AddText(ovFont, ovFont->LegacySize, ImVec2(px, py), OnSurfaceMedium(), tiles[i].label);
        py += ovFont->LegacySize + 4 * dp;

        // Value
        if (!tiles[i].isAction) {
            dl->AddText(capFont, capFont->LegacySize, ImVec2(tMin.x + tilePad, py),
                        tiles[i].accent, tiles[i].value);
        } else {
            dl->AddText(capFont, capFont->LegacySize, ImVec2(tMin.x + tilePad, py),
                        OnSurfaceMedium(), TR("tile_click_to_open"));
        }

        // Click
        if (material::IsRectHovered(tMin, tMax)) {
            dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
                        tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
            ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
            if (ImGui::IsMouseClicked(0))
                app->setCurrentPage(tiles[i].nav);
        }
    }

    float totalTileH = numRows * tileH + (numRows - 1) * cGap;
    ImGui::Dummy(ImVec2(availW, totalTileH));
    ImGui::Dummy(ImVec2(0, cGap));

    float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
    float dashAddrOverride = S.drawElement("tabs.balance.dashboard", "address-table-height").size;
    float addrH = (dashAddrOverride >= 0.0f) ? dashAddrOverride * dp
        : ImGui::GetContentRegionAvail().y - recentReserve
                  - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
    RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
    RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
}

// ============================================================================
// Layout 4: Vertical Stack
// ============================================================================
static void RenderBalanceVerticalStack(App* app) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const auto& state = app->state();
    UpdateBalanceLerp(app);

    ImVec2 contentAvail = ImGui::GetContentRegionAvail();
    float availW = contentAvail.x;
    float hs = Layout::hScale(availW);
    float vs = Layout::vScale(contentAvail.y);
    float glassRound = Layout::glassRounding();
    float cGap = Layout::cardGap();
    const float dp = Layout::dpiScale();
    ImDrawList* dl = ImGui::GetWindowDrawList();
    char buf[64];

    float vstackTopMargin = S.drawElement("tabs.balance.vertical-stack", "top-margin").size;
    if (vstackTopMargin >= 0.0f)
        ImGui::Dummy(ImVec2(0, vstackTopMargin));

    ImFont* capFont = Type().caption();
    ImFont* body2 = Type().body2();
    ImFont* sub1 = Type().subtitle1();
    // Font-content floor per row: icon + label + value must fit
    float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize)
        + Layout::spacingSm() * 2;
    float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f) * dp;
    float vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3;
    float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size;
    float stackH;
    if (vstackCardH >= 0.0f) {
        stackH = std::max(vstackCardH * dp, vstackFontFloor);
    } else {
        stackH = std::max({vstackFontFloor,
            S.drawElement("tabs.balance.vertical-stack", "stack-min-height").sizeOr(80.0f) * dp,
            contentAvail.y * S.drawElement("tabs.balance.vertical-stack", "stack-height-ratio").sizeOr(0.16f)});
    }
    float rowH = (stackH - 3 * rowGap) / 4.0f;
    float rowMinH = std::max(
        S.drawElement("tabs.balance.vertical-stack", "row-min-height").sizeOr(20.0f) * dp,
        vstackRowFontFloor);
    if (rowH < rowMinH) rowH = rowMinH;

    float total = (float)s_dispTotal;
    float shieldRatio = (total > 1e-9f) ? (float)(s_dispShielded / total) : 0.5f;
    float transRatio = 1.0f - shieldRatio;

    struct RowInfo {
        const char* label;
        const char* icon;
        ImU32 accent;
        double amount;
        float ratio;
    };

    RowInfo rowInfos[4] = {
        {TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
        {TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
        {TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
        {TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
    };

    for (int i = 0; i < 4; i++) {
        ImVec2 rowPos = ImGui::GetCursorScreenPos();
        ImVec2 rowEnd(rowPos.x + availW, rowPos.y + rowH);

        float vstackPadOverride = S.drawElement("tabs.balance.vertical-stack", "card-padding").size;
        float rowPad = (vstackPadOverride >= 0.0f) ? vstackPadOverride : Layout::spacingLg();

        // Subtle background
        float rowBgAlpha = S.drawElement("tabs.balance.vertical-stack", "row-bg-alpha").sizeOr(8.0f);
        dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, (int)rowBgAlpha), 4.0f * dp);

        // Left accent — clipped to row rounding
        dl->PushClipRect(rowPos, rowEnd, true);
        dl->AddRectFilled(ImVec2(rowPos.x, rowPos.y),
                          ImVec2(rowPos.x + 3 * dp, rowEnd.y),
                          rowInfos[i].accent);
        dl->PopClipRect();

        float px = rowPos.x + rowPad;
        float cy = rowPos.y + (rowH - capFont->LegacySize) * 0.5f;

        // Icon
        ImFont* iconFont = Type().iconSmall();
        ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, rowInfos[i].icon);
        dl->AddText(iconFont, iconFont->LegacySize,
                    ImVec2(px, rowPos.y + (rowH - iSz.y) * 0.5f),
                    rowInfos[i].accent, rowInfos[i].icon);
        px += iSz.x + Layout::spacingSm();

        // Label
        dl->AddText(capFont, capFont->LegacySize, ImVec2(px, cy),
                    OnSurfaceMedium(), rowInfos[i].label);

        // Amount (right side)
        if (i < 3) {
            snprintf(buf, sizeof(buf), "%.8f %s", rowInfos[i].amount, DRAGONX_TICKER);
        } else {
            if (state.market.price_usd >= 0.01)
                snprintf(buf, sizeof(buf), "$%.4f", state.market.price_usd);
            else if (state.market.price_usd > 0)
                snprintf(buf, sizeof(buf), "$%.8f", state.market.price_usd);
            else
                snprintf(buf, sizeof(buf), "$--.--");
        }
        ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
        dl->AddText(capFont, capFont->LegacySize,
                    ImVec2(rowEnd.x - amtSz.x - rowPad, cy),
                    i == 0 ? OnSurface() : rowInfos[i].accent, buf);

        // Proportion bar (for shielded/transparent rows — fills gap between label and amount)
        if (i == 1 || i == 2) {
            ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
            float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
            float barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
            float barH = std::max(
                S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f),
                rowH * S.drawElement("tabs.balance.vertical-stack", "bar-height-ratio").sizeOr(0.15f));
            float barLeft = px + labelSz.x + barGap;
            float barRight = rowEnd.x - amtSz.x - rowPad - barGap;
            if (barLeft < barRight) {
                float barW = barRight - barLeft;
                float barY = rowPos.y + (rowH - barH) * 0.5f;
                dl->AddRectFilled(ImVec2(barLeft, barY), ImVec2(barRight, barY + barH),
                                  SurfaceOverlay(15), barH * 0.5f);
                dl->AddRectFilled(ImVec2(barLeft, barY),
                                  ImVec2(barLeft + barW * rowInfos[i].ratio, barY + barH),
                                  WithAlpha(rowInfos[i].accent, 180), barH * 0.5f);
            }
        }

        // Market: 24h change + sparkline
        if (i == 3 && state.market.price_usd > 0) {
            bool pos = state.market.change_24h >= 0;
            snprintf(buf, sizeof(buf), "%s%.1f%%", pos ? "+" : "", state.market.change_24h);
            ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
            float chgX = rowEnd.x - amtSz.x - Layout::spacingLg() - chgSz.x - Layout::spacingSm();
            dl->AddText(capFont, capFont->LegacySize, ImVec2(chgX, cy),
                        pos ? Success() : Error(), buf);

            // Sparkline in the gap between label and 24h change
            if (state.market.price_history.size() >= 2) {
                ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
                float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
                float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f) * dp;
                float sparkLeft = px + labelSz.x + sparkGap;
                float sparkRight = chgX - sparkGap;
                if (sparkLeft < sparkRight) {
                    ImVec2 spMin(sparkLeft, rowPos.y + sparkPad);
                    ImVec2 spMax(sparkRight, rowEnd.y - sparkPad);
                    ImU32 lineCol = pos
                        ? WithAlpha(Success(), 200)
                        : WithAlpha(Error(), 200);
                    DrawSparkline(dl, spMin, spMax,
                                  state.market.price_history, lineCol);
                }
            }
        }

        ImGui::Dummy(ImVec2(availW, rowH));
        if (i < 3) ImGui::Dummy(ImVec2(0, rowGap));
    }

    RenderSyncBar(app, dl, vs);
    ImGui::Dummy(ImVec2(0, cGap));

    float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
    float vstackAddrOverride = S.drawElement("tabs.balance.vertical-stack", "address-table-height").size;
    float addrH = (vstackAddrOverride >= 0.0f) ? vstackAddrOverride * dp
        : ImGui::GetContentRegionAvail().y - recentReserve
                  - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
    RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
    RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
}

// ============================================================================
// Layout 5b: Vertical 2×2 (Total+Market left, Shielded+Transparent right)
// ============================================================================
static void RenderBalanceVertical2x2(App* app) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const auto& state = app->state();
    UpdateBalanceLerp(app);

    ImVec2 contentAvail = ImGui::GetContentRegionAvail();
    float availW = contentAvail.x;
    float hs = Layout::hScale(availW);
    float vs = Layout::vScale(contentAvail.y);
    float glassRound = Layout::glassRounding();
    float cGap = Layout::cardGap();
    const float dp = Layout::dpiScale();
    ImDrawList* dl = ImGui::GetWindowDrawList();
    char buf[64];

    const char* cfgSec = "tabs.balance.vertical-2x2";

    float topMargin = S.drawElement(cfgSec, "top-margin").size;
    if (topMargin >= 0.0f)
        ImGui::Dummy(ImVec2(0, topMargin));

    ImFont* capFont = Type().caption();
    ImFont* iconFont = Type().iconSmall();
    // Font-content floor per row: caption text + vertical padding
    float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2;
    float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f) * dp;
    float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f) * dp;
    float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap;
    float cardHOverride = S.drawElement(cfgSec, "card-height").size;
    float stackH;
    if (cardHOverride >= 0.0f) {
        stackH = std::max(cardHOverride * dp, v2x2FontFloor);
    } else {
        stackH = std::max({v2x2FontFloor,
            S.drawElement(cfgSec, "stack-min-height").sizeOr(60.0f) * dp,
            contentAvail.y * S.drawElement(cfgSec, "stack-height-ratio").sizeOr(0.12f)});
    }
    float rowH = (stackH - rowGap) / 2.0f;
    float rowMinH = std::max(
        S.drawElement(cfgSec, "row-min-height").sizeOr(24.0f) * dp,
        v2x2RowFontFloor);
    if (rowH < rowMinH) rowH = rowMinH;

    float colW = (availW - colGap) / 2.0f;

    float padOverride = S.drawElement(cfgSec, "card-padding").size;
    float rowPad = (padOverride >= 0.0f) ? padOverride : Layout::spacingLg();
    float rowBgAlpha = S.drawElement(cfgSec, "row-bg-alpha").sizeOr(8.0f);

    float total = (float)s_dispTotal;
    float shieldRatio = (total > 1e-9f) ? (float)(s_dispShielded / total) : 0.5f;
    float transRatio = 1.0f - shieldRatio;

    // Grid: [row][col] — row 0 top, row 1 bottom; col 0 left, col 1 right
    // Left col:  Total Balance (row 0), Market (row 1)
    // Right col: Shielded (row 0), Transparent (row 1)
    struct CellInfo {
        const char* label;
        const char* icon;
        ImU32 accent;
        double amount;
        float ratio;
        bool isMarket;
        bool hasBar;
    };

    CellInfo cells[2][2] = {
        // Row 0: Total Balance (left), Shielded (right)
        {
            {TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
            {TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
        },
        // Row 1: Market (left), Transparent (right)
        {
            {TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
            {TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
        },
    };

    ImVec2 gridOrigin = ImGui::GetCursorScreenPos();

    for (int row = 0; row < 2; row++) {
        for (int col = 0; col < 2; col++) {
            const auto& cell = cells[row][col];

            float cellX = gridOrigin.x + col * (colW + colGap);
            float cellY = gridOrigin.y + row * (rowH + rowGap);
            ImVec2 cellMin(cellX, cellY);
            ImVec2 cellMax(cellX + colW, cellY + rowH);

            // Background
            dl->AddRectFilled(cellMin, cellMax, IM_COL32(255, 255, 255, (int)rowBgAlpha), 4.0f * dp);

            // Left accent — clipped to cell rounding
            dl->PushClipRect(cellMin, cellMax, true);
            dl->AddRectFilled(ImVec2(cellMin.x, cellMin.y),
                              ImVec2(cellMin.x + 3 * dp, cellMax.y),
                              cell.accent);
            dl->PopClipRect();

            float px = cellMin.x + rowPad;
            float cy = cellMin.y + (rowH - capFont->LegacySize) * 0.5f;

            // Icon
            ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, cell.icon);
            dl->AddText(iconFont, iconFont->LegacySize,
                        ImVec2(px, cellMin.y + (rowH - iSz.y) * 0.5f),
                        cell.accent, cell.icon);
            px += iSz.x + Layout::spacingSm();

            // Label
            dl->AddText(capFont, capFont->LegacySize, ImVec2(px, cy),
                        OnSurfaceMedium(), cell.label);

            // Amount (right-aligned)
            if (!cell.isMarket) {
                snprintf(buf, sizeof(buf), "%.8f %s", cell.amount, DRAGONX_TICKER);
            } else {
                if (state.market.price_usd >= 0.01)
                    snprintf(buf, sizeof(buf), "$%.4f", state.market.price_usd);
                else if (state.market.price_usd > 0)
                    snprintf(buf, sizeof(buf), "$%.8f", state.market.price_usd);
                else
                    snprintf(buf, sizeof(buf), "$--.--");
            }
            ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(cellMax.x - amtSz.x - rowPad, cy),
                        cell.isMarket ? cell.accent : (row == 0 && col == 0 ? OnSurface() : cell.accent), buf);

            // Proportion bar (shielded/transparent)
            if (cell.hasBar) {
                float barW = colW * S.drawElement(cfgSec, "bar-width-ratio").sizeOr(0.12f);
                float barH = std::max(
                    S.drawElement(cfgSec, "bar-min-height").sizeOr(3.0f),
                    rowH * S.drawElement(cfgSec, "bar-height-ratio").sizeOr(0.15f));
                float barX = cellMax.x - amtSz.x - rowPad - barW - Layout::spacingSm();
                float barY = cellMin.y + (rowH - barH) * 0.5f;
                dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
                                  SurfaceOverlay(15), barH * 0.5f);
                dl->AddRectFilled(ImVec2(barX, barY),
                                  ImVec2(barX + barW * cell.ratio, barY + barH),
                                  WithAlpha(cell.accent, 180), barH * 0.5f);
            }

            // Market: 24h change + sparkline
            if (cell.isMarket && state.market.price_usd > 0) {
                bool pos = state.market.change_24h >= 0;
                snprintf(buf, sizeof(buf), "%s%.1f%%", pos ? "+" : "", state.market.change_24h);
                ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
                float chgX = cellMax.x - amtSz.x - Layout::spacingLg() - chgSz.x - Layout::spacingSm();
                dl->AddText(capFont, capFont->LegacySize, ImVec2(chgX, cy),
                            pos ? Success() : Error(), buf);

                // Sparkline between label and 24h change
                if (state.market.price_history.size() >= 2) {
                    ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label);
                    float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f) * dp;
                    float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f) * dp;
                    float sparkLeft = px + labelSz.x + sparkGap;
                    float sparkRight = chgX - sparkGap;
                    if (sparkLeft < sparkRight) {
                        ImVec2 spMin(sparkLeft, cellMin.y + sparkPad);
                        ImVec2 spMax(sparkRight, cellMax.y - sparkPad);
                        ImU32 lineCol = pos
                            ? WithAlpha(Success(), 200)
                            : WithAlpha(Error(), 200);
                        DrawSparkline(dl, spMin, spMax,
                                      state.market.price_history, lineCol);
                    }
                }
            }
        }
    }

    // Advance cursor past the 2×2 grid
    float totalGridH = 2.0f * rowH + rowGap;
    ImGui::Dummy(ImVec2(availW, totalGridH));

    RenderSyncBar(app, dl, vs);
    ImGui::Dummy(ImVec2(0, cGap));

    float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
    float addrOverride = S.drawElement(cfgSec, "address-table-height").size;
    float addrH = (addrOverride >= 0.0f) ? addrOverride * dp
        : ImGui::GetContentRegionAvail().y - recentReserve
                  - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
    RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
    RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
}

// ============================================================================
// Layout 5: Privacy Shield Meter
// ============================================================================
static void RenderBalanceShield(App* app) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const auto& state = app->state();
    UpdateBalanceLerp(app);

    ImVec2 contentAvail = ImGui::GetContentRegionAvail();
    float availW = contentAvail.x;
    float hs = Layout::hScale(availW);
    float vs = Layout::vScale(contentAvail.y);
    float glassRound = Layout::glassRounding();
    float cGap = Layout::cardGap();
    const float dp = Layout::dpiScale();
    ImDrawList* dl = ImGui::GetWindowDrawList();
    char buf[64];

    // Hero section
    float shieldTopMargin = S.drawElement("tabs.balance.shield", "top-margin").size;
    if (shieldTopMargin >= 0.0f)
        ImGui::Dummy(ImVec2(0, shieldTopMargin));
    else
        ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs));
    {
        Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
        ImGui::Dummy(ImVec2(0, 2 * dp));
        snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
        ImFont* heroFont = Type().h2();
        ImVec2 pos = ImGui::GetCursorScreenPos();
        DrawTextShadow(dl, heroFont, heroFont->LegacySize, pos, OnSurface(), buf);
        ImVec2 heroSz = heroFont->CalcTextSizeA(heroFont->LegacySize, 10000.0f, 0.0f, buf);
        ImGui::Dummy(heroSz);
        ImGui::SameLine();
        ImFont* capFont = Type().caption();
        dl->AddText(capFont, capFont->LegacySize,
                    ImVec2(ImGui::GetCursorScreenPos().x, pos.y + heroSz.y - capFont->LegacySize),
                    OnSurfaceMedium(), DRAGONX_TICKER);
        ImGui::NewLine();

        double usd_value = state.getBalanceUSD();
        if (usd_value > 0.0) snprintf(buf, sizeof(buf), "$%.2f USD", usd_value);
        else snprintf(buf, sizeof(buf), "$-.-- USD");
        Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
    }
    RenderSyncBar(app, dl, vs);
    ImGui::Dummy(ImVec2(0, cGap));

    // Shield gauge panel
    {
        ImFont* shieldCap = Type().caption();
        ImFont* shieldSub1 = Type().subtitle1();
        // Font-content floor: gauge area + legend text (subtitle + 2 captions)
        float shieldFontFloor = Layout::spacingLg() * 2
            + shieldSub1->LegacySize + Layout::spacingSm()
            + shieldCap->LegacySize * 2 + Layout::spacingSm() * 2;
        float shieldCardH = S.drawElement("tabs.balance.shield", "card-height").size;
        float gaugeH;
        if (shieldCardH >= 0.0f) {
            gaugeH = std::max(shieldCardH * dp, shieldFontFloor);
        } else {
            gaugeH = std::max({shieldFontFloor,
                S.drawElement("tabs.balance.shield", "gauge-min-height").sizeOr(80.0f) * dp,
                contentAvail.y * S.drawElement("tabs.balance.shield", "gauge-height-ratio").sizeOr(0.18f)});
        }
        ImVec2 panelMin = ImGui::GetCursorScreenPos();
        ImVec2 panelMax(panelMin.x + availW, panelMin.y + gaugeH);
        GlassPanelSpec spec;
        spec.rounding = glassRound;
        DrawGlassPanel(dl, panelMin, panelMax, spec);

        float total = (float)s_dispTotal;
        float privacyRatio = (total > 1e-9f) ? (float)(s_dispShielded / total) : 0.0f;
        float privPct = privacyRatio * 100.0f;

        // Semicircle gauge
        float gaugeCx = panelMin.x + gaugeH;
        float gaugeCy = panelMin.y + gaugeH * S.drawElement("tabs.balance.shield", "gauge-center-y-ratio").sizeOr(0.7f);
        float gaugeR = std::min(
            gaugeH * S.drawElement("tabs.balance.shield", "gauge-radius-ratio").sizeOr(0.55f),
            availW * S.drawElement("tabs.balance.shield", "gauge-max-radius-ratio").sizeOr(0.15f));
        float gaugeInnerR = gaugeR * S.drawElement("tabs.balance.shield", "gauge-inner-ratio").sizeOr(0.7f);

        // Background arc (gray)
        dl->PathClear();
        dl->PathArcTo(ImVec2(gaugeCx, gaugeCy), gaugeR, IM_PI, 2.0f * IM_PI, 32);
        dl->PathArcTo(ImVec2(gaugeCx, gaugeCy), gaugeInnerR, 2.0f * IM_PI, IM_PI, 32);
        dl->PathFillConvex(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.shield", "gauge-bg-alpha").sizeOr(20.0f)));

        // Filled arc (colored by threshold)
        ImU32 gaugeCol;
        float goodThreshold = S.drawElement("tabs.balance.shield", "good-threshold").sizeOr(80.0f);
        float medThreshold = S.drawElement("tabs.balance.shield", "medium-threshold").sizeOr(50.0f);
        if (privPct >= goodThreshold) gaugeCol = WithAlpha(Success(), 200);
        else if (privPct >= medThreshold) gaugeCol = WithAlpha(Warning(), 200);
        else gaugeCol = WithAlpha(Error(), 200);

        float fillEnd = IM_PI + IM_PI * privacyRatio;
        if (privacyRatio > 0.01f) {
            dl->PathClear();
            dl->PathArcTo(ImVec2(gaugeCx, gaugeCy), gaugeR, IM_PI, fillEnd, 32);
            dl->PathArcTo(ImVec2(gaugeCx, gaugeCy), gaugeInnerR, fillEnd, IM_PI, 32);
            dl->PathFillConvex(gaugeCol);
        }

        // Needle line
        float needleAngle = IM_PI + IM_PI * privacyRatio;
        float needleLen = gaugeR * 0.85f;
        ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen,
                         gaugeCy + sinf(needleAngle) * needleLen);
        dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol,
                    S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f) * dp);

        // Center text: percentage
        ImFont* sub1 = Type().subtitle1();
        snprintf(buf, sizeof(buf), "%.0f%%", privPct);
        ImVec2 pctSz = sub1->CalcTextSizeA(sub1->LegacySize, 1000, 0, buf);
        dl->AddText(sub1, sub1->LegacySize,
                    ImVec2(gaugeCx - pctSz.x * 0.5f, gaugeCy - pctSz.y - 2 * dp),
                    gaugeCol, buf);

        // Label below gauge
        ImFont* capFont = Type().caption();
        const char* statusMsg;
        if (privPct >= 80.0f) statusMsg = TR("privacy_great");
        else if (privPct >= 50.0f) statusMsg = TR("privacy_medium");
        else statusMsg = TR("privacy_low");
        ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, 1000, 0, statusMsg);
        dl->AddText(capFont, capFont->LegacySize,
                    ImVec2(gaugeCx - msgSz.x * 0.5f, gaugeCy + 4 * dp),
                    OnSurfaceMedium(), statusMsg);

        // Right side: balances + market
        float shieldPadOverride = S.drawElement("tabs.balance.shield", "card-padding").size;
        float shieldPad = (shieldPadOverride >= 0.0f) ? shieldPadOverride : Layout::spacingLg();
        float infoX = panelMin.x + gaugeH * 2 + shieldPad;
        float infoY = panelMin.y + shieldPad;
        ImFont* ovFont = Type().overline();

        std::string shieldLabelUpper = TR("shielded");
        std::transform(shieldLabelUpper.begin(), shieldLabelUpper.end(), shieldLabelUpper.begin(),
                       [](unsigned char c){ return (char)std::toupper(c); });
        std::string transLabelUpper = TR("transparent");
        std::transform(transLabelUpper.begin(), transLabelUpper.end(), transLabelUpper.begin(),
                       [](unsigned char c){ return (char)std::toupper(c); });
        dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Success(), shieldLabelUpper.c_str());
        infoY += ovFont->LegacySize + 2 * dp;
        snprintf(buf, sizeof(buf), "%.8f", s_dispShielded);
        dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Success(), buf);
        infoY += capFont->LegacySize + 6 * dp;

        dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Warning(), transLabelUpper.c_str());
        infoY += ovFont->LegacySize + 2 * dp;
        snprintf(buf, sizeof(buf), "%.8f", s_dispTransparent);
        dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Warning(), buf);
        infoY += capFont->LegacySize + 6 * dp;

        const auto& market = state.market;
        if (market.price_usd > 0) {
            if (market.price_usd >= 0.01)
                snprintf(buf, sizeof(buf), "$%.4f", market.price_usd);
            else
                snprintf(buf, sizeof(buf), "$%.8f", market.price_usd);
            dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY),
                        OnSurfaceMedium(), buf);
        }

        ImGui::Dummy(ImVec2(availW, gaugeH));
    }

    ImGui::Dummy(ImVec2(0, cGap));

    float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
    float shieldAddrOverride = S.drawElement("tabs.balance.shield", "address-table-height").size;
    float addrH = (shieldAddrOverride >= 0.0f) ? shieldAddrOverride * dp
        : ImGui::GetContentRegionAvail().y - recentReserve
                  - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
    RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
    RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
}

// ============================================================================
// Layout 6: Balance Timeline (placeholder — requires history tracking)
// ============================================================================
static void RenderBalanceTimeline(App* app) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const auto& state = app->state();
    UpdateBalanceLerp(app);

    ImVec2 contentAvail = ImGui::GetContentRegionAvail();
    float availW = contentAvail.x;
    float hs = Layout::hScale(availW);
    float vs = Layout::vScale(contentAvail.y);
    float glassRound = Layout::glassRounding();
    float cGap = Layout::cardGap();
    const float dp = Layout::dpiScale();
    ImDrawList* dl = ImGui::GetWindowDrawList();
    char buf[64];

    // Hero
    float tlTopMargin = S.drawElement("tabs.balance.timeline", "top-margin").size;
    if (tlTopMargin >= 0.0f)
        ImGui::Dummy(ImVec2(0, tlTopMargin));
    else
        ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs));
    {
        Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
        ImGui::Dummy(ImVec2(0, 2 * dp));
        snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
        ImFont* heroFont = Type().h2();
        ImVec2 pos = ImGui::GetCursorScreenPos();
        DrawTextShadow(dl, heroFont, heroFont->LegacySize, pos, OnSurface(), buf);
        ImVec2 heroSz = heroFont->CalcTextSizeA(heroFont->LegacySize, 10000.0f, 0.0f, buf);
        ImGui::Dummy(heroSz);
        ImGui::SameLine();
        ImFont* capFont = Type().caption();
        dl->AddText(capFont, capFont->LegacySize,
                    ImVec2(ImGui::GetCursorScreenPos().x, pos.y + heroSz.y - capFont->LegacySize),
                    OnSurfaceMedium(), DRAGONX_TICKER);
        ImGui::NewLine();

        double usd_value = state.getBalanceUSD();
        if (usd_value > 0.0) snprintf(buf, sizeof(buf), "$%.2f USD", usd_value);
        else snprintf(buf, sizeof(buf), "$-.-- USD");
        Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
    }
    RenderSyncBar(app, dl, vs);
    ImGui::Dummy(ImVec2(0, cGap));

    // Chart area placeholder
    {
        float tlChartH = S.drawElement("tabs.balance.timeline", "chart-height").size;
        float chartH;
        if (tlChartH >= 0.0f) {
            chartH = tlChartH * dp;  // scaled by dp for DPI + font scale
        } else {
            chartH = std::max(
                S.drawElement("tabs.balance.timeline", "chart-min-height").sizeOr(80.0f) * dp,
                contentAvail.y * S.drawElement("tabs.balance.timeline", "chart-height-ratio").sizeOr(0.20f));
        }
        ImVec2 chartMin = ImGui::GetCursorScreenPos();
        ImVec2 chartMax(chartMin.x + availW, chartMin.y + chartH);
        GlassPanelSpec spec;
        spec.rounding = glassRound;
        DrawGlassPanel(dl, chartMin, chartMax, spec);

        ImFont* capFont = Type().caption();
        const char* msg = TR("balance_history_collecting");
        ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, msg);
        dl->AddText(capFont, capFont->LegacySize,
                    ImVec2(chartMin.x + (availW - msgSz.x) * 0.5f,
                           chartMin.y + (chartH - msgSz.y) * 0.5f),
                    OnSurfaceDisabled(), msg);

        // If we have market sparkline data, use it as a preview
        const auto& market = state.market;
        if (market.price_history.size() >= 2) {
            float sparkPad = Layout::spacingLg();
            ImVec2 spMin(chartMin.x + sparkPad, chartMin.y + capFont->LegacySize + sparkPad * 2);
            ImVec2 spMax(chartMax.x - sparkPad, chartMax.y - sparkPad);
            if (spMax.y > spMin.y && spMax.x > spMin.x) {
                ImU32 lineCol = market.change_24h >= 0
                    ? WithAlpha(Success(), (int)S.drawElement("tabs.balance.timeline", "sparkline-alpha").sizeOr(120.0f))
                    : WithAlpha(Error(), (int)S.drawElement("tabs.balance.timeline", "sparkline-alpha").sizeOr(120.0f));
                DrawSparkline(dl, spMin, spMax, market.price_history, lineCol);
            }
        }

        ImGui::Dummy(ImVec2(availW, chartH));
    }

    // Compact 3 summary cards
    ImGui::Dummy(ImVec2(0, cGap));
    {
        ImFont* ovFont = Type().overline();
        ImFont* capFont = Type().caption();
        // Font-content floor: pad + overline + gap + caption + pad
        float tlPadVal = S.drawElement("tabs.balance.timeline", "card-padding").size;
        float tlPad = (tlPadVal >= 0.0f) ? tlPadVal : Layout::spacingXs();
        float tlFontFloor = tlPad + ovFont->LegacySize + 2.0f * dp + capFont->LegacySize + tlPad;
        float tlSummaryH = S.drawElement("tabs.balance.timeline", "summary-card-height").size;
        float cardH;
        if (tlSummaryH >= 0.0f) {
            cardH = std::max(tlSummaryH * dp, tlFontFloor);
        } else {
            cardH = std::max({tlFontFloor,
                S.drawElement("tabs.balance.timeline", "summary-min-height").sizeOr(44.0f) * dp,
                contentAvail.y * S.drawElement("tabs.balance.timeline", "summary-height-ratio").sizeOr(0.08f)});
        }
        float cardW = (availW - 2 * cGap) / 3.0f;
        ImVec2 origin = ImGui::GetCursorScreenPos();
        GlassPanelSpec spec;
        spec.rounding = glassRound;

        struct SumCard { const char* label; ImU32 col; double val; bool isMoney; };
        auto upperTR = [](const char* key) {
            std::string s = TR(key);
            std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return (char)std::toupper(c); });
            return s;
        };
        std::string cShielded = upperTR("shielded"), cTransparent = upperTR("transparent"), cMarket = upperTR("market");
        SumCard cards[3] = {
            {cShielded.c_str(), Success(), s_dispShielded, false},
            {cTransparent.c_str(), Warning(), s_dispTransparent, false},
            {cMarket.c_str(), Primary(), state.market.price_usd, true},
        };
        for (int i = 0; i < 3; i++) {
            ImVec2 cMin(origin.x + i * (cardW + cGap), origin.y);
            ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
            DrawGlassPanel(dl, cMin, cMax, spec);

            float tlPadOverride = S.drawElement("tabs.balance.timeline", "card-padding").size;
            float cx = cMin.x + ((tlPadOverride >= 0.0f) ? tlPadOverride : Layout::spacingSm());
            float cy = cMin.y + ((tlPadOverride >= 0.0f) ? tlPadOverride : Layout::spacingXs());
            dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), cards[i].label);
            cy += ovFont->LegacySize + 2 * dp;
            if (cards[i].isMoney) {
                if (cards[i].val >= 0.01) snprintf(buf, sizeof(buf), "$%.4f", cards[i].val);
                else if (cards[i].val > 0) snprintf(buf, sizeof(buf), "$%.8f", cards[i].val);
                else snprintf(buf, sizeof(buf), "$--.--");
            } else {
                snprintf(buf, sizeof(buf), "%.8f", cards[i].val);
            }
            dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), cards[i].col, buf);
        }
        ImGui::Dummy(ImVec2(availW, cardH));
    }

    ImGui::Dummy(ImVec2(0, cGap));

    float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
    float tlAddrOverride = S.drawElement("tabs.balance.timeline", "address-table-height").size;
    float addrH = (tlAddrOverride >= 0.0f) ? tlAddrOverride * dp
        : ImGui::GetContentRegionAvail().y - recentReserve
                  - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
    RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
    RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
}

// ============================================================================
// Layout 7: Two-Row Hero
// ============================================================================
static void RenderBalanceTwoRow(App* app) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const auto& state = app->state();
    UpdateBalanceLerp(app);

    ImVec2 contentAvail = ImGui::GetContentRegionAvail();
    float availW = contentAvail.x;
    float hs = Layout::hScale(availW);
    float vs = Layout::vScale(contentAvail.y);
    float glassRound = Layout::glassRounding();
    float cGap = Layout::cardGap();
    const float dp = Layout::dpiScale();
    ImDrawList* dl = ImGui::GetWindowDrawList();
    char buf[64];

    // Top margin
    float twoRowTopMargin = S.drawElement("tabs.balance.two-row", "top-margin").size;
    if (twoRowTopMargin >= 0.0f)
        ImGui::Dummy(ImVec2(0, twoRowTopMargin));

    // Row 1: logo + balance + USD + actions
    RenderCompactHero(app, dl, availW, hs, vs);

    // Sync + mining on same line
    {
        const auto& state = app->state();
        ImFont* capFont = Type().caption();
        if (state.sync.syncing && state.sync.headers > 0) {
            float pct = static_cast<float>(state.sync.verification_progress) * 100.0f;
            snprintf(buf, sizeof(buf), TR("balance_syncing_pct"), pct);
            Type().textColored(TypeStyle::Caption, Warning(), buf);
            ImGui::SameLine();
        }
        if (state.mining.generate) {
            double hr = state.mining.localHashrate;
            snprintf(buf, sizeof(buf), TR("balance_mining_rate"), FormatHashrate(hr).c_str());
            Type().textColored(TypeStyle::Caption, WithAlpha(Success(), 200), buf);
            ImGui::SameLine();
        }

        // Action buttons right-aligned
        float btnW = S.drawElement("tabs.balance.two-row", "action-btn-width").sizeOr(80.0f) * dp;
        float rightEdge = ImGui::GetWindowWidth() - Layout::spacingLg();
        ImGui::SameLine(rightEdge - btnW * 2 - Layout::spacingSm());
        // Stable ## ids keep the button identity fixed across translations.
        char sendBtn[64], recvBtn[64];
        snprintf(sendBtn, sizeof(sendBtn), "%s##tworow-send", TR("send"));
        snprintf(recvBtn, sizeof(recvBtn), "%s##tworow-receive", TR("receive"));
        if (TactileButton(sendBtn, ImVec2(btnW, 0), S.resolveFont("button"))) {
            app->setCurrentPage(NavPage::Send);
        }
        ImGui::SameLine();
        if (TactileButton(recvBtn, ImVec2(btnW, 0), S.resolveFont("button"))) {
            app->setCurrentPage(NavPage::Receive);
        }
    }

    RenderSyncBar(app, dl, vs);
    ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f) * dp));

    // Row 2: 3 mini-cards inline
    {
        ImFont* twoRowCap = Type().caption();
        // Font-content floor: caption centered + vertical padding
        float twoRowFontFloor = twoRowCap->LegacySize + Layout::spacingSm() * 2;
        float twoRowCardH = S.drawElement("tabs.balance.two-row", "card-height").size;
        float miniH;
        if (twoRowCardH >= 0.0f) {
            miniH = std::max(twoRowCardH * dp, twoRowFontFloor);
        } else {
            miniH = std::max({twoRowFontFloor,
                S.drawElement("tabs.balance.two-row", "mini-min-height").sizeOr(28.0f) * dp,
                S.drawElement("tabs.balance.two-row", "mini-base-height").sizeOr(36.0f) * vs});
        }
        float miniW = (availW - 2 * cGap) / 3.0f;
        ImVec2 origin = ImGui::GetCursorScreenPos();
        GlassPanelSpec spec;
        spec.rounding = std::max(
            S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f),
            glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f));
        ImFont* capFont = Type().caption();
        float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f) * dp;
        int balDecimals = (int)S.drawElement("tabs.balance.two-row", "balance-decimals").sizeOr(4.0f);
        float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size;
        float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm();

        // Shielded mini-card
        {
            ImVec2 cMin = origin;
            ImVec2 cMax(cMin.x + miniW, cMin.y + miniH);
            DrawGlassPanel(dl, cMin, cMax, spec);
            float cx = cMin.x + miniPad;
            float cy = cMin.y + (miniH - capFont->LegacySize) * 0.5f;
            dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f), indicatorR, Success());
            snprintf(buf, sizeof(buf), "%.*f", balDecimals, s_dispShielded);
            dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 12 * dp, cy), Success(), buf);

            // Percentage of total (right-aligned)
            float shieldPct = (s_dispTotal > 1e-9) ? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f;
            snprintf(buf, sizeof(buf), "%.0f%%", shieldPct);
            ImVec2 pctSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(cMax.x - pctSz.x - miniPad, cy),
                        WithAlpha(Success(), 160), buf);
        }

        // Transparent mini-card
        {
            ImVec2 cMin(origin.x + miniW + cGap, origin.y);
            ImVec2 cMax(cMin.x + miniW, cMin.y + miniH);
            DrawGlassPanel(dl, cMin, cMax, spec);
            float cx = cMin.x + miniPad;
            float cy = cMin.y + (miniH - capFont->LegacySize) * 0.5f;
            dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f), indicatorR, Warning());
            snprintf(buf, sizeof(buf), "%.*f", balDecimals, s_dispTransparent);
            dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 12 * dp, cy), Warning(), buf);

            // Percentage of total (right-aligned)
            float transPct = (s_dispTotal > 1e-9) ? (float)(s_dispTransparent / s_dispTotal * 100.0) : 0.0f;
            snprintf(buf, sizeof(buf), "%.0f%%", transPct);
            ImVec2 pctSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(cMax.x - pctSz.x - miniPad, cy),
                        WithAlpha(Warning(), 160), buf);
        }

        // Market mini-card
        {
            ImVec2 cMin(origin.x + 2 * (miniW + cGap), origin.y);
            ImVec2 cMax(cMin.x + miniW, cMin.y + miniH);
            DrawGlassPanel(dl, cMin, cMax, spec);
            float cx = cMin.x + miniPad;
            float cy = cMin.y + (miniH - capFont->LegacySize) * 0.5f;
            const auto& market = state.market;
            if (market.price_usd >= 0.01)
                snprintf(buf, sizeof(buf), "$%.4f", market.price_usd);
            else if (market.price_usd > 0)
                snprintf(buf, sizeof(buf), "$%.8f", market.price_usd);
            else
                snprintf(buf, sizeof(buf), "$--.--");
            ImVec2 priceSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
            dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), buf);

            float sparkRight = cMax.x - miniPad;
            if (market.price_usd > 0) {
                bool pos = market.change_24h >= 0;
                snprintf(buf, sizeof(buf), "%s%.1f%%", pos ? "+" : "", market.change_24h);
                ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
                float chgX = cMax.x - chgSz.x - miniPad;
                dl->AddText(capFont, capFont->LegacySize,
                            ImVec2(chgX, cy),
                            pos ? Success() : Error(), buf);
                sparkRight = chgX;

                // Sparkline between price and percentage
                if (market.price_history.size() >= 2) {
                    float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f) * dp;
                    float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f) * dp;
                    float sparkLeft = cx + priceSz.x + sparkGap;
                    float sparkRightEdge = sparkRight - sparkGap;
                    if (sparkLeft < sparkRightEdge) {
                        ImVec2 spMin(sparkLeft, cMin.y + sparkPad);
                        ImVec2 spMax(sparkRightEdge, cMax.y - sparkPad);
                        ImU32 lineCol = pos
                            ? WithAlpha(Success(), 200)
                            : WithAlpha(Error(), 200);
                        DrawSparkline(dl, spMin, spMax,
                                      market.price_history, lineCol);
                    }
                }
            }
        }

        ImGui::Dummy(ImVec2(availW, miniH));
    }

    ImGui::Dummy(ImVec2(0, cGap));

    float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
    float twoRowAddrOverride = S.drawElement("tabs.balance.two-row", "address-table-height").size;
    float addrH = (twoRowAddrOverride >= 0.0f) ? twoRowAddrOverride * dp
        : ImGui::GetContentRegionAvail().y - recentReserve
                  - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
    RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
    RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
}

// ============================================================================
// Layout 8: Minimal (card-less typography only)
// ============================================================================
static void RenderBalanceMinimal(App* app) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const auto& state = app->state();
    UpdateBalanceLerp(app);

    ImVec2 contentAvail = ImGui::GetContentRegionAvail();
    float availW = contentAvail.x;
    float hs = Layout::hScale(availW);
    float vs = Layout::vScale(contentAvail.y);
    float glassRound = Layout::glassRounding();
    const float dp = Layout::dpiScale();
    ImDrawList* dl = ImGui::GetWindowDrawList();
    char buf[64];

    // Top margin
    float minTopMargin = S.drawElement("tabs.balance.minimal", "top-margin").size;
    if (minTopMargin >= 0.0f)
        ImGui::Dummy(ImVec2(0, minTopMargin));

    ImFont* heroFont = Type().h2();
    ImFont* sub1 = Type().subtitle1();
    ImFont* capFont = Type().caption();

    // Line 1: Big balance + market price
    snprintf(buf, sizeof(buf), "%.8f %s", s_dispTotal, DRAGONX_TICKER);
    ImVec2 pos = ImGui::GetCursorScreenPos();
    DrawTextShadow(dl, heroFont, heroFont->LegacySize, pos, OnSurface(), buf);
    ImVec2 heroSz = heroFont->CalcTextSizeA(heroFont->LegacySize, 10000.0f, 0.0f, buf);

    // Market price right-aligned
    const auto& market = state.market;
    if (market.price_usd > 0) {
        if (market.price_usd >= 0.01)
            snprintf(buf, sizeof(buf), "$%.4f", market.price_usd);
        else
            snprintf(buf, sizeof(buf), "$%.8f", market.price_usd);
        ImVec2 pSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
        float rightEdge = pos.x + availW;
        dl->AddText(capFont, capFont->LegacySize,
                    ImVec2(rightEdge - pSz.x, pos.y + heroSz.y - capFont->LegacySize),
                    OnSurfaceMedium(), buf);
    }
    ImGui::Dummy(heroSz);

    // Line 2: Shielded + Transparent
    snprintf(buf, sizeof(buf), TR("balance_shielded_fmt"), s_dispShielded);
    Type().textColored(TypeStyle::Caption, Success(), buf);
    ImGui::SameLine(0, Layout::spacingLg());
    snprintf(buf, sizeof(buf), TR("balance_transparent_fmt"), s_dispTransparent);
    Type().textColored(TypeStyle::Caption, Warning(), buf);

    // USD value
    double usd_value = state.getBalanceUSD();
    if (usd_value > 0.0) snprintf(buf, sizeof(buf), "$%.2f USD", usd_value);
    else snprintf(buf, sizeof(buf), "$-.-- USD");
    ImGui::SameLine(0, Layout::spacingLg());
    Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);

    RenderSyncBar(app, dl, vs);

    // Dashed separator
    {
        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
        ImVec2 sepPos = ImGui::GetCursorScreenPos();
        float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f) * dp;
        float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f) * dp;
        float sepAlpha = S.drawElement("tabs.balance.minimal", "separator-alpha").sizeOr(25.0f);
        float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f) * dp;
        float x = sepPos.x;
        float endX = sepPos.x + availW;
        while (x < endX) {
            float x2 = std::min(x + dashLen, endX);
            dl->AddLine(ImVec2(x, sepPos.y), ImVec2(x2, sepPos.y),
                        IM_COL32(255, 255, 255, (int)sepAlpha), sepThick);
            x += dashLen + gapLen;
        }
        ImGui::Dummy(ImVec2(availW, 1));
        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
    }

    float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
    float minAddrOverride = S.drawElement("tabs.balance.minimal", "address-table-height").size;
    float addrH = (minAddrOverride >= 0.0f) ? minAddrOverride * dp
        : ImGui::GetContentRegionAvail().y - recentReserve
                  - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
    RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
    RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
}

} // namespace ui
} // namespace dragonx
