// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Receive Tab — redesigned to match Send tab layout
// - Address dropdown at top (like Send's source selector)
// - Single glass card containing QR, address, payment request
// - Action buttons below the card
// - Recent received at bottom

#include "receive_tab.h"
#include "../../app.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/payment_uri.h"
#include "../../util/platform.h"
#include "../../util/text_format.h"
#include "../../config/version.h"
#include "../../data/wallet_state.h"
#include "../../ui/widgets/qr_code.h"
#include "../sidebar.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../notifications.h"
#include "imgui.h"

#include <string>
#include <algorithm>
#include <cmath>
#include <ctime>
#include <map>
#include <vector>

namespace dragonx {
namespace ui {

using namespace material;

static std::string TrId(const char* key, const char* id) {
    return std::string(TR(key)) + "##" + id;
}

// ============================================================================
// State
// ============================================================================
static int s_selected_address_idx = -1;
static double s_request_amount = 0.0;
static char s_request_memo[512] = "";
static double s_request_usd_amount = 0.0;
static bool s_request_usd_mode = false;
static std::string s_cached_qr_data;
static uintptr_t s_qr_texture = 0;
static bool s_auto_selected = false;

// Address type filter
static int s_addr_type_filter = 0; // 0=All, 1=Z, 2=T

// Source dropdown preview string
static std::string s_source_preview;

// Track newly created addresses for NEW badge
static std::map<std::string, double> s_new_address_timestamps;
static size_t s_prev_address_count = 0;

// Address labels (in-memory until persistent config)
static std::map<std::string, std::string> s_address_labels;
static std::string s_pending_select_address;
static bool s_generating_address = false;

// ============================================================================
// Helpers
// ============================================================================
static void OpenExplorerURL(App* app, const std::string& address) {
    std::string url = app->settings()->getAddressExplorerUrl() + address;
    dragonx::util::Platform::openUrl(url);
}

// ============================================================================
// Track new addresses (detect creations)
// ============================================================================
static void TrackNewAddresses(const WalletState& state) {
    if (state.addresses.size() > s_prev_address_count && s_prev_address_count > 0) {
        for (const auto& a : state.addresses) {
            if (s_new_address_timestamps.find(a.address) == s_new_address_timestamps.end()) {
                s_new_address_timestamps[a.address] = ImGui::GetTime();
            }
        }
    } else if (s_prev_address_count == 0) {
        for (const auto& a : state.addresses) {
            s_new_address_timestamps[a.address] = 0.0;
        }
    }
    s_prev_address_count = state.addresses.size();
}

// ============================================================================
// Sync banner
// ============================================================================
static void RenderSyncBanner(const WalletState& state) {
    if (!state.sync.syncing || state.sync.isSynced()) return;

    // Use the work-weighted verification_progress like every other surface (not the naive
    // blocks/headers ratio, which over-reports during early sync).
    double vp = state.sync.verification_progress;
    if (vp < 0.0) vp = 0.0; else if (vp > 1.0) vp = 1.0;
    float syncPct = (float)(vp * 100.0);
    char syncBuf[128];
    snprintf(syncBuf, sizeof(syncBuf),
        TR("blockchain_syncing"), syncPct);
    ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.receive", "sync-banner-bg-color").color)));
    float syncH = std::max(schema::UI().drawElement("tabs.receive", "sync-banner-min-height").size, schema::UI().drawElement("tabs.receive", "sync-banner-height").size * Layout::vScale());
    ImGui::BeginChild("##SyncBannerRecv", ImVec2(ImGui::GetContentRegionAvail().x, syncH),
        false, ImGuiWindowFlags_NoScrollbar);
    ImGui::SetCursorPos(ImVec2(Layout::spacingLg(), (syncH - Type().caption()->LegacySize) * 0.5f));
    Type().textColored(TypeStyle::Caption, Warning(), syncBuf);
    ImGui::EndChild();
    ImGui::PopStyleColor();
}

// ============================================================================
// Address Dropdown — matches Send tab's source selector style
// ============================================================================
static void RenderAddressDropdown(App* app, float width) {
    const auto& state = app->getWalletState();
    char buf[256];

    // Header row: label + address type toggle buttons
    Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("address_upper"));

    float toggleBtnW = std::max(schema::UI().drawElement("tabs.receive", "toggle-btn-min-width").size, schema::UI().drawElement("tabs.receive", "toggle-btn-width").size * Layout::hScale(width));
    float toggleGap = schema::UI().drawElement("tabs.receive", "toggle-gap").size;
    float toggleTotalW = toggleBtnW * 3 + toggleGap * 2;
    float segH = ImGui::GetFrameHeight();
    ImGui::SameLine(width - toggleTotalW);
    ImVec2 segOrigin = ImGui::GetCursorScreenPos();
    const char* filterLabels[] = { TR("all_filter"), "Z", "T" };
    int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, toggleTotalW, segH,
                                            filterLabels, 3, s_addr_type_filter, Type().caption(),
                                            "##addrFilter", Layout::dpiScale());
    if (segClk >= 0) s_addr_type_filter = segClk;
    ImGui::Dummy(ImVec2(toggleTotalW, segH));

    ImGui::Dummy(ImVec2(0, Layout::spacingSm()));

    TrackNewAddresses(state);

    // Auto-select address with the largest balance on first load
    if (!s_auto_selected && app->isConnected() && !state.addresses.empty()) {
        int bestIdx = -1;
        double bestBal = -1.0;
        for (size_t i = 0; i < state.addresses.size(); i++) {
            if (state.addresses[i].balance > bestBal) {
                bestBal = state.addresses[i].balance;
                bestIdx = static_cast<int>(i);
            }
        }
        if (bestIdx >= 0) {
            s_selected_address_idx = bestIdx;
        }
        s_auto_selected = true;
    }

