// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Layout E: Top Bar Source + Bottom Form
// - Persistent source address bar at top (always visible)
// - Centered compose form below
// - Recent sends at bottom

#include "send_tab.h"
#include "../../app.h"
#include "../../config/version.h"
#include "../../data/wallet_state.h"
#include "../../util/address_validation.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../notifications.h"
#include "../layout.h"
#include "contact_picker.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"

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

namespace dragonx {
namespace ui {

using namespace material;

// ============================================================================
// Form state
// ============================================================================
static char s_from_address[512] = "";
static char s_to_address[512] = "";
static double s_amount = 0.0;
static char s_memo[512] = "";
static double s_fee = DRAGONX_DEFAULT_FEE;
static bool s_send_max = false;
static int s_selected_from_idx = -1;
static std::string s_tx_status = "";
static bool s_sending = false;

// Transaction progress state
static double s_status_timestamp = 0.0;
static bool s_status_success = false;
static std::string s_result_txid = "";

static double s_send_start_time = 0.0;

// Undo buffer for clear action
struct FormSnapshot {
    char from_address[512];
    char to_address[512];
    double amount;
    char memo[512];
    double fee;
    bool send_max;
    int selected_from_idx;
};
static FormSnapshot s_undo_snapshot;
static double s_undo_timestamp = 0.0;
static constexpr double UNDO_TIMEOUT = 8.0;

// Fee selection
static int s_fee_tier = 1;  // 0=low, 1=normal, 2=high
static constexpr double FEE_LOW    = DRAGONX_DEFAULT_FEE * 0.5;
static constexpr double FEE_NORMAL = DRAGONX_DEFAULT_FEE;
static constexpr double FEE_HIGH   = DRAGONX_DEFAULT_FEE * 2.0;

static bool s_clear_confirm_pending = false;

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

// Amount input mode: false = DRGX, true = USD
static bool s_input_usd = false;
static double s_usd_amount = 0.0;  // tracks USD input when in USD mode
static bool s_auto_selected = false;  // tracks if auto-selection has been done
static bool s_show_confirm = false;  // persistent: keeps popup open at top-level scope

// ============================================================================
// Helpers
// ============================================================================
static bool FormHasData() {
    return (s_to_address[0] != '\0' || s_amount > 0.0 || s_memo[0] != '\0');
}

static void SaveFormSnapshot() {
    snprintf(s_undo_snapshot.from_address, sizeof(s_undo_snapshot.from_address), "%s", s_from_address);
    snprintf(s_undo_snapshot.to_address, sizeof(s_undo_snapshot.to_address), "%s", s_to_address);
    s_undo_snapshot.amount = s_amount;
    snprintf(s_undo_snapshot.memo, sizeof(s_undo_snapshot.memo), "%s", s_memo);
    s_undo_snapshot.fee = s_fee;
    s_undo_snapshot.send_max = s_send_max;
    s_undo_snapshot.selected_from_idx = s_selected_from_idx;
    s_undo_timestamp = ImGui::GetTime();
}

static void RestoreFormSnapshot() {
    snprintf(s_from_address, sizeof(s_from_address), "%s", s_undo_snapshot.from_address);
    snprintf(s_to_address, sizeof(s_to_address), "%s", s_undo_snapshot.to_address);
    s_amount = s_undo_snapshot.amount;
    snprintf(s_memo, sizeof(s_memo), "%s", s_undo_snapshot.memo);
    s_fee = s_undo_snapshot.fee;
    s_send_max = s_undo_snapshot.send_max;
    s_selected_from_idx = s_undo_snapshot.selected_from_idx;
    s_undo_timestamp = 0.0;
}

static void ClearFormWithUndo() {
    SaveFormSnapshot();
    s_from_address[0] = '\0';
    s_to_address[0] = '\0';
    s_amount = 0.0;
    s_memo[0] = '\0';
    s_fee = DRAGONX_DEFAULT_FEE;
    s_send_max = false;
    s_selected_from_idx = -1;
    s_auto_selected = false;
    s_tx_status = "";
    s_fee_tier = 1;
}

static double GetAvailableBalance(App* app) {
    const auto& state = app->getWalletState();
    // Resolve the balance by the source ADDRESS STRING (the value actually debited on send),
    // not a stored list index. The index desyncs from s_from_address after an address-list
    // refresh, and is left at -1 when the source is chosen from another tab ("Send from this
    // address") — which previously made the sufficiency check see 0 and block a valid send.
    // CONFIRMED balance only — this is the spend ceiling (Max button, slider, validation, pre-broadcast
    // re-check). z_sendmany runs at minconf=1, so offering 0-conf change here would just make the send fail.
    if (s_from_address[0] != '\0') {
        for (const auto& a : state.addresses) {
            if (a.address == s_from_address) return a.spendableBalance;
        }
    }
    if (s_selected_from_idx >= 0 && s_selected_from_idx < static_cast<int>(state.addresses.size())) {
        return state.addresses[s_selected_from_idx].spendableBalance;
    }
    return 0.0;
}

// Recipient validity via the shared, structure-based recognizers: a real encoding-checksum check
// plus the actual DragonX address types — so a transcription error is never "Valid", and a valid
// P2SH/multisig ('b…') recipient is no longer dropped by a hardcoded 'R'-only prefix filter.
static bool IsValidShieldedAddr(const char* a) {
    return a && dragonx::util::isShieldedAddress(a);
}
static bool IsValidTransparentAddr(const char* a) {
    return a && dragonx::util::isTransparentAddress(a);
}

// Source-address balance caption for the "sending from" dropdown: the TOTAL balance is the headline (so
// it matches the Overview figure and a source whose change is still confirming never looks like it lost
// funds), with the confirmed-spendable amount shown as a smaller "(N available)" note ONLY when a pending
// send/receive makes it differ. The Max button + validation still cap spends at the spendable amount.
static std::string FormatSourceBalance(double total, double spendable) {
    char b[128];
    if (total - spendable > 1e-9)
        snprintf(b, sizeof(b), "%.8f %s  (%.8f %s)", total, DRAGONX_TICKER,
                 spendable, TR("send_available_note"));
    else
        snprintf(b, sizeof(b), "%.8f %s", total, DRAGONX_TICKER);
    return b;
}

static std::string timeAgo(int64_t timestamp) {
    return dragonx::util::formatTimeAgoShort(timestamp);
}

static void DrawTxIcon(ImDrawList* dl, const std::string& type,
                       float cx, float cy, float /*s*/, ImU32 col)
{
    using namespace material;
    ImFont* iconFont = Type().iconSmall();
    const char* icon;
    if (type == "send") {
        icon = ICON_MD_CALL_MADE;
    } else if (type == "receive") {
        icon = ICON_MD_CALL_RECEIVED;
    } else {
        icon = ICON_MD_CONSTRUCTION;  // mined
    }
    ImVec2 sz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
    dl->AddText(iconFont, iconFont->LegacySize,
                ImVec2(cx - sz.x * 0.5f, cy - sz.y * 0.5f), col, icon);
}

static void PasteClipboardToAddress() {
    const char* clipboard = ImGui::GetClipboardText();
    if (clipboard) {
        std::string trimmed(clipboard);
        while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
               trimmed.front() == '\n' || trimmed.front() == '\r'))
            trimmed.erase(trimmed.begin());
        while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
               trimmed.back() == '\n' || trimmed.back() == '\r'))
            trimmed.pop_back();
        snprintf(s_to_address, sizeof(s_to_address), "%s", trimmed.c_str());
    }
}

// ============================================================================
// 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 (status bar,
    // balance, explorer, network, mining). The naive blocks/headers ratio over-reports
    // during early sync — misleading on the very screen where the user is about to spend.
    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.send", "sync-banner-bg-color").color)));
    float syncH = std::max(schema::UI().drawElement("tabs.send", "sync-banner-min-height").size, schema::UI().drawElement("tabs.send", "sync-banner-height").size * Layout::vScale());
    ImGui::BeginChild("##SyncBanner", 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();
}

// ============================================================================
// Source Address Dropdown — simple combo selector
// ============================================================================
static void RenderSourceDropdown(App* app, float width) {
    const auto& state = app->getWalletState();
    auto& S = schema::UI();
    char buf[256];

    Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_sending_from"));
    ImGui::Dummy(ImVec2(0, Layout::spacingSm()));

    // Auto-select the address with the largest balance on first load
    if (!s_auto_selected && app->isConnected() && !state.addresses.empty()) {
        int bestIdx = bestSpendableAddressIndex(state.addresses);
        if (bestIdx >= 0) {
            s_selected_from_idx = bestIdx;
            snprintf(s_from_address, sizeof(s_from_address), "%s",
                     state.addresses[bestIdx].address.c_str());
        }
        s_auto_selected = true;
    }

    // Build preview string for selected address
    if (!app->isConnected()) {
        s_source_preview = TR(app->isLiteBuild() ? "lite_no_wallet_short" : "not_connected");
    } else if (s_selected_from_idx >= 0 &&
               s_selected_from_idx < (int)state.addresses.size()) {
        const auto& addr = state.addresses[s_selected_from_idx];
        bool isZ = addr.type == "shielded";
        const char* tag = isZ ? "[Z]" : "[T]";
        std::string trunc = util::truncateMiddle(addr.address,
            static_cast<int>(std::max(S.drawElement("tabs.send", "addr-preview-trunc-min").size, width / S.drawElement("tabs.send", "addr-preview-trunc-divisor").size)));
        snprintf(buf, sizeof(buf), "%s %s  —  %s",
                 tag, trunc.c_str(),
                 FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
        s_source_preview = buf;
    } else {
        s_source_preview = TR("send_select_source");
    }

    ImGui::SetNextItemWidth(width);

    ImGui::PushFont(Type().getFont(TypeStyle::Body2));
    if (ImGui::BeginCombo("##SendFrom", s_source_preview.c_str())) {
        if (!app->isConnected() || state.addresses.empty()) {
            ImGui::TextDisabled("%s", TR("no_addresses_available"));
        } else {
            // List every address that HOLDS a balance (confirmed or still-confirming), sorted by total
            // descending — so a source whose change is pending stays visible with its real total instead
            // of vanishing. The spend gates (Max / validation) below still cap at the spendable amount.
            std::vector<size_t> sortedIdx;
            sortedIdx.reserve(state.addresses.size());
            for (size_t i = 0; i < state.addresses.size(); ++i)
                if (state.addresses[i].isSpendable() && state.addresses[i].balance > 0.0)
                    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("send_no_balance"));
            } else {
                size_t addrTruncLen = static_cast<size_t>(std::max(S.drawElement("tabs.send", "addr-dropdown-trunc-min").size, width / S.drawElement("tabs.send", "addr-dropdown-trunc-divisor").size));

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

                    std::string trunc = util::truncateMiddle(addr.address, (int)addrTruncLen);
                    snprintf(buf, sizeof(buf), "%s %s  —  %s",
                             tag, trunc.c_str(),
                             FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());

                    ImGui::PushID(static_cast<int>(i));
                    if (ImGui::Selectable(buf, isCurrent)) {
                        s_selected_from_idx = static_cast<int>(i);
                        snprintf(s_from_address, sizeof(s_from_address), "%s",
                                 addr.address.c_str());
                    }
                    if (ImGui::IsItemHovered()) {
                        material::Tooltip("%s\n%s",
                            addr.address.c_str(),
                            FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
                    }
                    ImGui::PopID();
                }
            }
        }
        ImGui::EndCombo();
    }
    ImGui::PopFont();
}

// ============================================================================
// Address suggestions dropdown
// ============================================================================
static void RenderAddressSuggestions(const WalletState& state, float width, const char* childId) {
    std::string partial(s_to_address);
    if (partial.length() < 2) return;
    bool is_valid_z = IsValidShieldedAddr(s_to_address);
    bool is_valid_t = IsValidTransparentAddr(s_to_address);
    if (is_valid_z || is_valid_t) return;

    // Memoize the suggestion scan: it walks the whole transaction list, so rebuilding it every
    // frame (the user pauses on a partial address for many frames) is wasted work. Recompute only
    // when the typed text or the transaction count changes.
    static std::vector<std::string> suggestions;
    static std::string s_sugKey;
    const std::string sugKey = partial + "#" + std::to_string(state.transactions.size());
    if (sugKey != s_sugKey) {
        s_sugKey = sugKey;
        suggestions.clear();
        const size_t maxSug = (size_t)schema::UI().drawElement("tabs.send", "max-suggestions").size;
        for (const auto& tx : state.transactions) {
            if (tx.type != "send" || tx.address.empty()) continue;
            if (tx.address.find(partial) != std::string::npos) {
                bool dup = false;
                for (const auto& s : suggestions) { if (s == tx.address) { dup = true; break; } }
                if (!dup) suggestions.push_back(tx.address);
                if (suggestions.size() >= maxSug) break;
            }
        }
    }
    if (suggestions.empty()) return;

    ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.send", "suggestion-bg-color").color)));
    const float dp = Layout::dpiScale();
    float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size * dp + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size * dp, schema::UI().drawElement("tabs.send", "suggestion-max-height").size * dp);
    ImGui::BeginChild(childId, ImVec2(width, sugH), true);
    for (size_t si = 0; si < suggestions.size(); si++) {
        int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
        std::string dispSug = util::truncateMiddle(suggestions[si], sugTrunc > 0 ? sugTrunc : 50);
        ImGui::PushID((int)si);
        if (ImGui::Selectable(dispSug.c_str())) {
            snprintf(s_to_address, sizeof(s_to_address), "%s", suggestions[si].c_str());
        }
        if (ImGui::IsItemHovered()) {
            material::Tooltip("%s", suggestions[si].c_str());
        }
        ImGui::PopID();
    }
    ImGui::EndChild();
    ImGui::PopStyleColor();
}

// ============================================================================
// Fee tier selector
// ============================================================================
static void RenderFeeTierSelector(const char* suffix = "") {
    auto& S = schema::UI();
    const float dp = Layout::dpiScale();
    const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
    const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };

    ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
    ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size);
    for (int fi = 0; fi < 3; fi++) {
        if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size * dp);
        bool active = (s_fee_tier == fi);
        if (active) {
            ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size)));
            ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
        }
        char feeId[32];
        snprintf(feeId, sizeof(feeId), "%s%s", feeLabels[fi], suffix);
        if (TactileSmallButton(feeId, S.resolveFont("button"))) {
            s_fee_tier = fi;
            s_fee = feeValues[fi];
        }
        if (active) ImGui::PopStyleColor(2);
    }
    ImGui::PopStyleVar();
    ImGui::PopStyleColor();
}