    // Auto-select pending new address
    if (!s_pending_select_address.empty()) {
        for (size_t i = 0; i < state.addresses.size(); i++) {
            if (state.addresses[i].address == s_pending_select_address) {
                s_selected_address_idx = static_cast<int>(i);
                s_cached_qr_data.clear();
                s_pending_select_address.clear();
                break;
            }
        }
    }

    // Combo/button widths first — the preview truncates to the combo's real pixel width below.
    float copyBtnW = std::max(schema::UI().drawElement("tabs.receive", "copy-btn-min-width").size, schema::UI().drawElement("tabs.receive", "copy-btn-width").size * Layout::hScale(width));
    float newBtnW = std::max(schema::UI().drawElement("tabs.receive", "new-btn-min-width").size, schema::UI().drawElement("tabs.receive", "new-btn-width").size * Layout::hScale(width));
    float dropdownW = width - copyBtnW - newBtnW - Layout::spacingSm() * 2;

    // Build preview string
    if (!app->isConnected()) {
        s_source_preview = TR(app->isLiteBuild() ? "lite_no_wallet_short" : "not_connected");
    } else if (s_selected_address_idx >= 0 &&
               s_selected_address_idx < (int)state.addresses.size()) {
        const auto& addr = state.addresses[s_selected_address_idx];
        bool isZ = addr.type == "shielded";
        const char* tag = isZ ? "[Z]" : "[T]";
        // Reserve pixel room for the tag prefix and the trailing balance, then middle-truncate the
        // address to whatever remains — measured with the combo's own Body2 font. Char-count
        // truncation kept MORE chars as the column widened, so at 150% the scaled font overflowed
        // and the combo hard-clipped "— 12.00000000 DRGX" to "— 1"; measuring in pixels keeps the
        // balance visible at any scale.
        ImFont* comboFont = Type().getFont(TypeStyle::Body2);
        float comboFontSz = comboFont->LegacySize;
        char prefix[16]; snprintf(prefix, sizeof(prefix), "%s ", tag);
        char suffix[64]; snprintf(suffix, sizeof(suffix), "  \xe2\x80\x94  %.8f %s", addr.balance, DRAGONX_TICKER);
        float fixedW = comboFont->CalcTextSizeA(comboFontSz, FLT_MAX, 0.0f, prefix).x
                     + comboFont->CalcTextSizeA(comboFontSz, FLT_MAX, 0.0f, suffix).x;
        // Combo interior = dropdownW minus its dropdown-arrow button (~frame height) and both frame paddings.
        float addrBudget = dropdownW - ImGui::GetFrameHeight() - ImGui::GetStyle().FramePadding.x * 2.0f - fixedW;
        if (addrBudget < 24.0f) addrBudget = 24.0f;  // floor: truncate to a stub rather than overflow
        std::string trunc = material::TruncateToWidth(addr.address, comboFont, comboFontSz, addrBudget);
        snprintf(buf, sizeof(buf), "%s%s%s", prefix, trunc.c_str(), suffix);
        s_source_preview = buf;
    } else {
        s_source_preview = TR("select_receiving_address");
    }

    ImGui::SetNextItemWidth(dropdownW);
    ImGui::PushFont(Type().getFont(TypeStyle::Body2));
    if (ImGui::BeginCombo("##RecvAddr", s_source_preview.c_str())) {
        if (!app->isConnected() || state.addresses.empty()) {
            ImGui::TextDisabled("%s", TR("no_addresses_available"));
        } else {
            // Build filtered and sorted list
            std::vector<size_t> sortedIdx;
            sortedIdx.reserve(state.addresses.size());
            for (size_t i = 0; i < state.addresses.size(); i++) {
                bool isZ = state.addresses[i].type == "shielded";
                if (s_addr_type_filter == 1 && !isZ) continue;
                if (s_addr_type_filter == 2 && isZ) continue;
                sortedIdx.push_back(i);
            }
            std::sort(sortedIdx.begin(), sortedIdx.end(),
                [&](size_t a, size_t b) {
                    return state.addresses[a].balance > state.addresses[b].balance;
                });

            if (sortedIdx.empty()) {
                ImGui::TextDisabled("%s", TR("no_addresses_match"));
            } else {
                size_t addrTruncLen = static_cast<size_t>(std::max(schema::UI().drawElement("tabs.receive", "addr-dropdown-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-dropdown-trunc-divisor").size));
                double now = ImGui::GetTime();

                for (size_t si = 0; si < sortedIdx.size(); si++) {
                    size_t i = sortedIdx[si];
                    const auto& addr = state.addresses[i];
                    bool isCurrent = (s_selected_address_idx == static_cast<int>(i));
                    bool isZ = addr.type == "shielded";
                    const char* tag = isZ ? "[Z]" : "[T]";

                    // Check for NEW badge
                    bool isNew = false;
                    auto newIt = s_new_address_timestamps.find(addr.address);
                    if (newIt != s_new_address_timestamps.end() && newIt->second > 0.0) {
                        double age = now - newIt->second;
                        if (age < schema::UI().drawElement("tabs.receive", "new-badge-timeout").size) isNew = true;
                    }

                    // Check for label
                    auto lblIt = s_address_labels.find(addr.address);
                    bool hasLabel = (lblIt != s_address_labels.end() && !lblIt->second.empty());

                    std::string trunc = util::truncateMiddle(addr.address, (int)addrTruncLen);
                    if (hasLabel) {
                        snprintf(buf, sizeof(buf), "%s %s (%s)  \xe2\x80\x94  %.8f %s%s",
                                 tag, lblIt->second.c_str(), trunc.c_str(),
                                 addr.balance, DRAGONX_TICKER,
                                 isNew ? TR("grpb_new_badge_suffix") : "");
                    } else {
                        snprintf(buf, sizeof(buf), "%s %s  \xe2\x80\x94  %.8f %s%s",
                                 tag, trunc.c_str(), addr.balance, DRAGONX_TICKER,
                                 isNew ? TR("grpb_new_badge_suffix") : "");
                    }

                    ImGui::PushID(static_cast<int>(i));
                    if (ImGui::Selectable(buf, isCurrent)) {
                        s_selected_address_idx = static_cast<int>(i);
                        s_cached_qr_data.clear(); // Force QR regeneration
                    }
                    if (ImGui::IsItemHovered()) {
                        material::Tooltip(TR("grpb_tooltip_address_balance"),
                            addr.address.c_str(), addr.balance, DRAGONX_TICKER,
                            isCurrent ? TR("grpb_selected_suffix") : "");
                    }
                    ImGui::PopID();
                }
            }
        }
        ImGui::EndCombo();
    }
    ImGui::PopFont();