// ============================================================================
// Combined amount slider + usage bar
// ============================================================================
static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
                            ImFont* capFont, double usd_price = 0.0,
                            const char* suffix = "") {
    double maxAmount = available - s_fee;
    if (maxAmount < 0) maxAmount = 0;

    float progress = (available > 0)
        ? std::clamp((float)((s_amount + s_fee) / available), 0.0f, 1.0f)
        : 0.0f;

    const float dp = Layout::dpiScale();
    float maxBtnW = schema::UI().drawElement("tabs.send", "amount-bar-max-btn-width").size * dp;
    float gap = Layout::spacingMd();
    float barW = innerW - maxBtnW - gap;
    float minBarW = schema::UI().drawElement("tabs.send", "progress-bar-min-width").size * dp;
    if (barW < minBarW) barW = minBarW;
    float barH = schema::UI().drawElement("tabs.send", "amount-bar-height").size * dp;
    float barRound = barH * 0.5f;

    ImVec2 barMin = ImGui::GetCursorScreenPos();
    ImVec2 barMax(barMin.x + barW, barMin.y + barH);

    // Track background
    dl->AddRectFilled(barMin, barMax, IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "amount-bar-track-alpha").size), barRound);

    // Color-coded fill
    int fillAlpha = (int)schema::UI().drawElement("tabs.send", "progress-fill-alpha").size;
    ImU32 fillCol;
    if (progress < schema::UI().drawElement("tabs.send", "progress-threshold-ok").size)       fillCol = WithAlpha(Success(), fillAlpha);
    else if (progress < schema::UI().drawElement("tabs.send", "progress-threshold-warn").size)  fillCol = WithAlpha(Warning(), fillAlpha);
    else                       fillCol = WithAlpha(Error(), fillAlpha);

    // Map fill so its right-side rounding matches the thumb circle exactly
    float thumbR = barH * 0.5f;
    float usableW = barW - thumbR * 2.0f;
    float thumbCenterX = barMin.x + thumbR + usableW * progress;
    // Extend fill to the right edge of the thumb circle so the rounded
    // corner (radius = barRound = thumbR) produces the same semicircle
    float fillRight = thumbCenterX + thumbR;
    if (fillRight > barMin.x + barRound * 2.0f) {
        dl->AddRectFilled(barMin, ImVec2(fillRight, barMax.y),
                          fillCol, barRound);
    } else if (progress > 0.0f) {
        // Very small fill — just draw a circle at the thumb position
        dl->AddCircleFilled(ImVec2(thumbCenterX, barMin.y + barH * 0.5f),
                            thumbR, fillCol, 24);
    }

    // Percentage label centered on bar
    char pctBuf[32];
    snprintf(pctBuf, sizeof(pctBuf), "%.0f%%", progress * 100.0f);
    ImVec2 textSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, pctBuf);
    float textX = barMin.x + (barW - textSz.x) * 0.5f;
    float textY = barMin.y + (barH - textSz.y) * 0.5f;
    dl->AddText(capFont, capFont->LegacySize, ImVec2(textX, textY),
                IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "progress-pct-text-alpha").size), pctBuf);

    // Invisible button for click-to-set interaction
    char barId[32];
    snprintf(barId, sizeof(barId), "##AmtBar%s", suffix);
    ImGui::InvisibleButton(barId, ImVec2(barW, barH));
    bool barActive = ImGui::IsItemActive();
    bool barHovered = ImGui::IsItemHovered();
    if (barActive) {
        float mouseX = ImGui::GetIO().MousePos.x;
        float clickPct = std::clamp((mouseX - barMin.x) / barW, 0.0f, 1.0f);
        s_amount = maxAmount * clickPct;
        if (s_amount < 0) s_amount = 0;
        s_send_max = (clickPct >= 1.0f);
        // Sync USD amount so the USD input field stays in sync
        if (usd_price > 0.0)
            s_usd_amount = s_amount * usd_price;
        // Recalculate fill position for the thumb while dragging
        progress = (available > 0)
            ? std::clamp((float)((s_amount + s_fee) / available), 0.0f, 1.0f)
            : 0.0f;
    }
    if (barHovered) {
        ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
        char tipBuf[128];
        snprintf(tipBuf, sizeof(tipBuf), "%.8f / %.8f %s (%.1f%%)",
                 s_amount, maxAmount > 0 ? maxAmount : 0.0, DRAGONX_TICKER,
                 progress * 100.0f);
        material::Tooltip("%s", tipBuf);
    }

    // Glass thumb circle at the fill edge
    {
        // thumbCenterX already computed above
        float thumbY = barMin.y + barH * 0.5f;

        // Glass circle body — translucent fill with border
        dl->AddCircleFilled(ImVec2(thumbCenterX, thumbY), thumbR,
                            IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "thumb-fill-alpha").size), 24);
        dl->AddCircle(ImVec2(thumbCenterX, thumbY), thumbR,
                      IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "thumb-border-alpha").size), 24, schema::UI().drawElement("tabs.send", "thumb-border-thickness").size);
    }

    // Max button — use caption font to fit bar height
    ImGui::SameLine(0, gap);
    char maxId[32];
    snprintf(maxId, sizeof(maxId), "%s%s", TR("grpb_max"), suffix);
    if (TactileButton(maxId, ImVec2(maxBtnW, barH), capFont)) {
        s_amount = maxAmount;
        s_send_max = true;
        if (usd_price > 0.0)
            s_usd_amount = s_amount * usd_price;
    }
}

// ============================================================================
// Transaction progress indicator
// ============================================================================
static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
                             ImFont* body2, ImFont* capFont,
                             float cardBottomY = -1.0f) {
    // Auto-clear old success status
    if (!s_tx_status.empty() && s_status_success && s_status_timestamp > 0.0 &&
        (ImGui::GetTime() - s_status_timestamp) > schema::UI().drawElement("tabs.send", "tx-success-timeout").size) {
        s_tx_status.clear();
        s_result_txid.clear();
        s_status_timestamp = 0.0;
    }
    if (s_tx_status.empty() && !s_sending) return;

    const float dp = Layout::dpiScale();

    // Drive error styling from the authoritative result flag, not English substrings in a
    // (translatable) status string — otherwise a failed send renders as a green success
    // under a non-English locale.
    bool is_error = !s_sending && !s_tx_status.empty() && !s_status_success;

    // ---- ERROR: absolute-positioned overlay, does not displace layout ----
    if (is_error) {
        float pad = Layout::spacingLg();
        float btnH = std::max(schema::UI().drawElement("tabs.send", "error-btn-min-height").size, schema::UI().drawElement("tabs.send", "error-btn-height").size) * dp;
        float textWrapW = w - pad * 2 - schema::UI().drawElement("tabs.send", "error-icon-inset").size * dp; // icon space
        ImVec2 textSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, textWrapW, s_tx_status.c_str());
        float contentH = textSz.y + Layout::spacingMd() + btnH + pad * 2;

        // Anchor to bottom of compose card, overlapping upward (position: absolute)
        float anchorY = (cardBottomY > 0) ? cardBottomY : y;
        float overlayY = anchorY - contentH - Layout::spacingMd();
        ImVec2 pMin(x, overlayY);
        ImVec2 pMax(x + w, overlayY + contentH);

        // Save cursor — this overlay must not displace layout
        ImVec2 savedCursor = ImGui::GetCursorScreenPos();

        // Glass card background — matches compose form card style
        GlassPanelSpec errGlass;
        errGlass.rounding = Layout::glassRounding();

        // Use foreground draw list so overlay renders on top of everything
        ImDrawList* fgDl = ImGui::GetForegroundDrawList();

        // Glass panel background
        DrawGlassPanel(fgDl, pMin, pMax, errGlass);

        // Red accent bar on left
        fgDl->AddRectFilled(pMin, ImVec2(pMin.x + schema::UI().drawElement("tabs.send", "error-accent-bar-width").size * dp, pMax.y), Error(), errGlass.rounding);

        float ix = pMin.x + pad;
        float iy = pMin.y + pad;

        // Error icon (Material Design)
        {
            ImFont* iconFont = material::Type().iconMed();
            const char* errIcon = ICON_MD_ERROR;
            ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, errIcon);
            fgDl->AddText(iconFont, iconFont->LegacySize,
                          ImVec2(ix + schema::UI().drawElement("tabs.send", "error-icon-x-offset").size * dp, iy + body2->LegacySize * 0.5f - iSz.y * 0.5f),
                          Error(), errIcon);
        }

        // Error text (wrapped)
        fgDl->AddText(body2, body2->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "error-text-x-offset").size * dp, iy), Error(),
                    s_tx_status.c_str(), s_tx_status.c_str() + s_tx_status.size(), textWrapW);

        // Buttons row — use invisible window for interactive widgets on top of overlay
        float btnY = iy + textSz.y + Layout::spacingMd();
        ImGui::SetNextWindowPos(ImVec2(ix, btnY));
        ImGui::SetNextWindowSize(ImVec2(w - pad * 2, btnH + schema::UI().drawElement("tabs.send", "error-btn-area-padding").size * dp));
        ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
        ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0));
        ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
        ImGui::Begin("##ErrOverlayBtns", nullptr,
            ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
            ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
            ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize |
            ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoFocusOnAppearing |
            ImGuiWindowFlags_NoBringToFrontOnFocus);
        ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-bg-alpha").size)));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-hover-alpha").size)));
        ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "error-btn-rounding").size * dp);
        if (TactileSmallButton(TR("send_copy_error"), schema::UI().resolveFont("button"))) {
            ImGui::SetClipboardText(s_tx_status.c_str());
            Notifications::instance().info(TR("send_error_copied"));
        }
        ImGui::SameLine();
        if (TactileSmallButton(TR("send_dismiss"), schema::UI().resolveFont("button"))) {
            s_tx_status.clear();
            s_result_txid.clear();
            s_status_success = false;
        }
        ImGui::PopStyleVar();
        ImGui::PopStyleColor(2);
        ImGui::End();
        ImGui::PopStyleVar();
        ImGui::PopStyleColor(2);

        // Restore cursor — no layout displacement
        ImGui::SetCursorScreenPos(savedCursor);
        return;
    }

    // ---- SENDING / SUCCESS: inline progress card ----
    float progCardH = schema::UI().drawElement("tabs.send", "progress-card-height").size * dp;
    float progCardHTxid = schema::UI().drawElement("tabs.send", "progress-card-height-txid").size * dp;
    float progH = s_result_txid.empty() ? progCardH : progCardHTxid;
    ImVec2 pMin(x, y);
    ImVec2 pMax(x + w, y + progH);

    GlassPanelSpec progGlass;
    progGlass.rounding = Layout::glassRounding() * schema::UI().drawElement("tabs.send", "progress-glass-rounding-ratio").size;
    DrawGlassPanel(dl, pMin, pMax, progGlass);

    float progPadX = schema::UI().drawElement("tabs.send", "progress-card-pad-x").size * dp;
    float progPadY = schema::UI().drawElement("tabs.send", "progress-card-pad-y").size * dp;
    float ix = pMin.x + progPadX;
    float iy = pMin.y + progPadY;
    char buf[128];

    if (s_sending) {
        // Spinning refresh icon
        ImFont* iconFont = material::Type().iconMed();
        const char* spinIcon = ICON_MD_REFRESH;
        ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, spinIcon);
        dl->AddText(iconFont, iconFont->LegacySize,
                    ImVec2(ix, iy), Primary(), spinIcon);
        double elapsed = ImGui::GetTime() - s_send_start_time;
        snprintf(buf, sizeof(buf), TR("send_submitting"), elapsed);
        dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size * dp, iy), OnSurface(), buf);
    } else {
        // Success checkmark
        ImFont* iconFont = material::Type().iconMed();
        const char* checkIcon = ICON_MD_CHECK;
        ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, checkIcon);
        dl->AddText(iconFont, iconFont->LegacySize,
                    ImVec2(ix, iy), Success(), checkIcon);
        dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size * dp, iy), Success(), TR("send_tx_sent"));

        if (!s_result_txid.empty()) {
            float txY = iy + body2->LegacySize + schema::UI().drawElement("tabs.send", "txid-y-offset").size * dp;
            int txidThreshold = (int)schema::UI().drawElement("tabs.send", "txid-display-threshold").size;
            int txidTruncLen = (int)schema::UI().drawElement("tabs.send", "txid-trunc-len").size;
            std::string dispTxid = (int)s_result_txid.length() > txidThreshold
                ? s_result_txid.substr(0, txidTruncLen) + "..." + s_result_txid.substr(s_result_txid.length() - txidTruncLen)
                : s_result_txid;
            snprintf(buf, sizeof(buf), TR("send_txid_label"), dispTxid.c_str());
            dl->AddText(capFont, capFont->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "txid-label-x-offset").size * dp, txY),
                        OnSurfaceDisabled(), buf);
            ImGui::SetCursorScreenPos(ImVec2(pMax.x - schema::UI().drawElement("tabs.send", "txid-copy-btn-right-offset").size * dp, txY - schema::UI().drawElement("tabs.send", "txid-copy-btn-y-offset").size * dp));
            if (TactileSmallButton(TR("copy"), schema::UI().resolveFont("button"))) {
                ImGui::SetClipboardText(s_result_txid.c_str());
                Notifications::instance().info(TR("send_txid_copied"));
            }
        }
    }

    ImGui::SetCursorScreenPos(ImVec2(pMin.x, pMax.y));
    ImGui::Dummy(ImVec2(w, 0));
}

// ============================================================================
// Confirmation dialog
// ============================================================================
void SweepShowSendConfirm(App* app, bool show) {
    (void)app;
    if (show) {
        std::snprintf(s_from_address, sizeof(s_from_address), "%s",
                      "zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000");
        std::snprintf(s_to_address, sizeof(s_to_address), "%s",
                      "zs1demopeerreceivingaddressabcdef0000000000000000000000000000000");
        s_amount = 2.50000000; s_fee = DRAGONX_DEFAULT_FEE;
        std::snprintf(s_memo, sizeof(s_memo), "%s", "thanks!");
        s_show_confirm = true;   // RenderSendConfirmPopup re-issues OpenPopup while this is set
    } else {
        s_show_confirm = false;
        s_from_address[0] = s_to_address[0] = s_memo[0] = '\0';
        s_amount = 0.0;
    }
}

void RenderSendConfirmPopup(App* app) {
    if (!s_show_confirm) return;

    // Called every frame while the popup should be visible.
    // OpenPopup is idempotent when the popup is already open.
    ImGui::OpenPopup(TR("confirm_send"));
    bool is_valid_z = IsValidShieldedAddr(s_to_address);
    auto& S = schema::UI();
    const auto& state = app->getWalletState();
    const auto& market = state.market;
    ImFont* capFont = Type().caption();
    ImFont* sub1    = Type().subtitle1();
    ImFont* body2   = Type().body2();
    char buf[128];

    double total = s_amount + s_fee;

    float popupAvailW = ImGui::GetMainViewport()->Size.x * S.drawElement("tabs.send", "confirm-popup-width-ratio").size;
    float popupW = std::min(schema::UI().drawElement("tabs.send", "confirm-popup-max-width").size, popupAvailW);
    float popVs = Layout::vScale();
    const float dp = Layout::dpiScale();
    material::OverlayDialogSpec ov;
    ov.title = TR("confirm_send"); ov.p_open = nullptr;
    ov.style = material::OverlayStyle::BlurFloat;
    ov.cardWidth = popupW; ov.idSuffix = "confirmsend";
    if (material::BeginOverlayDialog(ov)) {

        if (ImGui::IsKeyPressed(ImGuiKey_Escape) && !s_sending) {
            s_show_confirm = false;
        }

        float popW = ImGui::GetContentRegionAvail().x;
        ImDrawList* popDl = ImGui::GetWindowDrawList();

        float addrCardH = std::max(schema::UI().drawElement("tabs.send", "confirm-addr-card-min-height").size, schema::UI().drawElement("tabs.send", "confirm-addr-card-height").size * popVs);
        float popGlassRound = Layout::glassRounding() * S.drawElement("tabs.send", "confirm-glass-rounding-ratio").size;

        // FROM card
        {
            Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("from_upper"));
            ImVec2 cMin = ImGui::GetCursorScreenPos();
            ImVec2 cMax(cMin.x + popW, cMin.y + addrCardH);
            GlassPanelSpec gs; gs.rounding = popGlassRound;
            DrawGlassPanel(popDl, cMin, cMax, gs);
            popDl->AddText(body2, body2->LegacySize,
                ImVec2(cMin.x + Layout::spacingMd(), cMin.y + Layout::spacingSm()),
                Success(), util::truncateMiddle(s_from_address, (int)S.drawElement("tabs.send", "confirm-addr-trunc-len").size).c_str());
            ImGui::Dummy(ImVec2(popW, addrCardH));
            ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
        }

        // TO card
        {
            Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("to_upper"));
            ImVec2 cMin = ImGui::GetCursorScreenPos();
            ImVec2 cMax(cMin.x + popW, cMin.y + addrCardH);
            GlassPanelSpec gs; gs.rounding = popGlassRound;
            DrawGlassPanel(popDl, cMin, cMax, gs);
            popDl->AddText(body2, body2->LegacySize,
                ImVec2(cMin.x + Layout::spacingMd(), cMin.y + Layout::spacingSm()),
                Success(), util::truncateMiddle(s_to_address, (int)S.drawElement("tabs.send", "confirm-addr-trunc-len").size).c_str());
            ImGui::Dummy(ImVec2(popW, addrCardH));
            ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
        }

        // Fee tier selector
        {
            Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_network_fee"));
            ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
            double feeBefore = s_fee;
            RenderFeeTierSelector("##confirm");
            // If Max was selected, a fee bump here would push total over the available
            // balance (s_amount was clamped to available - old fee). Re-clamp so the send
            // stays within budget.
            if (s_send_max && s_fee != feeBefore) {
                double avail = GetAvailableBalance(app);
                double maxAmount = avail - s_fee;
                if (maxAmount < 0) maxAmount = 0;
                s_amount = maxAmount;
            }
            // Recalculate total after potential fee change
            total = s_amount + s_fee;
            ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
        }

        // Amount / Fee / Total summary
        {
            float valX = std::max(schema::UI().drawElement("tabs.send", "confirm-val-col-min-x").size, schema::UI().drawElement("tabs.send", "confirm-val-col-x").size * popVs);
            float usdX = popW - std::max(schema::UI().drawElement("tabs.send", "confirm-usd-col-min-x").size, schema::UI().drawElement("tabs.send", "confirm-usd-col-x").size * popVs);

            Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_details"));
            ImVec2 cMin = ImGui::GetCursorScreenPos();
            // Floor the row step at the *actual* scaled caption-row height (+ a small dp-scaled
            // gap) so the Fee/Total divider — drawn at rowStep*0.5 below the Fee baseline —
            // clears the (already-DPI-scaled) fee text instead of striking through it at HiDPI.
            // capFont->LegacySize is already DPI-scaled; do not scale it again.
            float rowStep = std::max(schema::UI().drawElement("tabs.send", "confirm-row-step-min").size, schema::UI().drawElement("tabs.send", "confirm-row-step").size * popVs);
            rowStep = std::max(rowStep, capFont->LegacySize + 6.0f * dp);
            float configuredH = std::max(schema::UI().drawElement("tabs.send", "confirm-amount-card-min-height").size, schema::UI().drawElement("tabs.send", "confirm-amount-card-height").size * popVs);
            float contentH = Layout::spacingMd() * 2.0f + capFont->LegacySize * 2.0f + sub1->LegacySize + rowStep * 2.0f;
            float cH = std::max(configuredH, contentH);
            ImVec2 cMax(cMin.x + popW, cMin.y + cH);
            GlassPanelSpec gs; gs.rounding = popGlassRound;
            DrawGlassPanel(popDl, cMin, cMax, gs);

            float cx = cMin.x + Layout::spacingLg();
            float cy = cMin.y + Layout::spacingMd();

            popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_amount"));
            snprintf(buf, sizeof(buf), "%.8f %s", s_amount, DRAGONX_TICKER);
            popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + valX, cy), OnSurface(), buf);
            if (market.price_usd > 0) {
                snprintf(buf, sizeof(buf), "$%.4f", s_amount * market.price_usd);
                popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + usdX, cy), OnSurfaceDisabled(), buf);
            }
            cy += rowStep;

            popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_fee"));
            snprintf(buf, sizeof(buf), "%.8f %s", s_fee, DRAGONX_TICKER);
            popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + valX, cy), OnSurface(), buf);
            if (market.price_usd > 0) {
                snprintf(buf, sizeof(buf), "$%.6f", s_fee * market.price_usd);
                popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + usdX, cy), OnSurfaceDisabled(), buf);
            }
            cy += rowStep * 0.5f;
            popDl->AddLine(ImVec2(cx, cy),
                ImVec2(cMax.x - Layout::spacingLg(), cy),
                ImGui::GetColorU32(Divider()), S.drawElement("tabs.send", "confirm-divider-thickness").size);
            cy += rowStep * 0.5f;

            popDl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_total"));
            snprintf(buf, sizeof(buf), "%.8f %s", total, DRAGONX_TICKER);
            DrawTextShadow(popDl, sub1, sub1->LegacySize, ImVec2(cx + valX, cy), Primary(), buf);
            if (market.price_usd > 0) {
                snprintf(buf, sizeof(buf), "$%.4f", total * market.price_usd);
                popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + usdX, cy + S.drawElement("tabs.send", "confirm-usd-y-offset").size), OnSurfaceDisabled(), buf);
            }
            ImGui::Dummy(ImVec2(popW, cH));
            ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
        }

        if (s_memo[0] != '\0' && is_valid_z) {
            Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_upper"));
            Type().textColored(TypeStyle::Caption, OnSurface(), s_memo);
            ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
        }

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

        if (s_sending) {
            Type().text(TypeStyle::Body2, TR("sending"));
        } else {
            // Size to max(schema width, measured label + padding) so a longer translation (e.g. the
            // Russian "Подтвердить и отправить") isn't clipped on this pre-broadcast confirm button.
            ImFont* confirmFont = S.resolveFont(S.button("tabs.send", "confirm-button").font);
            if (!confirmFont) confirmFont = Type().button();
            const float confirmW = std::max(
                S.button("tabs.send", "confirm-button").width * Layout::dpiScale(),
                confirmFont->CalcTextSizeA(confirmFont->LegacySize, FLT_MAX, 0, TR("confirm_and_send")).x
                + ImGui::GetStyle().FramePadding.x * 2.0f + 16.0f * Layout::dpiScale());
            const float confirmH = std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size,
                                            schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs);
            if (TactileButton(TR("confirm_and_send"), ImVec2(confirmW, confirmH), confirmFont)) {
              // Re-validate against LIVE state — the confirm dialog persists across frames, so the
              // balance could have dropped or sync (re)started (or the fee bumped total over available)
              // since Review. Don't broadcast a now-invalid transaction.
              const auto& liveState = app->getWalletState();
              double liveAvail = GetAvailableBalance(app);
              if (!app->isConnected() || liveState.sync.syncing || s_amount <= 0 ||
                  (s_amount + s_fee) > liveAvail) {
                s_tx_status = TR("send_cannot_send_now");
                s_status_success = false;
                s_status_timestamp = ImGui::GetTime();
              }
              // A locked encrypted lite wallet can't spend; prompt to unlock instead of sending
              // (the backend would otherwise reject with "Wallet is locked").
              else if (app->liteWallet() && app->getWalletState().isLocked()) {
                app->requestLiteUnlock();
              } else {
                s_sending = true;
                s_send_start_time = ImGui::GetTime();
                s_tx_status = std::string(TR("sending")) + "...";

                std::string memo_str = (is_valid_z && s_memo[0]) ? s_memo : "";

                app->sendTransaction(
                    s_from_address,
                    s_to_address,
                    s_amount,
                    s_fee,
                    memo_str,
                    [](bool success, const std::string& result) {
                        s_sending = false;
                        s_status_timestamp = ImGui::GetTime();
                        if (success) {
                            s_tx_status = TR("send_tx_sent");
                            s_result_txid = result;
                            s_status_success = true;
                            Notifications::instance().success(TR("send_tx_success"), 5.0f);
                            s_to_address[0] = '\0';
                            s_amount = 0.0;
                            s_memo[0] = '\0';
                            s_send_max = false;
                        } else {
                            s_tx_status = std::string(TR("send_error_prefix")) + result;
                            s_result_txid.clear();
                            s_status_success = false;
                            Notifications::instance().error(std::string(TR("send_tx_failed")) + result);
                        }
                    }
                );
                s_show_confirm = false;
              }
            }
            ImGui::SameLine();
            if (TactileButton(TR("cancel"), ImVec2(S.button("tabs.send", "cancel-button").width * Layout::dpiScale(), std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
                s_show_confirm = false;
            }
        }

        material::EndOverlayDialog();
    }
}