    // Copy address button
    ImGui::SameLine(0, Layout::spacingSm());
    ImGui::BeginDisabled(!app->isConnected() || s_selected_address_idx < 0 ||
                         s_selected_address_idx >= (int)state.addresses.size());
    if (TactileButton(TrId("copy", "recvAddr").c_str(), ImVec2(copyBtnW, 0), schema::UI().resolveFont("button"))) {
        if (s_selected_address_idx >= 0 && s_selected_address_idx < (int)state.addresses.size()) {
            ImGui::SetClipboardText(state.addresses[s_selected_address_idx].address.c_str());
            Notifications::instance().info(TR("address_copied"));
        }
    }
    ImGui::EndDisabled();

    // New address button on same line
    ImGui::SameLine(0, Layout::spacingSm());
    ImGui::BeginDisabled(!app->isConnected() || s_generating_address);
    if (s_generating_address) {
        char genLabel[64];
        snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), material::LoadingDots());
        TactileButton(genLabel, ImVec2(newBtnW, 0), schema::UI().resolveFont("button"));
    } else if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
        // createNewAddress can early-return without ever invoking the callback (e.g. connection
        // dropped between this button being enabled and the click landing), which would leave
        // s_generating_address stuck. Only enter the "generating…" state when a dispatch is possible.
        if (!app->isConnected()) {
            s_generating_address = false;
        } else {
            s_generating_address = true;
            if (s_addr_type_filter != 2) {
                app->createNewZAddress([](const std::string& addr) {
                    s_generating_address = false;
                    if (addr.empty())
                        Notifications::instance().error(TR("failed_create_shielded"));
                    else {
                        s_pending_select_address = addr;
                        Notifications::instance().success(TR("new_shielded_created"));
                    }
                });
            } else {
                app->createNewTAddress([](const std::string& addr) {
                    s_generating_address = false;
                    if (addr.empty())
                        Notifications::instance().error(TR("failed_create_transparent"));
                    else {
                        s_pending_select_address = addr;
                        Notifications::instance().success(TR("new_transparent_created"));
                    }
                });
            }
        }
    }
    ImGui::EndDisabled();
}

// ============================================================================
// Helpers: DrawRecvIcon (local copy — original is static in send_tab).
// Relative time uses the shared util::formatTimeAgoShort ("14d ago"), matching Overview/Send.
// ============================================================================
static void DrawRecvIcon(ImDrawList* dl, float cx, float cy, float s, ImU32 col) {
    dl->AddTriangleFilled(
        ImVec2(cx, cy + s),
        ImVec2(cx - s * 0.65f, cy - s * 0.3f),
        ImVec2(cx + s * 0.65f, cy - s * 0.3f), col);
    dl->AddRectFilled(
        ImVec2(cx - s * 0.2f, cy - s * 0.8f),
        ImVec2(cx + s * 0.2f, cy - s * 0.3f), col);
}