// ============================================================================
// Zero balance CTA
// ============================================================================
static bool RenderZeroBalanceCTA(App* app, ImDrawList* dl, float width) {
    const auto& state = app->getWalletState();
    double totalBal = 0.0;
    for (const auto& a : state.addresses) totalBal += a.balance;
    if (!app->isConnected() || state.addresses.empty() || totalBal > 0.0)
        return false;

    ImFont* sub1 = Type().subtitle1();
    ImFont* capFont = Type().caption();
    const float dp = Layout::dpiScale();

    ImVec2 ctaMin = ImGui::GetCursorScreenPos();
    float ctaH = schema::UI().drawElement("tabs.send", "cta-height").size * dp;
    ImVec2 ctaMax(ctaMin.x + width, ctaMin.y + ctaH);
    GlassPanelSpec ctaGlass;
    ctaGlass.rounding = Layout::glassRounding();
    DrawGlassPanel(dl, ctaMin, ctaMax, ctaGlass);

    float cx = ctaMin.x + Layout::spacingXl();
    float cy = ctaMin.y + Layout::spacingLg();
    dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurface(), TR("send_wallet_empty"));
    cy += sub1->LegacySize + Layout::spacingSm();
    dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(),
        TR("send_switch_to_receive"));
    cy += capFont->LegacySize + Layout::spacingMd();
    ImGui::SetCursorScreenPos(ImVec2(cx, cy));
    if (TactileButton(TR("send_go_to_receive"), ImVec2(schema::UI().drawElement("tabs.send", "cta-button-width").size * dp, schema::UI().drawElement("tabs.send", "cta-button-height").size * dp), schema::UI().resolveFont("button"))) {
        app->setCurrentPage(NavPage::Receive);
    }
    ImGui::SetCursorScreenPos(ImVec2(ctaMin.x, ctaMax.y + Layout::spacingLg()));
    ImGui::Dummy(ImVec2(width, 0));
    return true;
}

// ============================================================================
// Action buttons (Send + Clear + Undo)
// ============================================================================
static void RenderActionButtons(App* app, float width, float vScale,
                                bool is_valid_address, double available,
                                const char* suffix = "") {
    auto& S = schema::UI();
    const float dp = Layout::dpiScale();
    const auto& state = app->getWalletState();
    double total = s_amount + s_fee;
    // Block spending from a view-only source (imported viewing key, no spending key) — it would only
    // fail at submit. Defaults to spendable if the address isn't in the loaded list yet.
    bool sourceSpendable = true;
    for (const auto& a : state.addresses) {
        if (a.address == s_from_address) { sourceSpendable = a.has_spending_key; break; }
    }
    bool can_send = app->isConnected() &&
                    !state.sync.syncing &&
                    is_valid_address &&
                    s_amount > 0 &&
                    s_from_address[0] != '\0' &&
                    total <= available &&
                    sourceSpendable &&
                    !s_sending;

    float btnGap = Layout::spacingMd();
    float cancelBtnW = std::max(schema::UI().drawElement("tabs.send", "cancel-btn-min-width").size, width * S.drawElement("tabs.send", "cancel-btn-width-ratio").size);
    float sendBtnW = width - cancelBtnW - btnGap;
    float btnH = std::max(schema::UI().drawElement("tabs.send", "action-btn-min-height").size, schema::UI().drawElement("tabs.send", "action-btn-height").size * vScale);

    // ---- Send button ----
    if (!can_send) {
        // The disabled fill must contrast the surface: a white overlay reads on dark skins but vanished
        // on the light/pastel skins (white-on-white). Use a dark overlay for light themes.
        int dAlpha = (int)S.drawElement("tabs.send", "disabled-btn-bg-alpha").size;
        ImU32 disFill = IsLightTheme() ? IM_COL32(0, 0, 0, dAlpha) : IM_COL32(255, 255, 255, dAlpha);
        ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(disFill));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(disFill));
        ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
    }

    ImGui::BeginDisabled(!can_send);
    char sendId[64];
    snprintf(sendId, sizeof(sendId), "%s##ReviewSend%s", TR("review_send"), suffix);
    if (TactileButton(sendId, ImVec2(sendBtnW, btnH), S.resolveFont(S.button("tabs.send", "send-button").font))) {
        s_show_confirm = true;
    }
    ImGui::EndDisabled();

    if (!can_send && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
        if (!app->isConnected())
            material::Tooltip("%s", TR(app->isLiteBuild() ? "lite_no_wallet_short" : "send_tooltip_not_connected"));
        else if (state.sync.syncing)
            material::Tooltip("%s", TR("send_tooltip_syncing"));
        else if (s_from_address[0] == '\0')
            material::Tooltip("%s", TR("send_tooltip_select_source"));
        else if (!is_valid_address)
            material::Tooltip("%s", TR("send_tooltip_invalid_address"));
        else if (s_amount <= 0)
            material::Tooltip("%s", TR("send_tooltip_enter_amount"));
        else if (total > available)
            material::Tooltip("%s", TR("send_tooltip_exceeds_balance"));
        else if (!sourceSpendable)
            material::Tooltip("%s", TR("send_tooltip_view_only"));
        else if (s_sending)
            material::Tooltip("%s", TR("send_tooltip_in_progress"));
    }
    if (!can_send) ImGui::PopStyleColor(3);

    // ---- Cancel button (same line) ----
    ImGui::SameLine(0, btnGap);
    ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "cancel-btn-hover-alpha").size)));
    ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
    ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.send", "cancel-btn-border-size").size);
    char clearId[64];
    snprintf(clearId, sizeof(clearId), "%s##Cancel%s", TR("cancel"), suffix);
    if (TactileButton(clearId, ImVec2(cancelBtnW, btnH), S.resolveFont(S.button("tabs.send", "clear-button").font))) {
        if (FormHasData()) {
            s_clear_confirm_pending = true;
        } else {
            ClearFormWithUndo();
        }
    }
    ImGui::PopStyleVar(); // FrameBorderSize
    ImGui::PopStyleColor(3);

    // Clear confirmation popup
    char confirmClearId[32];
    snprintf(confirmClearId, sizeof(confirmClearId), "##ConfirmClear%s", suffix);
    if (s_clear_confirm_pending) {
        ImGui::OpenPopup(confirmClearId);
        s_clear_confirm_pending = false;
    }
    if (ImGui::BeginPopup(confirmClearId)) {
        ImGui::Text("%s", TR("send_clear_fields"));
        ImGui::Spacing();
        if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size * dp, 0), S.resolveFont("button"))) {
            ClearFormWithUndo();
            ImGui::CloseCurrentPopup();
        }
        ImGui::SameLine();
        if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size * dp, 0), S.resolveFont("button"))) {
            ImGui::CloseCurrentPopup();
        }
        ImGui::EndPopup();
    }

    // Undo button (shown briefly after clear)
    if (s_undo_timestamp > 0.0) {
        double undoElapsed = ImGui::GetTime() - s_undo_timestamp;
        if (undoElapsed < UNDO_TIMEOUT) {
            ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
            float undoAlpha = (float)std::max(0.0, 1.0 - undoElapsed / UNDO_TIMEOUT);
            ImGui::PushStyleVar(ImGuiStyleVar_Alpha, undoAlpha);
            ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-bg-alpha").size)));
            ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size)));
            char undoId[32];
            snprintf(undoId, sizeof(undoId), "%s%s", TR("grpb_undo_clear"), suffix);
            if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
                RestoreFormSnapshot();
                Notifications::instance().info(TR("send_form_restored"));
            }
            ImGui::PopStyleColor(2);
            ImGui::PopStyleVar();
        } else {
            s_undo_timestamp = 0.0;
        }
    }
}

// ============================================================================
// Recent Sends section — styled to match transactions list
// ============================================================================
static void RenderRecentSends(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("send_recent_sends"));
    ImGui::Dummy(ImVec2(0, Layout::spacingXs()));

    ImVec2 avail = ImGui::GetContentRegionAvail();
    float hs = Layout::hScale(avail.x);
    float vs = Layout::vScale(std::max(1.0f, avail.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 sendCol = Error();

    // Grow the list to fill the dead space beneath the (fixed-height) compose 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.
    // Row budget is hoisted above the collect loop (it only needs the remaining region
    // height + row height, no per-row state) so the scan can early-exit.
    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 scan only a
    // bounded prefix and stop once maxRows matches are gathered (mirrors the early-exit in
    // balance_components.cpp:RenderSharedRecentTx) instead of filtering the whole history.
    std::vector<const TransactionInfo*> sends;
    sends.reserve(maxRows);
    for (const auto& tx : state.transactions) {
        if (tx.type != "send" && tx.type != "shield") continue;
        sends.push_back(&tx);
        if (sends.size() == maxRows) break;  // newest-first: enough rows to fill the region
    }

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

    char buf[64];
    if (sends.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_MADE, TR("send_no_recent"));
        ImGui::EndChild();
        return;
    }

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

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

        // Icon
        DrawTxIcon(rowDL, "send", rowPos.x + Layout::spacingMd(), rowY, iconSz, sendCol);

        // Type label (first line)
        float txX = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
        rowDL->AddText(capFont, capFont->LegacySize,
                       ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), TR("sent_type"));

        // Address (second line)
        float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs;
        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());

        // Amount (right-aligned, first line)
        snprintf(buf, sizeof(buf), "-%.4f %s", std::abs(tx.amount), DRAGONX_TICKER);
        ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, buf);
        float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
        float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size,
                                                     S.drawElement("tabs.balance", "amount-right-margin").size * hs);
        rowDL->AddText(capFont, capFont->LegacySize,
                       ImVec2(amtX, rowPos.y + 2.0f * dp), sendCol, buf);

        // Time ago
        std::string ago = timeAgo(tx.timestamp);
        ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
        rowDL->AddText(capFont, capFont->LegacySize,
                       ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f) * hs,
                              rowPos.y + 2.0f * dp),
                       OnSurfaceDisabled(), ago.c_str());

        // 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: RenderSendTab — Layout E: Top Bar Source + Bottom Form