// ============================================================================
// Recent received transactions — styled to match transactions list
// ============================================================================
static void RenderRecentReceived(const AddressInfo& /* addr */,
                                 const WalletState& state, float width,
                                 ImFont* capFont, App* app) {
    auto& S = schema::UI();
    ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
    Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_received"));
    ImGui::Dummy(ImVec2(0, Layout::spacingXs()));

    float hs = Layout::hScale(width);
    float vs = Layout::vScale(std::max(1.0f, ImGui::GetContentRegionAvail().y));
    float dp = Layout::dpiScale();
    float rowH = std::max(18.0f * dp, S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f) * vs);
    float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
                            S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
    ImU32 recvCol = Success();

    // Grow the list to fill the dead space beneath the (fixed-height) receive card:
    // fit as many newest-first rows as the remaining region can show, instead of a
    // fixed 4. The child scrolls if the real history exceeds what fits, so nothing is
    // lost. A floor of 4 keeps the section substantial when the region is short.
    // Compute maxRows BEFORE the collect loop so the scan can stop early (below).
    float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
    size_t maxRows = std::max<size_t>(4, (size_t)std::floor(listH / rowH));

    // Collect matching transactions. state.transactions is newest-first, so the first
    // maxRows matches ARE exactly the rows we render — stop scanning once we have them
    // instead of filtering the entire history every frame (mirrors RenderSharedRecentTx
    // in balance_components.cpp).
    std::vector<const TransactionInfo*> recvs;
    recvs.reserve(maxRows);
    for (const auto& tx : state.transactions) {
        if (tx.type != "receive" && tx.type != "mined") continue;
        recvs.push_back(&tx);
        if (recvs.size() >= maxRows) break;  // newest-first: prefix is all we show
    }

    ImGui::BeginChild("##RecentReceivedRows", ImVec2(width, listH), false,
        ImGuiWindowFlags_NoBackground);
    ImDrawList* rowDL = ImGui::GetWindowDrawList();

    char buf[64];
    if (recvs.empty()) {
        // Fill the empty canvas with a centered material empty-state (icon + title)
        // rather than a lone left-aligned caption stranded at the top of dead space.
        material::DrawEmptyState(ICON_MD_CALL_RECEIVED, TR("no_recent_receives"));
        ImGui::EndChild();
        return;
    }

    for (size_t ri = 0; ri < recvs.size(); ri++) {
        const auto& tx = *recvs[ri];

        ImVec2 rowPos = ImGui::GetCursorScreenPos();
        float rowY = rowPos.y + rowH * 0.5f;

        // Icon
        DrawRecvIcon(rowDL, rowPos.x + Layout::spacingMd(), rowY, iconSz * 0.5f, recvCol);

        // Type label (first line)
        float txX = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
        const char* typeText = tx.type == "mined" ? TR("mined_type") : TR("received_label");
        rowDL->AddText(capFont, capFont->LegacySize,
                       ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), typeText);

        // Address — start it AFTER the measured type-label width (not a fixed offset that shrinks below
        // the label at narrow widths), mirroring Overview's recent-tx list. The schema offset is a floor.
        float typeW = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, typeText).x;
        float addrX = txX + std::max(S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs,
                                     typeW + Layout::spacingSm());
        std::string addrDisplay = util::truncateMiddle(tx.address,
            (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
        rowDL->AddText(capFont, capFont->LegacySize,
                       ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());

        // Time ago — short "14d ago" form (shared helper, matches Overview/Send). Measure it
        // first so the amount can chain its right edge off this width and never overlap.
        std::string ago = util::formatTimeAgoShort(tx.timestamp);
        ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
        float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
        float agoMargin = S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f) * hs;
        rowDL->AddText(capFont, capFont->LegacySize,
                       ImVec2(rightEdge - agoSz.x - agoMargin, rowPos.y + 2.0f * dp),
                       OnSurfaceDisabled(), ago.c_str());

        // Amount (right-aligned, first line) — anchored to the LEFT of the time-ago text
        // (measured width + a gap) so the two columns can never collide, whatever the strings.
        snprintf(buf, sizeof(buf), "+%.4f %s", std::abs(tx.amount), DRAGONX_TICKER);
        ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, buf);
        float amtGap = std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size,
                                S.drawElement("tabs.balance", "amount-right-margin").size * hs);
        float amtRightEdge = rightEdge - agoSz.x - agoMargin - amtGap;
        float amtX = amtRightEdge - amtSz.x;
        rowDL->AddText(capFont, capFont->LegacySize,
                       ImVec2(amtX, rowPos.y + 2.0f * dp), recvCol, buf);

        // Clickable row — hover highlight + navigate to History
        float rowW = ImGui::GetContentRegionAvail().x;
        ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
        if (material::IsRectHovered(rowPos, rowEnd)) {
            rowDL->AddRectFilled(rowPos, rowEnd,
                                 IM_COL32(255, 255, 255, 15),
                                 S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
            ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
            if (ImGui::IsMouseClicked(0)) {
                app->setCurrentPage(ui::NavPage::History);
            }
        }

        ImGui::Dummy(ImVec2(0, rowH));
    }

    ImGui::EndChild();
}