// ============================================================================
void RenderSendTab(App* app)
{
    const auto& state = app->getWalletState();
    const auto& market = state.market;
    auto& S = schema::UI();

    // Handle pending payment from URI
    if (app->hasPendingPayment()) {
        // A URI arriving mid-compose would silently overwrite the user's in-progress form.
        // Capture an undo snapshot first so "Undo" can restore what they were typing.
        if (FormHasData()) {
            SaveFormSnapshot();
        }
        strncpy(s_to_address, app->getPendingToAddress().c_str(), sizeof(s_to_address) - 1);
        s_to_address[sizeof(s_to_address) - 1] = '\0';
        s_amount = app->getPendingAmount();
        strncpy(s_memo, app->getPendingMemo().c_str(), sizeof(s_memo) - 1);
        s_memo[sizeof(s_memo) - 1] = '\0';
        app->clearPendingPayment();
    }

    RenderSyncBanner(state);

    bool sendSyncing = state.sync.syncing && !state.sync.isSynced();
    ImGui::BeginDisabled(sendSyncing);

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

    float availWidth = sendAvail.x;

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

    double available = GetAvailableBalance(app);
    bool is_valid_z = IsValidShieldedAddr(s_to_address);
    bool is_valid_t = IsValidTransparentAddr(s_to_address);
    bool is_valid_address = is_valid_z || is_valid_t;
    float sectionGap = Layout::spacingXl() * vScale;

    char buf[128];

    // ================================================================
    // SCROLLABLE CONTENT
    // ================================================================
    ImVec2 formAvail = ImGui::GetContentRegionAvail();
    // NOTE: no NoScrollbar/NoScrollWithMouse here (mirrors receive_tab's ##ReceiveScroll).
    // At font_scale 1.5 the form card grows to mainCardTargetH and pushes the appended
    // "Recent Sends" list below the fold; letting this child scroll keeps it reachable.
    // No-op at 1.0x where the content already fits (no scrollbar appears).
    ImGui::BeginChild("##SendFormScroll", formAvail, false,
        ImGuiWindowFlags_NoBackground);
    dl = ImGui::GetWindowDrawList();

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

    float formAvailW = ImGui::GetContentRegionAvail().x;
    // Fill the available column up to the content-max-width cap, then center. Shared with the
    // Receive tab via mainComposeCardBox() so the two card envelopes are identical in width and
    // position (Send previously capped at 760dp vs Receive's 860dp, so Send rendered narrower).
    // The recent-sends list below deliberately keeps the full column width (formAvailW).
    Layout::CardBox formBox = Layout::mainComposeCardBox(formAvailW);
    float formCardW = formBox.width;
    float formOffsetX = formBox.offsetX;
    float formW = formAvailW;
    ImGui::BeginGroup();

    // Zero balance CTA
    if (RenderZeroBalanceCTA(app, dl, formW)) {
        ImGui::EndGroup();
        ImGui::EndDisabled(); // sendSyncing guard
        ImGui::EndChild();
        return;
    }

    // ================================================================
    // COMPOSE FORM — single container for all fields
    // ================================================================
    // Full-column left edge, restored after the centered card so the recent-sends
    // list below spans the full width again.
    float formLeftX = ImGui::GetCursorPosX();
    {
        // Center the capped compose card: offset the cursor by the leftover half-margin,
        // then derive every field/divider/button from the card width (not the full column).
        float formW = formCardW;
        if (formOffsetX > 0.0f)
            ImGui::SetCursorPosX(ImGui::GetCursorPosX() + formOffsetX);
        ImVec2 containerMin = ImGui::GetCursorScreenPos();
        float pad = Layout::spacingLg();
        float innerW = formW - pad * 2;
        float innerGap = Layout::spacingMd() * vScale;

        // Single-column layout
        float colW = innerW;

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

        // Indent content by pad so every line is inset from the card edges. Fold in formOffsetX so the
        // auto-layout fields land inside the centered card (Indent() positions from the window's left, so
        // without this the fields would sit at the un-offset column while the glass card is centered).
        ImGui::Indent(pad + formOffsetX);
        ImGui::Dummy(ImVec2(0, pad * vScale)); // top padding

        // ---- SOURCE ADDRESS ----
        RenderSourceDropdown(app, colW);

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

        // ---- RECIPIENT ----
        {
            // Static preview state (declared early for title indicator)
            static bool s_paste_previewing = false;
            static std::string s_preview_text;

            Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_recipient"));

            // Validation indicator — inline next to title (no height change)
            // Check the preview text during hover, otherwise check actual address
            const char* validAddr = s_to_address;
            bool checkPreview = false;
            if (s_paste_previewing && !s_preview_text.empty()) {
                validAddr = s_preview_text.c_str();
                checkPreview = true;
            }
            if (validAddr[0] != '\0') {
                bool vz = IsValidShieldedAddr(validAddr);
                bool vt = IsValidTransparentAddr(validAddr);
                if (vz || vt) {
                    ImGui::SameLine();
                    if (vz)
                        Type().textColored(TypeStyle::Caption, Success(), TR("send_valid_shielded"));
                    else
                        Type().textColored(TypeStyle::Caption, Warning(), TR("send_valid_transparent"));
                } else if (!checkPreview) {
                    ImGui::SameLine();
                    Type().textColored(TypeStyle::Caption, Error(), TR("invalid_address"));
                }
            }

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

            float pasteW = std::max(schema::UI().drawElement("tabs.send", "paste-btn-min-width").size, colW * schema::UI().drawElement("tabs.send", "paste-btn-width-ratio").size);
            float contactsW = ImGui::GetFrameHeight();  // compact square icon button for the contact picker
            // Reserve the TWO real SameLine gaps (each = ItemSpacing.x) between input|Paste|icon.
            // Reserving spacingSm (a smaller token) under-counted the gap, so the row overshot colW by
            // ~2*(ItemSpacing.x - spacingSm) and the icon's right border clipped past the card edge.
            ImGui::PushItemWidth(colW - pasteW - contactsW - ImGui::GetStyle().ItemSpacing.x * 2.0f);

            // Show clipboard preview as transparent overlay when paste button is hovered
            bool paste_hovered = false;

            ImGui::InputText("##ToAddr", s_to_address, sizeof(s_to_address));
            // Capture input rect for preview overlay
            ImVec2 inputMin = ImGui::GetItemRectMin();
            ImGui::PopItemWidth();
            ImGui::SameLine();

            // Detect hover BEFORE the button click
            ImVec2 pasteBtnPos = ImGui::GetCursorScreenPos();
            ImVec2 pasteBtnSize(pasteW, ImGui::GetFrameHeight());
            paste_hovered = material::IsRectHovered(pasteBtnPos,
                ImVec2(pasteBtnPos.x + pasteBtnSize.x, pasteBtnPos.y + pasteBtnSize.y));

            // Handle preview state — don't modify s_to_address, just store preview text
            if (paste_hovered && !s_paste_previewing) {
                const char* clip = ImGui::GetClipboardText();
                if (clip && clip[0] != '\0') {
                    std::string trimmed(clip);
                    while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\n' || trimmed.front() == '\r' || trimmed.front() == '\t'))
                        trimmed.erase(trimmed.begin());
                    while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\n' || trimmed.back() == '\r' || trimmed.back() == '\t'))
                        trimmed.pop_back();
                    bool looksValid = dragonx::util::isValidRecipientAddress(trimmed);
                    if (looksValid && s_to_address[0] == '\0') {
                        s_preview_text = trimmed;
                        s_paste_previewing = true;
                    }
                }
            } else if (!paste_hovered && s_paste_previewing) {
                s_paste_previewing = false;
                s_preview_text.clear();
            }

            // Draw transparent preview text overlay on the input field
            if (s_paste_previewing && !s_preview_text.empty()) {
                ImVec2 textPos(inputMin.x + ImGui::GetStyle().FramePadding.x,
                               inputMin.y + ImGui::GetStyle().FramePadding.y);
                ImVec4 previewCol = ImGui::GetStyleColorVec4(ImGuiCol_Text);
                previewCol.w = S.drawElement("tabs.send", "paste-preview-alpha").size;
                ImGui::GetWindowDrawList()->AddText(textPos, ImGui::ColorConvertFloat4ToU32(previewCol),
                    s_preview_text.c_str(), s_preview_text.c_str() + std::min(s_preview_text.size(), (size_t)S.drawElement("tabs.send", "paste-preview-max-chars").size));
            }

            if (TactileButton((std::string(TR("paste")) + "##to").c_str(), ImVec2(pasteW, 0), S.resolveFont(S.button("tabs.send", "paste-button").font))) {
                if (s_paste_previewing) {
                    // Commit the preview
                    snprintf(s_to_address, sizeof(s_to_address), "%s", s_preview_text.c_str());
                    s_paste_previewing = false;
                    s_preview_text.clear();
                } else {
                    PasteClipboardToAddress();
                }
            }

            // Contact picker — pick a saved contact's address as the recipient. Pin the height to the
            // frame height (== the input + Paste height) so the larger iconMed font doesn't auto-size
            // this square button taller than its row-mates. (Passing a non-zero size also routes
            // TactileButton through its precise InvisibleButton + centered-glyph path.)
            ImGui::SameLine();
            if (material::TactileButton(ICON_MD_CONTACTS "##pickContact", ImVec2(contactsW, ImGui::GetFrameHeight()),
                                        material::Type().iconMed()))
                ImGui::OpenPopup("##ContactPickerPopup");
            if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("send_contacts_button"));
            ContactPickerPopup("##ContactPickerPopup", app->addressBook(),
                               s_to_address, sizeof(s_to_address));

            // Recently sent-to suggestions
            RenderAddressSuggestions(state, colW, "##AddrSugForm");
        }

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

        // ---- AMOUNT ----
        {
            Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_upper"));
            ImGui::Dummy(ImVec2(0, Layout::spacingSm()));

            // Toggle between DRGX and USD input
            float toggleW = schema::UI().drawElement("tabs.send", "toggle-currency-width").size * Layout::dpiScale();
            float amtInputW = colW - toggleW - Layout::spacingMd();
            if (amtInputW < schema::UI().drawElement("tabs.send", "amount-input-min-width").size) amtInputW = schema::UI().drawElement("tabs.send", "amount-input-min-width").size;

            if (s_input_usd && market.price_usd > 0) {
                // USD input mode — no step buttons (step=0)
                ImGui::PushItemWidth(amtInputW);
                if (ImGui::InputDouble("##AmountUSD", &s_usd_amount, 0, 0, "$%.2f")) {
                    // Normalize to 8dp (satoshi precision) so the reviewed/sent DRGX matches the preview.
                    s_amount = std::round((s_usd_amount / market.price_usd) * 1e8) / 1e8;
                }
                // Draw DRGX equivalent inside the input field (right-aligned overlay)
                {
                    ImVec2 iMin = ImGui::GetItemRectMin();
                    ImVec2 iMax = ImGui::GetItemRectMax();
                    // Show full 8-dp precision so the inline DRGX preview matches the confirm
                    // panel and the amount actually sent (was %.4f — disagreed with the send).
                    snprintf(buf, sizeof(buf), "\xe2\x89\x88 %.8f %s", s_amount, DRAGONX_TICKER);
                    ImFont* font = capFont;
                    ImVec2 sz = font->CalcTextSizeA(font->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(font, font->LegacySize, ImVec2(tx, ty),
                                IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "input-overlay-text-alpha").size), buf);
                }
                ImGui::PopItemWidth();
            } else {
                // DRGX input mode — no step buttons (step=0)
                ImGui::PushItemWidth(amtInputW);
                if (ImGui::InputDouble("##Amount", &s_amount, 0, 0, "%.8f")) {
                    // Normalize to 8dp so digits past satoshi precision aren't silently dropped at send.
                    s_amount = std::round(s_amount * 1e8) / 1e8;
                    if (s_amount < 0) s_amount = 0;
                    if (market.price_usd > 0)
                        s_usd_amount = s_amount * market.price_usd;
                }
                // Draw USD equivalent inside the input field (right-aligned overlay)
                if (market.price_usd > 0 && s_amount > 0) {
                    ImVec2 iMin = ImGui::GetItemRectMin();
                    ImVec2 iMax = ImGui::GetItemRectMax();
                    double usd_value = s_amount * market.price_usd;
                    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);
                    ImFont* font = capFont;
                    ImVec2 sz = font->CalcTextSizeA(font->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(font, font->LegacySize, ImVec2(tx, ty),
                                IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "input-overlay-text-alpha").size), buf);
                }
                ImGui::PopItemWidth();
            }

            // Toggle button — shows current mode with swap icon
            ImGui::SameLine(0, Layout::spacingMd());
            {
                const char* currLabel = s_input_usd ? "DRGX" : "USD";
                bool canToggle = (market.price_usd > 0);
                ImGui::BeginDisabled(!canToggle);
                if (TactileButton("##ToggleCurrency", ImVec2(toggleW, 0), S.resolveFont("button"))) {
                    s_input_usd = !s_input_usd;
                    if (s_input_usd && market.price_usd > 0)
                        s_usd_amount = s_amount * market.price_usd;
                }
                // Draw swap arrows icon + label centered on button
                {
                    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.send", "swap-icon-width").size * Layout::dpiScale();
                    float iconGap = schema::UI().drawElement("tabs.send", "swap-icon-gap").size * Layout::dpiScale();
                    float totalW = iconW + iconGap + textSz.x;
                    float startX = bMin.x + ((bMax.x - bMin.x) - totalW) * 0.5f;
                    float cy = bMin.y + bH * 0.5f;
                    ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
                    float s = iconW * 0.5f;
                    float cx = startX + s;
                    // Swap icon (Material Design)
                    ImFont* swapFont = Type().iconSmall();
                    const char* swapIcon = ICON_MD_SWAP_HORIZ;
                    ImVec2 swapSz = swapFont->CalcTextSizeA(swapFont->LegacySize, 1000.0f, 0.0f, swapIcon);
                    dl->AddText(swapFont, swapFont->LegacySize,
                        ImVec2(cx - swapSz.x * 0.5f, cy - swapSz.y * 0.5f), iconCol, swapIcon);
                    // Text label
                    float tx = startX + iconW + iconGap;
                    float ty = cy - textSz.y * 0.5f;
                    dl->AddText(font, font->LegacySize, ImVec2(tx, ty), iconCol, currLabel);
                }
                ImGui::EndDisabled();
            }

            // Combined amount bar (slider + usage indicator)
            RenderAmountBar(dl, available, colW, capFont, market.price_usd, "##f");

            // Amount error
            if (s_amount > 0 && s_amount + s_fee > available && available > 0) {
                snprintf(buf, sizeof(buf), TR("send_exceeds_available"), available - s_fee);
                Type().textColored(TypeStyle::Caption, Error(), buf);
            }
        }

        // ---- MEMO (shielded only) ----
        if (is_valid_z || s_to_address[0] == '\0') {
            ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
            {
                ImVec2 divPos = ImGui::GetCursorScreenPos();
                dl->AddLine(ImVec2(containerMin.x + pad, divPos.y),
                            ImVec2(containerMin.x + pad + colW, divPos.y),
                            ImGui::GetColorU32(Divider()), S.drawElement("tabs.send", "divider-thickness").size);
            }
            ImGui::Dummy(ImVec2(0, innerGap * 0.5f));

            Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
            ImGui::Dummy(ImVec2(0, S.drawElement("tabs.send", "memo-label-gap").size));

            float memoInputH = std::max(S.drawElement("tabs.send", "memo-min-height").size, S.drawElement("tabs.send", "memo-base-height").size * vScale);
            ImGui::PushItemWidth(colW);
            ImGui::InputTextMultiline("##Memo", s_memo, sizeof(s_memo),
                ImVec2(colW, memoInputH));
            ImGui::PopItemWidth();

            // The Sapling memo limit is in BYTES (multi-byte UTF-8 fills it faster than visible chars);
            // warn in colour once the field is at its cap so it's clear input has stopped being accepted.
            size_t memo_len = strlen(s_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);
        }

        // Divider before action buttons
        {
            float actionBtnH = std::max(S.drawElement("tabs.send", "action-btn-min-height").size,
                                        S.drawElement("tabs.send", "action-btn-height").size * vScale);
            float footerH = innerGap + actionBtnH + pad * vScale;
            float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
            float targetCardH = Layout::mainCardTargetH(formW, vScale);
            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 + formW - pad, divPos.y),
                        ImGui::GetColorU32(Divider()), S.drawElement("tabs.send", "divider-thickness").size);
        }
        ImGui::Dummy(ImVec2(0, innerGap * 0.5f));

        // ---- ACTION BUTTONS (full width) ----
        RenderActionButtons(app, innerW, vScale, is_valid_address, available, "##main");

        // Add bottom padding
        ImGui::Dummy(ImVec2(0, pad * vScale));
        ImGui::Unindent(pad + formOffsetX);

        // Enforce shared card height (matches receive tab)
        {
            float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
            float targetCardH  = Layout::mainCardTargetH(formW, vScale);
            if (currentCardH < targetCardH)
                ImGui::Dummy(ImVec2(0, targetCardH - currentCardH));
        }

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

        ImGui::SetCursorScreenPos(ImVec2(containerMin.x, containerMax.y));
        ImGui::Dummy(ImVec2(formW, 0));
        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));

        // Pass card bottom Y so error overlay can anchor to it
        float cardBottom = containerMax.y;

        // ---- TRANSACTION PROGRESS ----
        {
            ImVec2 progPos = ImGui::GetCursorScreenPos();
            RenderTxProgress(dl, progPos.x, progPos.y, formW, body2, capFont, cardBottom);
            if ((!s_tx_status.empty() || s_sending) && (s_sending || s_status_success)) {
                ImGui::Dummy(ImVec2(0, sectionGap));
            }
        }
    }

    // ---- RECENT SENDS ---- (full column width; reset X off the centered card)
    ImGui::SetCursorPosX(formLeftX);
    RenderRecentSends(state, formW, capFont, app);

    ImGui::EndGroup();
    ImGui::EndDisabled(); // sendSyncing guard
    // Round to nearest pixel to prevent sub-pixel oscillation (vibration)
    // when content like "recent sends" elapsed-time text changes width each frame
    float measuredH = ImGui::GetCursorPosY() - contentStartY;
    s_sendContentH = std::round(measuredH);
    ImGui::EndChild(); // ##SendFormScroll
}

void SetSendFromAddress(const std::string& address)
{
    strncpy(s_from_address, address.c_str(), sizeof(s_from_address) - 1);
    s_from_address[sizeof(s_from_address) - 1] = '\0';
    s_selected_from_idx = -1;
}

} // namespace ui
} // namespace dragonx