// ============================================================================
// MAIN: RenderReceiveTab
// ============================================================================
void RenderReceiveTab(App* app)
{
    const auto& state = app->getWalletState();
    auto& S = schema::UI();

    RenderSyncBanner(state);

    ImVec2 recvAvail = ImGui::GetContentRegionAvail();
    float hs = Layout::hScale(recvAvail.x);
    float vScale = Layout::vScale(recvAvail.y);
    float glassRound = Layout::glassRounding();

    float availWidth = recvAvail.x;

    ImDrawList* dl = ImGui::GetWindowDrawList();
    ImFont* capFont = Type().caption();
    ImFont* sub1    = Type().subtitle1();
    ImFont* body2   = Type().body2();
    GlassPanelSpec glassSpec;
    glassSpec.rounding = glassRound;

    float sectionGap = Layout::spacingXl() * vScale;
    char buf[128];

    // ================================================================
    // NON-SCROLLING CONTENT — resizes to fit available height
    // ================================================================
    ImVec2 formAvail = ImGui::GetContentRegionAvail();
    ImGui::BeginChild("##ReceiveScroll", formAvail, false,
        ImGuiWindowFlags_NoBackground);
    dl = ImGui::GetWindowDrawList();

    // Top-aligned content — consistent vertical position across all tabs
    static float s_recvContentH = 0;
    float scrollAvailH = ImGui::GetContentRegionAvail().y;
    float groupStartY  = ImGui::GetCursorPosY();
    float contentStartY = ImGui::GetCursorPosY();

    // Reserve a slice of the available height for RECENT RECEIVED (ratio — mirrors
    // balance_tab's recent-tx-reserve). The main card's target height is capped so it can
    // never grow past (available - reserve): a no-op at 1.0x (mainCardTargetH already fits
    // well within scrollAvailH there), but at HiDPI it prevents the card — whose QR (280*dp)
    // and pads scale with dp while scrollAvailH is physical px — from eating the whole child
    // and evicting the list.
    float recvReserveRatio = S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
    float recvRecentReserve = std::max(0.0f, scrollAvailH) * recvReserveRatio;
    float recvCardCapH = std::max(0.0f, scrollAvailH - recvRecentReserve);

    float formAvailW = ImGui::GetContentRegionAvail().x;
    float formW = formAvailW;
    ImGui::BeginGroup();

    // ================================================================
    // Not connected / empty state
    // ================================================================
    if (!app->isConnected()) {
        ImVec2 emptyMin = ImGui::GetCursorScreenPos();
        float emptyH = std::max(schema::UI().drawElement("tabs.receive", "empty-state-min-height").size, schema::UI().drawElement("tabs.receive", "empty-state-height").size * Layout::vScale());
        ImVec2 emptyMax(emptyMin.x + formW, emptyMin.y + emptyH);
        DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
        dl->AddText(sub1, sub1->LegacySize,
            ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl()),
            OnSurfaceDisabled(), TR(app->isLiteBuild() ? "lite_no_wallet" : "waiting_for_daemon"));
        dl->AddText(capFont, capFont->LegacySize,
            ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl() + sub1->LegacySize + S.drawElement("tabs.receive", "empty-state-subtitle-gap").size),
            OnSurfaceDisabled(), TR("addresses_appear_here"));
        ImGui::Dummy(ImVec2(formW, emptyH));
        ImGui::EndGroup();
        ImGui::EndChild();
        return;
    }

    if (state.addresses.empty()) {
        ImVec2 emptyMin = ImGui::GetCursorScreenPos();
        // Hand-drawn skeleton geometry is absolute px — scale heights/offsets/rounding by
        // dpiScale() so it doesn't render native-thin at >100% (width ratios multiply formW, leave as-is).
        float dp = Layout::dpiScale();
        float emptyH = S.drawElement("tabs.receive", "skeleton-height").size * dp;
        ImVec2 emptyMax(emptyMin.x + formW, emptyMin.y + emptyH);
        DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
        float alpha = (float)(schema::UI().drawElement("animations", "skeleton-base").size + schema::UI().drawElement("animations", "skeleton-amp").size * std::sin(ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-slow").size));
        ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
        float skelRound = schema::UI().drawElement("tabs.receive", "skeleton-rounding").size * dp;
        dl->AddRectFilled(
            ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg()),
            ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar1-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar1-height").size * dp),
            skelCol, skelRound);
        dl->AddRectFilled(
            ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-top").size * dp),
            ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar2-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-bottom").size * dp),
            skelCol, skelRound);
        dl->AddText(capFont, capFont->LegacySize,
            ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size * dp),
            OnSurfaceDisabled(), TR("loading_addresses"));
        ImGui::Dummy(ImVec2(formW, emptyH));
        ImGui::EndGroup();
        ImGui::EndChild();
        return;
    }

    // Ensure valid selection
    if (s_selected_address_idx < 0 ||
        s_selected_address_idx >= (int)state.addresses.size()) {
        s_selected_address_idx = 0;
    }

    const AddressInfo& selected = state.addresses[s_selected_address_idx];
    bool isZ = selected.type == "shielded";

    // Generate QR data
    std::string qr_data = selected.address;
    if (s_request_amount > 0) {
        // Canonical, URL-encoded "drgx:" URI via the shared builder. The old inline code emitted an
        // unparseable "dragonx:" scheme and appended the memo raw (spaces/&/= corrupted the QR).
        qr_data = util::buildPaymentUri(selected.address, s_request_amount, "",
                                        (s_request_memo[0] && isZ) ? std::string(s_request_memo) : std::string());
    }
    if (qr_data != s_cached_qr_data) {
        if (s_qr_texture) {
            FreeQRTexture(s_qr_texture);
            s_qr_texture = 0;
        }
        int w, h;
        s_qr_texture = GenerateQRTexture(qr_data.c_str(), &w, &h);
        s_cached_qr_data = qr_data;
    }

    // ================================================================
    // MAIN CARD — single glass panel (channel split like Send tab)
    // ================================================================
    {
        // Cap + center the card so the address/amount column stops stretching while the
        // QR plateaus. Shared with the Send tab via mainComposeCardBox() so the two card
        // envelopes are identical (they must never drift in width/position again).
        // The RECENT RECEIVED list below stays on the uncapped formW (handled separately).
        float cardDp = Layout::dpiScale();
        Layout::CardBox cardBox = Layout::mainComposeCardBox(formW);
        float cardW = cardBox.width;
        float cardLeftX = ImGui::GetCursorScreenPos().x;
        float cardOffsetX = cardBox.offsetX;
        if (cardOffsetX > 0.0f)
            ImGui::SetCursorPosX(ImGui::GetCursorPosX() + cardOffsetX);

        ImVec2 containerMin = ImGui::GetCursorScreenPos();
        float pad = Layout::spacingLg();
        float innerW = cardW - pad * 2;
        float innerGap = Layout::spacingLg();

        // Channel split: content on ch1, glass background on ch0
        dl->ChannelsSplit(2);
        dl->ChannelsSetCurrent(1);

        // Indent carries the centering offset too, so ImGui auto-layout content (the address
        // dropdown/inputs/chips) lands at containerMin.x + pad — matching the hand-drawn geometry
        // (glass panel / QR column) that keys off the offset containerMin.x.
        ImGui::Indent(pad + cardOffsetX);
        ImGui::Dummy(ImVec2(0, pad)); // top padding

        // ---- ADDRESS DROPDOWN + QR CODE — side by side ----
        {
            float qrColW = std::min(innerW * schema::UI().drawElement("tabs.receive", "qr-col-width-ratio").size, 340.0f * cardDp);
            float colGap = Layout::spacingLg();
            float addrColW = innerW - qrColW - colGap;
            float qrColX = containerMin.x + pad + addrColW + colGap;
            ImVec2 sectionTop = ImGui::GetCursorScreenPos();

            // LEFT: ADDRESS DROPDOWN + PAYMENT REQUEST
            {
                // Address dropdown replaces the old address display panel
                RenderAddressDropdown(app, addrColW);

                ImGui::Dummy(ImVec2(0, Layout::spacingMd()));

                // ---- PAYMENT REQUEST ----
                Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("payment_request"));
                ImGui::Dummy(ImVec2(0, Layout::spacingSm()));

                // Amount input with currency toggle
                float toggleW = S.drawElement("tabs.receive", "currency-toggle-width").size * Layout::dpiScale();
                float amtInputW = addrColW - toggleW - Layout::spacingMd();
                if (amtInputW < S.drawElement("tabs.receive", "amount-input-min-width").size) amtInputW = S.drawElement("tabs.receive", "amount-input-min-width").size;
                double usd_price = state.market.price_usd;

                ImGui::PushFont(body2);
                if (s_request_usd_mode && usd_price > 0) {
                    ImGui::PushItemWidth(amtInputW);
                    if (ImGui::InputDouble("##RequestAmountUSD", &s_request_usd_amount, 0, 0, "$%.2f")) {
                        if (s_request_usd_amount < 0) s_request_usd_amount = 0;
                        s_request_amount = s_request_usd_amount / usd_price;
                    }
                    {
                        ImVec2 iMin = ImGui::GetItemRectMin();
                        ImVec2 iMax = ImGui::GetItemRectMax();
                        snprintf(buf, sizeof(buf), "\xe2\x89\x88 %.4f %s", s_request_amount, DRAGONX_TICKER);
                        ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
                        float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x;
                        float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f;
                        dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty),
                                    IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf);
                    }
                    ImGui::PopItemWidth();
                } else {
                    ImGui::PushItemWidth(amtInputW);
                    if (ImGui::InputDouble("##RequestAmount", &s_request_amount, 0, 0, "%.8f")) {
                        if (s_request_amount < 0) s_request_amount = 0;
                        if (usd_price > 0)
                            s_request_usd_amount = s_request_amount * usd_price;
                    }
                    if (usd_price > 0 && s_request_amount > 0) {
                        ImVec2 iMin = ImGui::GetItemRectMin();
                        ImVec2 iMax = ImGui::GetItemRectMax();
                        double usd_value = s_request_amount * usd_price;
                        if (usd_value >= 0.01)
                            snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd_value);
                        else
                            snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.6f", usd_value);
                        ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
                        float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x;
                        float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f;
                        dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty),
                                    IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf);
                    }
                    ImGui::PopItemWidth();
                }

                // Toggle button
                ImGui::SameLine(0, Layout::spacingMd());
                {
                    const char* currLabel = s_request_usd_mode ? "DRGX" : "USD";
                    bool canToggle = (usd_price > 0);
                    ImGui::BeginDisabled(!canToggle);
                    if (TactileButton("##ToggleCurrencyRecv", ImVec2(toggleW, 0), S.resolveFont("button"))) {
                        s_request_usd_mode = !s_request_usd_mode;
                        if (s_request_usd_mode && usd_price > 0)
                            s_request_usd_amount = s_request_amount * usd_price;
                    }
                    {
                        ImVec2 bMin = ImGui::GetItemRectMin();
                        ImVec2 bMax = ImGui::GetItemRectMax();
                        float bH = bMax.y - bMin.y;
                        ImFont* font = ImGui::GetFont();
                        ImVec2 textSz = font->CalcTextSizeA(font->LegacySize, 10000, 0, currLabel);
                        float iconW = schema::UI().drawElement("tabs.receive", "currency-icon-width").size * Layout::dpiScale();
                        float iconGap2 = schema::UI().drawElement("tabs.receive", "currency-icon-gap").size * Layout::dpiScale();
                        float totalW2 = iconW + iconGap2 + textSz.x;
                        float startX = bMin.x + ((bMax.x - bMin.x) - totalW2) * 0.5f;
                        float cy = bMin.y + bH * 0.5f;
                        ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
                        float ss = iconW * 0.5f;
                        float cx = startX + ss;
                        float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
                        float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
                        float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
                        float ay1 = cy - ag;
                        dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
                        dl->AddTriangleFilled(
                            ImVec2(cx + al, ay1),
                            ImVec2(cx + al - hss, ay1 - hss),
                            ImVec2(cx + al - hss, ay1 + hss), iconCol);
                        float ay2 = cy + ag;
                        dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
                        dl->AddTriangleFilled(
                            ImVec2(cx - al, ay2),
                            ImVec2(cx - al + hss, ay2 - hss),
                            ImVec2(cx - al + hss, ay2 + hss), iconCol);
                        float tx = startX + iconW + iconGap2;
                        float ty = cy - textSz.y * 0.5f;
                        dl->AddText(font, font->LegacySize, ImVec2(tx, ty), iconCol, currLabel);
                    }
                    ImGui::EndDisabled();
                }
                ImGui::PopFont();

                // Preset amount chips
                {
                    ImGui::Dummy(ImVec2(0, Layout::spacingXs()));

                    float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
                    float chipGap   = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
                    float chipH     = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;

                    struct Preset { const char* label; double amount; };
                    Preset presets[] = {
                        { "0.1",   0.1     },
                        { "1",     1.0     },
                        { "10",    10.0    },
                        { "100",   100.0   },
                        { "1K",    1000.0  },
                        { "10K",   10000.0 },
                        { "50K",   50000.0 },
                        { "100K",  100000.0 },
                    };
                    constexpr int presetCount = 8;

                    // Calculate equal chip widths to span full addrColW
                    float totalGap = chipGap * (presetCount - 1);
                    float chipW = (addrColW - totalGap) / presetCount;

                    ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
                    ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, chipRound);

                    ImFont* chipFont = S.resolveFont("button");

                    for (int ci = 0; ci < presetCount; ci++) {
                        if (ci > 0) ImGui::SameLine(0, chipGap);

                        bool active = false;
                        if (s_request_usd_mode) {
                            active = (usd_price > 0 && std::abs(s_request_usd_amount - presets[ci].amount) < 0.001);
                        } else {
                            active = (std::abs(s_request_amount - presets[ci].amount) < 1e-8);
                        }

                        if (active) {
                            ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "chip-active-bg-alpha").size)));
                            ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
                        }

                        char chipId[32];
                        snprintf(chipId, sizeof(chipId), "%s##presetAmt%d", presets[ci].label, ci);
                        if (TactileButton(chipId, ImVec2(chipW, chipH), chipFont)) {
                            if (s_request_usd_mode) {
                                s_request_usd_amount = presets[ci].amount;
                                if (usd_price > 0)
                                    s_request_amount = s_request_usd_amount / usd_price;
                            } else {
                                s_request_amount = presets[ci].amount;
                                if (usd_price > 0)
                                    s_request_usd_amount = s_request_amount * usd_price;
                            }
                        }

                        if (active) ImGui::PopStyleColor(2);
                    }

                    ImGui::PopStyleVar();
                    ImGui::PopStyleColor();
                }

                // Memo (z-addresses only)
                if (isZ) {
                    ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
                    Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
                    ImGui::Dummy(ImVec2(0, S.drawElement("tabs.receive", "memo-label-gap").size));

                    float memoInputH = std::max(schema::UI().drawElement("tabs.receive", "memo-input-min-height").size, schema::UI().drawElement("tabs.receive", "memo-input-height").size * vScale);
                    ImGui::PushItemWidth(addrColW);
                    ImGui::InputTextMultiline("##RequestMemo", s_request_memo, sizeof(s_request_memo),
                        ImVec2(addrColW, memoInputH));
                    ImGui::PopItemWidth();

                    // The Sapling memo limit is in BYTES and matches the enforced buffer size
                    // (InputTextMultiline caps at sizeof(s_request_memo)); count against that, not the
                    // old display-only 256 value, and warn in colour once the field is at its cap.
                    size_t memo_len = strlen(s_request_memo);
                    size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
                    bool memoAtCap = memo_len + 1 >= memoMax;
                    snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
                    Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
                }

                // URI preview
                ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
                if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
                    float uriWrapW = addrColW;
                    dl->AddText(capFont, capFont->LegacySize, ImGui::GetCursorScreenPos(),
                                OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW);
                    ImVec2 uriSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX,
                                                           uriWrapW, s_cached_qr_data.c_str());
                    ImGui::Dummy(ImVec2(addrColW, uriSz.y + Layout::spacingSm()));
                } else {
                    ImGui::Dummy(ImVec2(addrColW, capFont->LegacySize + Layout::spacingSm()));
                }

            }
            float leftBottom = ImGui::GetCursorScreenPos().y;

            // RIGHT: QR CODE
            {
                float qrAvailW = qrColW;
                float rx = qrColX;
                float ry = sectionTop.y;

                float qrPadding = Layout::spacingMd();
                float qrWidthBound = std::max(0.0f, qrAvailW - qrPadding * 2.0f);
                float qrHeightBound = std::max(0.0f, leftBottom - sectionTop.y - qrPadding * 2.0f);
                float qrMaxSize = qrHeightBound > 0.0f
                    ? std::min(qrWidthBound, qrHeightBound)
                    : qrWidthBound;
                float qrMinSize = std::min(S.drawElement("tabs.receive", "qr-min-size").size, qrWidthBound);
                float qrSize = std::max(qrMinSize, qrMaxSize);
                float totalQrSize = qrSize + qrPadding * 2;
                float qrOffsetX = std::max(0.0f, (qrAvailW - totalQrSize) * 0.5f);
                ImVec2 qrPanelMin(rx + qrOffsetX, ry);
                ImVec2 qrPanelMax(qrPanelMin.x + totalQrSize, qrPanelMin.y + totalQrSize);

                GlassPanelSpec qrGlass;
                qrGlass.rounding = glassRound * S.drawElement("tabs.receive", "qr-glass-rounding-ratio").size;
                qrGlass.fillAlpha = (int)S.drawElement("tabs.receive", "qr-glass-fill-alpha").size;
                qrGlass.borderAlpha = (int)S.drawElement("tabs.receive", "qr-glass-border-alpha").size;
                DrawGlassPanel(dl, qrPanelMin, qrPanelMax, qrGlass);

                ImGui::SetCursorScreenPos(ImVec2(qrPanelMin.x + qrPadding, qrPanelMin.y + qrPadding));
                if (s_qr_texture) {
                    RenderQRCode(s_qr_texture, qrSize);
                } else {
                    ImGui::Dummy(ImVec2(qrSize, qrSize));
                    ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size * cardDp,
                                   qrPanelMin.y + totalQrSize * 0.5f);
                    dl->AddText(capFont, capFont->LegacySize, textPos,
                                OnSurfaceDisabled(), TR("qr_unavailable"));
                }

                ImGui::SetCursorScreenPos(qrPanelMin);
                ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
                if (ImGui::IsItemHovered()) {
                    ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                    material::Tooltip("%s",
                        s_request_amount > 0 ? TR("click_copy_uri") : TR("click_copy_address"));
                }
                if (ImGui::IsItemClicked()) {
                    ImGui::SetClipboardText(qr_data.c_str());
                    Notifications::instance().info(s_request_amount > 0
                        ? TR("payment_uri_copied") : TR("address_copied"));
                }

                ImGui::SetCursorScreenPos(ImVec2(rx, qrPanelMax.y));
                ImGui::Dummy(ImVec2(qrAvailW, 0));
            }
            float rightBottom = ImGui::GetCursorScreenPos().y;
            float sectionBottom = std::max(leftBottom, rightBottom);
            ImGui::SetCursorScreenPos(ImVec2(containerMin.x + pad, sectionBottom));
            ImGui::Dummy(ImVec2(innerW, 0));
        }

        // Divider before action buttons
        {
            float actionBtnH = std::max(S.drawElement("tabs.receive", "action-btn-min-height").size,
                                        S.drawElement("tabs.receive", "action-btn-height").size * vScale);
            float footerH = innerGap + actionBtnH + pad;
            float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
            float targetCardH = std::min(Layout::mainCardTargetH(cardW, vScale), recvCardCapH);
            float footerTopH = targetCardH - footerH;
            if (currentCardH < footerTopH) {
                ImGui::Dummy(ImVec2(0, footerTopH - currentCardH));
            }
        }

        ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
        {
            ImVec2 divPos = ImGui::GetCursorScreenPos();
            dl->AddLine(ImVec2(containerMin.x + pad, divPos.y),
                        ImVec2(containerMin.x + cardW - pad, divPos.y),
                        ImGui::GetColorU32(Divider()), S.drawElement("tabs.receive", "divider-thickness").size);
        }
        ImGui::Dummy(ImVec2(0, innerGap * 0.5f));

        // ---- ACTION BUTTONS (inside card) ----
        {
            float btnGap = Layout::spacingMd();
            float btnH = std::max(schema::UI().drawElement("tabs.receive", "action-btn-min-height").size, schema::UI().drawElement("tabs.receive", "action-btn-height").size * vScale);
            // Justify the footer edge-to-edge like Send's [Review Send][Cancel] row instead of packing
            // fixed-width buttons from the left (which left a large dead gap on the right). Split innerW
            // into equal shares over the live button count: 2 by default (Clear Request + Explorer),
            // 4 when an amount is requested (+ Copy URI + Share).
            int nBtns = (s_request_amount > 0 ? 2 : 0) + 2;
            float otherBtnW = std::max(S.drawElement("tabs.receive", "action-btn-min-width").size,
                                       (innerW - (nBtns - 1) * btnGap) / (float)nBtns);

            bool firstBtn = true;

            if (s_request_amount > 0) {
                if (!firstBtn) ImGui::SameLine(0, btnGap);
                firstBtn = false;
                if (TactileButton(TrId("copy_uri", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
                    ImGui::SetClipboardText(s_cached_qr_data.c_str());
                    Notifications::instance().info(TR("payment_uri_copied"));
                }
            }

            if (s_request_amount > 0) {
                if (!firstBtn) ImGui::SameLine(0, btnGap);
                firstBtn = false;
                ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
                ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
                    ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size)));
                ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
                if (TactileButton(TrId("share", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
                    char shareBuf[1024];
                    snprintf(shareBuf, sizeof(shareBuf),
                        "%s\n%s: %.8f %s\n%s: %s\nURI: %s",
                        TR("payment_request"), TR("amount"),
                        s_request_amount, DRAGONX_TICKER,
                        TR("address"), selected.address.c_str(),
                        s_cached_qr_data.c_str());
                    ImGui::SetClipboardText(shareBuf);
                    Notifications::instance().info(TR("payment_request_copied"));
                }
                ImGui::PopStyleColor(3);
            }

            {
                bool hasData = (s_request_amount > 0 || s_request_memo[0]);
                if (!firstBtn) ImGui::SameLine(0, btnGap);
                firstBtn = false;
                ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
                ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
                    ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "clear-btn-hover-alpha").size)));
                ImGui::PushStyleColor(ImGuiCol_Text,
                    ImGui::ColorConvertU32ToFloat4(hasData ? OnSurfaceMedium() : OnSurfaceDisabled()));
                ImGui::BeginDisabled(!hasData);
                if (TactileButton(TrId("clear_request", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
                    s_request_amount = 0.0;
                    s_request_usd_amount = 0.0;
                    s_request_memo[0] = '\0';
                }
                ImGui::EndDisabled();
                ImGui::PopStyleColor(3);
            }

            if (!firstBtn) ImGui::SameLine(0, btnGap);
            firstBtn = false;
            ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
            ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
                ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size)));
            ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
            ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.receive", "explorer-btn-border-size").size);
            ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
            if (TactileButton(TrId("explorer", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
                OpenExplorerURL(app, selected.address);
            }
            ImGui::PopStyleVar(); // FrameBorderSize
            ImGui::PopStyleColor(4);

        }

        // Bottom padding
        ImGui::Dummy(ImVec2(0, pad));
        ImGui::Unindent(pad + cardOffsetX);

        // Enforce shared card height (matches QR-driven target), capped so the reserved
        // RECENT RECEIVED slice below stays on-screen at HiDPI.
        {
            float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
            float targetCardH  = std::min(Layout::mainCardTargetH(cardW, vScale), recvCardCapH);
            if (currentCardH < targetCardH)
                ImGui::Dummy(ImVec2(0, targetCardH - currentCardH));
        }

        // Draw glass panel background on channel 0
        ImVec2 containerMax(containerMin.x + cardW, ImGui::GetCursorScreenPos().y);
        dl->ChannelsSetCurrent(0);
        DrawGlassPanel(dl, containerMin, containerMax, glassSpec);
        dl->ChannelsMerge();

        // Restore the original (uncapped) left edge so RECENT RECEIVED below renders
        // full-width as-is, unaffected by the card's centering offset.
        ImGui::SetCursorScreenPos(ImVec2(cardLeftX, containerMax.y));
        ImGui::Dummy(ImVec2(formW, 0));
        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
    }

    // ================================================================
    // RECENT RECEIVED
    // ================================================================
    RenderRecentReceived(selected, state, formW, capFont, app);

    ImGui::EndGroup();
    float measuredH = ImGui::GetCursorPosY() - contentStartY;
    s_recvContentH = std::round(measuredH);
    ImGui::EndChild(); // ##ReceiveScroll
}

} // namespace ui
} // namespace dragonx
