/*
 * argon2_sd_test.ino  v2
 * Генерация masterSecret (Argon2id) + 3 Ed25519 ключевых пары.
 * Результат сохраняется в NVS (внутренняя flash ESP32).
 *
 * Алгоритм совпадает с JS crypto-utils.js:
 *   loginNorm = trim(lowercase(login))
 *   salt      = SHA256("shine-auth-v2|login=<loginNorm>|suffix=master.secret")[0:16]
 *   passBytes = UTF8("<loginNorm>\n<password>")
 *   secret    = Argon2id(passBytes, salt, t=2, m=65536 KB, p=1, dkLen=32)
 *   legacy(empty password):
 *     secret = SHA256(base64(SHA256(password)) + "master.secret")
 *   keyPair_i = Ed25519(SHA256(base64(secret) + "|" + suffix_i))
 *   suffixes  = ["root.key", "bch.key", "dev.key"]
 *
 * Плата: Waveshare ESP32-S3-Touch-AMOLED-2.16
 * SD   : SDMMC 1-bit  CLK=GPIO2, CMD=GPIO1, D0=GPIO3
 */

#include <Arduino.h>
#include <Wire.h>
#include <string.h>
#include <stdint.h>
#include "argon2_types.h"
#include "driver/gpio.h"
#include "Arduino_GFX_Library.h"
#include "TouchDrvCSTXXX.hpp"
#include "SD_MMC.h"
#include "mbedtls/sha256.h"
#include "mbedtls/base64.h"
#include <Ed25519.h>
#include <Preferences.h>

// ═══════════════════════════════════════════════════════════
//  PINS
// ═══════════════════════════════════════════════════════════
#define PIN_LCD_CS    12
#define PIN_LCD_SCLK  38
#define PIN_LCD_D0     4
#define PIN_LCD_D1     5
#define PIN_LCD_D2     6
#define PIN_LCD_D3     7
#define PIN_LCD_RST    2
#define PIN_I2C_SDA   15
#define PIN_I2C_SCL   14
#define PIN_TP_INT    11
#define PIN_SD_CLK     2
#define PIN_SD_CMD     1
#define PIN_SD_D0      3

// ═══════════════════════════════════════════════════════════
//  DISPLAY
// ═══════════════════════════════════════════════════════════
#define DISP_W 480
#define DISP_H 480

Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
    PIN_LCD_CS, PIN_LCD_SCLK,
    PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
Arduino_CO5300 *gfx = new Arduino_CO5300(
    gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0);

// ═══════════════════════════════════════════════════════════
//  TOUCH  (полинг + прерывание)
// ═══════════════════════════════════════════════════════════
TouchDrvCST92xx gTouch;
volatile bool   gTouchIrq = false;
void IRAM_ATTR  onTouchIrq() { gTouchIrq = true; }

// Состояние свайпа
static bool    gTouching    = false;
static int16_t gTouchStartY = 0;
static int16_t gTouchPrevY  = 0;
static int16_t gTouchCurY   = 0;
static int16_t gTouchCurX   = 0;

// ═══════════════════════════════════════════════════════════
//  COLORS
// ═══════════════════════════════════════════════════════════
#define C_BG      0x0000u
#define C_KEY     0x2104u
#define C_KEY_HL  0x4208u
#define C_TEXT    0xFFFFu
#define C_HINT    0x7BEFu
#define C_GREEN   0x07E0u
#define C_RED     0xF800u
#define C_FIELD   0x18C3u
#define C_BAR_BG  0x18C3u
#define C_BAR_FG  0xFFFFu
#define C_SEP     0x2945u
#define C_ACCENT  0x05FFu

// ═══════════════════════════════════════════════════════════
//  APP STATE
// ═══════════════════════════════════════════════════════════
enum AppState {
    ST_LOGIN,    // ввод логина
    ST_PASS,     // ввод пароля
    ST_RUNNING,  // вычисление
    ST_DONE,     // результат (только что посчитан)
    ST_SAVED,    // результат (загружен из NVS)
    ST_CONFIRM,  // диалог "сгенерировать новый?"
    ST_ERROR
};
static AppState gState = ST_LOGIN;

static char    gLogin[64] = {};
static char    gPass[64]  = {};
static uint8_t gSecret[32];
static char    gSecretHex[65];
static char    gInputStatus[96] = {};
static bool    gKbNums  = false;

// ═══════════════════════════════════════════════════════════
//  КЛЮЧЕВЫЕ ПАРЫ
// ═══════════════════════════════════════════════════════════
struct KeyPair { uint8_t pub[32]; uint8_t priv[32]; };
static KeyPair      gKeys[3];
static const char * KEY_SUFFIXES[3] = {"root.key", "bch.key", "dev.key"};
static const char * KEY_LABELS[3]   = {"root.key", "bch.key",  "dev.key"};
static uint32_t     gElapsedSec = 0;

// Base58 представления (43-44 символа для 32 байт + \0)
static char gSecretB58[50];
static char gPubB58[3][50];
static char gPrivB58[3][50];

// ═══════════════════════════════════════════════════════════
//  ARGON2 ПАРАМЕТРЫ
// ═══════════════════════════════════════════════════════════
#define A2_T        2u
#define A2_M        65536u
#define A2_P        1u
#define A2_DKLEN    32u
#define A2_SYNC     4u
#define A2_SEG      (A2_M / A2_SYNC)
#define A2_VERSION  0x13u
#define A2_TYPE     2u
#define A2_BLKSZ    1024u
#define TOTAL_FILLS (A2_T * A2_M - 2u)

static uint32_t gDone    = 0;
static uint32_t gStartMs = 0;

// ─── прогресс-бар: антимерцание ───────────────────────────
static int     gBarFilledPx  = 0;   // сколько пикселей уже зелёных
static uint32_t gLastElSec   = 0xFFFFFFFF;  // последнее нарисованное время

// ═══════════════════════════════════════════════════════════
//  ВЫЧИСЛИТЕЛЬНОЕ СОСТОЯНИЕ
// ═══════════════════════════════════════════════════════════
static uint8_t   gH0[64];
static uint32_t  gCurPass  = 0;
static uint32_t  gCurBlock = 2;
static bool      gInitDone = false;

static uint8_t  *gBufPrev = nullptr;
static uint8_t  *gBufRef  = nullptr;
static uint8_t  *gBufOut  = nullptr;
static uint8_t  *gBufZero = nullptr;
static uint8_t  *gBufAddr = nullptr;
static uint32_t *gJ1Seg   = nullptr;

#define SD_MEM_FILE "/argon2.bin"
static File gSdFile;

// ═══════════════════════════════════════════════════════════
//  РЕЗУЛЬТАТ — прокрутка
// ═══════════════════════════════════════════════════════════
#define RES_BTN_H   52          // высота кнопки "Новый" внизу
#define RES_VIEW_H  (DISP_H - RES_BTN_H)   // 428 px
static int32_t gScrollY   = 0;
static int32_t gScrollMax = 0;  // рассчитывается при отрисовке

// NVS
static Preferences gPrefs;

// ═══════════════════════════════════════════════════════════
//  BLAKE2b
// ═══════════════════════════════════════════════════════════
#define ROTR64(x,n) (((x)>>(n))|((x)<<(64-(n))))

static const uint64_t B2IV[8] = {
    0x6A09E667F3BCC908ULL,0xBB67AE8584CAA73BULL,
    0x3C6EF372FE94F82BULL,0xA54FF53A5F1D36F1ULL,
    0x510E527FADE682D1ULL,0x9B05688C2B3E6C1FULL,
    0x1F83D9ABFB41BD6BULL,0x5BE0CD19137E2179ULL
};
static const uint8_t B2SIGMA[12][16] = {
    {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},
    {14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3},
    {11,8,12,0,5,2,15,13,10,14,3,6,7,1,9,4},
    {7,9,3,1,13,12,11,14,2,6,5,10,4,0,15,8},
    {9,0,5,7,2,4,10,15,14,1,11,12,6,8,3,13},
    {2,12,6,10,0,11,8,3,4,13,7,5,15,14,1,9},
    {12,5,1,15,14,13,4,10,0,7,6,3,9,2,8,11},
    {13,11,7,14,12,1,3,9,5,0,15,4,8,6,2,10},
    {6,15,14,9,11,3,0,8,12,2,13,7,1,4,10,5},
    {10,2,8,4,7,6,1,5,15,11,9,14,3,12,13,0},
    {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},
    {14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3}
};

static B2State gB2S;

static void b2_compress(B2State *S, const uint8_t *blk) {
    uint64_t m[16],v[16];
    for (int i=0;i<16;i++) m[i]=((const uint64_t*)blk)[i];
    for (int i=0;i<8;i++)  v[i]=S->h[i];
    v[8]=B2IV[0];v[9]=B2IV[1];v[10]=B2IV[2];v[11]=B2IV[3];
    v[12]=B2IV[4]^S->t[0];v[13]=B2IV[5]^S->t[1];
    v[14]=B2IV[6]^S->f[0];v[15]=B2IV[7]^S->f[1];
#define BG(r,i,a,b,c,d) \
    v[a]+=v[b]+m[B2SIGMA[r][2*(i)]]; v[d]=ROTR64(v[d]^v[a],32); \
    v[c]+=v[d];                       v[b]=ROTR64(v[b]^v[c],24); \
    v[a]+=v[b]+m[B2SIGMA[r][2*(i)+1]];v[d]=ROTR64(v[d]^v[a],16); \
    v[c]+=v[d];                        v[b]=ROTR64(v[b]^v[c],63)
    for (int r=0;r<12;r++){
        BG(r,0,0,4,8,12);BG(r,1,1,5,9,13);BG(r,2,2,6,10,14);BG(r,3,3,7,11,15);
        BG(r,4,0,5,10,15);BG(r,5,1,6,11,12);BG(r,6,2,7,8,13);BG(r,7,3,4,9,14);
    }
#undef BG
    for (int i=0;i<8;i++) S->h[i]^=v[i]^v[i+8];
}
static void b2_init(B2State *S,size_t outlen){
    memset(S,0,sizeof(*S));memcpy(S->h,B2IV,64);
    S->h[0]^=0x01010000ULL|outlen;S->outlen=outlen;
}
static void b2_update(B2State *S,const uint8_t *in,size_t inlen){
    while(inlen>0){
        size_t fill=128-S->buflen,use=inlen<fill?inlen:fill;
        memcpy(S->buf+S->buflen,in,use);S->buflen+=use;in+=use;inlen-=use;
        if(S->buflen==128&&inlen>0){
            S->t[0]+=128;if(S->t[0]<128)S->t[1]++;
            b2_compress(S,S->buf);S->buflen=0;
        }
    }
}
static void b2_final(B2State *S,uint8_t *digest){
    S->f[0]=~0ULL;S->t[0]+=S->buflen;if(S->t[0]<S->buflen)S->t[1]++;
    memset(S->buf+S->buflen,0,128-S->buflen);b2_compress(S,S->buf);
    uint8_t tmp[64];
    for(int i=0;i<8;i++){uint64_t w=S->h[i];for(int j=0;j<8;j++)tmp[i*8+j]=(uint8_t)(w>>(j*8));}
    memcpy(digest,tmp,S->outlen);
}
static void b2hash(const uint8_t *in,size_t inlen,uint8_t *digest,size_t outlen){
    b2_init(&gB2S,outlen);b2_update(&gB2S,in,inlen);b2_final(&gB2S,digest);
}
static void b2long(const uint8_t *in,size_t inlen,uint8_t *digest,uint32_t outlen){
    uint8_t lenle[4]={(uint8_t)outlen,(uint8_t)(outlen>>8),(uint8_t)(outlen>>16),(uint8_t)(outlen>>24)};
    if(outlen<=64){b2_init(&gB2S,outlen);b2_update(&gB2S,lenle,4);b2_update(&gB2S,in,inlen);b2_final(&gB2S,digest);return;}
    uint8_t tmp[64];
    b2_init(&gB2S,64);b2_update(&gB2S,lenle,4);b2_update(&gB2S,in,inlen);b2_final(&gB2S,tmp);
    memcpy(digest,tmp,32);digest+=32;uint32_t rem=outlen-32;
    while(rem>64){b2hash(tmp,64,tmp,64);memcpy(digest,tmp,32);digest+=32;rem-=32;}
    b2hash(tmp,64,tmp,rem);memcpy(digest,tmp,rem);
}

// ═══════════════════════════════════════════════════════════
//  ARGON2ID — BLOCK FILL
// ═══════════════════════════════════════════════════════════
#define A2G(a,b,c,d) do{ \
    (a)+=(b)+2*((a)&0xFFFFFFFFULL)*((b)&0xFFFFFFFFULL); \
    (d)=ROTR64((d)^(a),32); \
    (c)+=(d)+2*((c)&0xFFFFFFFFULL)*((d)&0xFFFFFFFFULL); \
    (b)=ROTR64((b)^(c),24); \
    (a)+=(b)+2*((a)&0xFFFFFFFFULL)*((b)&0xFFFFFFFFULL); \
    (d)=ROTR64((d)^(a),16); \
    (c)+=(d)+2*((c)&0xFFFFFFFFULL)*((d)&0xFFFFFFFFULL); \
    (b)=ROTR64((b)^(c),63); \
}while(0)
#define A2P(v) do{ \
    A2G((v)[0],(v)[4],(v)[8],(v)[12]);A2G((v)[1],(v)[5],(v)[9],(v)[13]); \
    A2G((v)[2],(v)[6],(v)[10],(v)[14]);A2G((v)[3],(v)[7],(v)[11],(v)[15]); \
    A2G((v)[0],(v)[5],(v)[10],(v)[15]);A2G((v)[1],(v)[6],(v)[11],(v)[12]); \
    A2G((v)[2],(v)[7],(v)[8],(v)[13]);A2G((v)[3],(v)[4],(v)[9],(v)[14]); \
}while(0)

static void fillBlock(uint32_t prevIdx,uint32_t refIdx,uint32_t outIdx,bool xorMode){
    gSdFile.seek((uint64_t)prevIdx*A2_BLKSZ);gSdFile.read(gBufPrev,A2_BLKSZ);
    gSdFile.seek((uint64_t)refIdx*A2_BLKSZ);gSdFile.read(gBufRef,A2_BLKSZ);
    uint64_t *R=(uint64_t*)gBufOut,*P=(uint64_t*)gBufPrev,*F=(uint64_t*)gBufRef;
    for(int i=0;i<128;i++)R[i]=P[i]^F[i];
    uint64_t *Q=(uint64_t*)gBufRef;memcpy(Q,R,A2_BLKSZ);
    for(int j=0;j<8;j++)A2P(&Q[16*j]);
    uint64_t row[16];
    for(int i=0;i<8;i++){
        for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
        A2P(row);
        for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
    }
    for(int i=0;i<128;i++)R[i]=Q[i]^R[i];
    if(xorMode){
        gSdFile.seek((uint64_t)outIdx*A2_BLKSZ);gSdFile.read(gBufPrev,A2_BLKSZ);
        uint64_t *O=(uint64_t*)gBufPrev;for(int i=0;i<128;i++)R[i]^=O[i];
    }
    gSdFile.seek((uint64_t)outIdx*A2_BLKSZ);gSdFile.write(gBufOut,A2_BLKSZ);
}

static void generateAddresses(uint32_t pass,uint32_t slice){
    memset(gBufZero,0,A2_BLKSZ);
    uint8_t inputBlk[A2_BLKSZ];memset(inputBlk,0,A2_BLKSZ);
    uint64_t *iv=(uint64_t*)inputBlk;
    iv[0]=pass;iv[1]=0;iv[2]=slice;iv[3]=A2_M;iv[4]=A2_T;iv[5]=A2_TYPE;
    uint32_t count=0;
    for(uint32_t b=0;b<A2_SEG;b+=128){
        iv[6]=++count;
        {
            uint64_t *R=(uint64_t*)gBufAddr,*Z=(uint64_t*)gBufZero,*I=iv;
            for(int i=0;i<128;i++)R[i]=Z[i]^I[i];
            uint64_t *Q=R,row[16];
            for(int j=0;j<8;j++)A2P(&Q[16*j]);
            for(int i=0;i<8;i++){
                for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
                A2P(row);
                for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
            }
            for(int i=0;i<128;i++)R[i]=Q[i]^(Z[i]^I[i]);
        }
        {
            uint64_t *R=(uint64_t*)gBufPrev,*Z=(uint64_t*)gBufZero,*A=(uint64_t*)gBufAddr;
            for(int i=0;i<128;i++)R[i]=Z[i]^A[i];
            uint64_t *Q=R,row[16];
            for(int j=0;j<8;j++)A2P(&Q[16*j]);
            for(int i=0;i<8;i++){
                for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
                A2P(row);
                for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
            }
            for(int i=0;i<128;i++)R[i]=Q[i]^(Z[i]^A[i]);
            memcpy(gBufAddr,gBufPrev,A2_BLKSZ);
        }
        uint64_t *addr=(uint64_t*)gBufAddr;
        uint32_t cnt=(b+128<=A2_SEG)?128:(A2_SEG-b);
        for(uint32_t j=0;j<cnt;j++)gJ1Seg[b+j]=(uint32_t)(addr[j]&0xFFFFFFFFULL);
    }
}

static uint32_t indexAlpha(uint32_t pass,uint32_t slice,uint32_t posInSlice,uint32_t J1){
    uint32_t refAreaSize;
    if(pass==0){
        if(slice==0){uint32_t pos=posInSlice;refAreaSize=(pos<2)?0:pos-1;}
        else refAreaSize=slice*A2_SEG+posInSlice-1;
    }else refAreaSize=A2_M-A2_SEG+posInSlice-1;
    if(refAreaSize==0)refAreaSize=1;
    uint64_t relPos=(uint64_t)J1*J1>>32;
    relPos=refAreaSize-1-((uint64_t)refAreaSize*relPos>>32);
    uint32_t startPos=0;
    if(pass!=0&&slice!=A2_SYNC-1)startPos=(slice+1)*A2_SEG;
    return (startPos+relPos)%A2_M;
}

// ═══════════════════════════════════════════════════════════
//  SHA-256
// ═══════════════════════════════════════════════════════════
static void sha256calc(const uint8_t *in,size_t len,uint8_t *out32){
    mbedtls_sha256_context ctx;
    mbedtls_sha256_init(&ctx);mbedtls_sha256_starts(&ctx,0);
    mbedtls_sha256_update(&ctx,in,len);mbedtls_sha256_finish(&ctx,out32);
    mbedtls_sha256_free(&ctx);
}

static void trimInPlace(char *text){
    if(!text)return;
    size_t len=strlen(text);
    size_t start=0;
    while(start<len&&(text[start]==' '||text[start]=='\t'||text[start]=='\n'||text[start]=='\r'))start++;
    size_t end=len;
    while(end>start&&(text[end-1]==' '||text[end-1]=='\t'||text[end-1]=='\n'||text[end-1]=='\r'))end--;
    if(start>0&&end>start)memmove(text,text+start,end-start);
    if(end<=start){text[0]='\0';return;}
    text[end-start]='\0';
}

static void lowercaseAsciiInPlace(char *text){
    if(!text)return;
    for(int i=0;text[i];i++){
        if(text[i]>='A'&&text[i]<='Z')text[i]+=32;
    }
}

static void normalizeLoginInPlace(char *text){
    trimInPlace(text);
    lowercaseAsciiInPlace(text);
}

static void setInputStatus(const char *message){
    snprintf(gInputStatus,sizeof(gInputStatus),"%s",message?message:"");
}

static void clearInputStatus(){
    gInputStatus[0]='\0';
}

static void bytesToBase64Std(const uint8_t *data,size_t len,char *out,size_t outSz){
    size_t b64len=0;
    if(outSz==0)return;
    if(mbedtls_base64_encode((uint8_t*)out,outSz,&b64len,data,len)!=0){
        out[0]='\0';
        return;
    }
    if(b64len>=outSz)b64len=outSz-1;
    out[b64len]='\0';
}

static void deriveLegacyMasterSecret(const char *password,uint8_t *out32){
    uint8_t baseHash[32];
    sha256calc((const uint8_t*)password,strlen(password),baseHash);

    char baseB64[64];
    bytesToBase64Std(baseHash,32,baseB64,sizeof(baseB64));

    char material[96];
    snprintf(material,sizeof(material),"%smaster.secret",baseB64);
    sha256calc((const uint8_t*)material,strlen(material),out32);
}

// ═══════════════════════════════════════════════════════════
//  BASE58
// ═══════════════════════════════════════════════════════════
static const char B58_ALPHA[] =
    "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

static void bytesToBase58(const uint8_t *data,size_t len,char *out,size_t outSz){
    int zeros=0;
    while(zeros<(int)len&&data[zeros]==0)zeros++;
    uint8_t tmp[64]={};int tmpLen=0;
    for(int i=zeros;i<(int)len;i++){
        int carry=data[i];
        for(int j=0;j<tmpLen;j++){carry+=256*tmp[j];tmp[j]=carry%58;carry/=58;}
        while(carry){tmp[tmpLen++]=carry%58;carry/=58;}
    }
    int n=0;
    for(int i=0;i<zeros&&n<(int)outSz-1;i++)out[n++]='1';
    for(int i=tmpLen-1;i>=0&&n<(int)outSz-1;i--)out[n++]=B58_ALPHA[(int)tmp[i]];
    out[n]='\0';
}

// ═══════════════════════════════════════════════════════════
//  KEY DERIVATION  (Ed25519 из masterSecret)
// ═══════════════════════════════════════════════════════════
static void deriveKeyPairs(){
    // base64(masterSecret) — standard base64 с '+','/'
    uint8_t b64buf[48];size_t b64len=0;
    mbedtls_base64_encode(b64buf,sizeof(b64buf),&b64len,gSecret,32);
    b64buf[b64len]='\0';

    for(int i=0;i<3;i++){
        char material[96];
        snprintf(material,sizeof(material),"%s|%s",(char*)b64buf,KEY_SUFFIXES[i]);
        uint8_t seed[32];
        sha256calc((uint8_t*)material,strlen(material),seed);
        memcpy(gKeys[i].priv,seed,32);
        Ed25519::derivePublicKey(gKeys[i].pub,seed);
    }
    // base58 для отображения
    bytesToBase58(gSecret,32,gSecretB58,sizeof(gSecretB58));
    for(int i=0;i<3;i++){
        bytesToBase58(gKeys[i].pub,32,gPubB58[i],sizeof(gPubB58[i]));
        bytesToBase58(gKeys[i].priv,32,gPrivB58[i],sizeof(gPrivB58[i]));
    }
}

// ═══════════════════════════════════════════════════════════
//  NVS
// ═══════════════════════════════════════════════════════════
static void nvsSave(){
    gPrefs.begin("shine",false);
    gPrefs.putString("login",gLogin);
    gPrefs.putBytes("secret",gSecret,32);
    for(int i=0;i<3;i++){
        char k[6];
        snprintf(k,sizeof(k),"pub%d",i); gPrefs.putBytes(k,gKeys[i].pub,32);
        snprintf(k,sizeof(k),"prv%d",i); gPrefs.putBytes(k,gKeys[i].priv,32);
    }
    gPrefs.putUInt("tsec",gElapsedSec);
    gPrefs.putBool("valid",true);
    gPrefs.end();
}

static bool nvsLoad(){
    gPrefs.begin("shine",true);
    bool valid=gPrefs.getBool("valid",false);
    if(valid){
        gPrefs.getString("login",gLogin,sizeof(gLogin));
        gPrefs.getBytes("secret",gSecret,32);
        for(int i=0;i<3;i++){
            char k[6];
            snprintf(k,sizeof(k),"pub%d",i); gPrefs.getBytes(k,gKeys[i].pub,32);
            snprintf(k,sizeof(k),"prv%d",i); gPrefs.getBytes(k,gKeys[i].priv,32);
        }
        gElapsedSec=gPrefs.getUInt("tsec",0);
        for(int i=0;i<32;i++)snprintf(gSecretHex+i*2,3,"%02x",gSecret[i]);
        // base58
        bytesToBase58(gSecret,32,gSecretB58,sizeof(gSecretB58));
        for(int i=0;i<3;i++){
            bytesToBase58(gKeys[i].pub,32,gPubB58[i],sizeof(gPubB58[i]));
            bytesToBase58(gKeys[i].priv,32,gPrivB58[i],sizeof(gPrivB58[i]));
        }
    }
    gPrefs.end();
    return valid;
}

static void nvsClear(){
    gPrefs.begin("shine",false);
    gPrefs.clear();
    gPrefs.end();
}

// ═══════════════════════════════════════════════════════════
//  ARGON2ID INIT
// ═══════════════════════════════════════════════════════════
static void argon2Init(const char *login,const char *password){
    char saltSrc[256];
    snprintf(saltSrc,sizeof(saltSrc),"shine-auth-v2|login=%s|suffix=master.secret",login);
    uint8_t saltHash[32];sha256calc((uint8_t*)saltSrc,strlen(saltSrc),saltHash);
    uint8_t salt[16];memcpy(salt,saltHash,16);

    char passBytes[128];
    snprintf(passBytes,sizeof(passBytes),"%s\n%s",login,password);
    uint32_t passLen=strlen(passBytes),saltLen=16;

    b2_init(&gB2S,64);
    uint8_t le4[4];
#define B2LE32(val) do{uint32_t _v=(val);le4[0]=_v;le4[1]=_v>>8;le4[2]=_v>>16;le4[3]=_v>>24;b2_update(&gB2S,le4,4);}while(0)
    B2LE32(A2_P);B2LE32(A2_DKLEN);B2LE32(A2_M);B2LE32(A2_T);
    B2LE32(A2_VERSION);B2LE32(A2_TYPE);
    B2LE32(passLen);b2_update(&gB2S,(const uint8_t*)passBytes,passLen);
    B2LE32(saltLen);b2_update(&gB2S,salt,saltLen);
    B2LE32(0);B2LE32(0);
#undef B2LE32
    b2_final(&gB2S,gH0);

    // Debug: print inputs and H0 to Serial for cross-check with reference
    Serial.printf("[DBG] login='%s' passLen=%u saltLen=%u\n",login,passLen,saltLen);
    Serial.print("[DBG] salt: ");
    for(int i=0;i<16;i++)Serial.printf("%02x",salt[i]);Serial.println();
    Serial.print("[DBG] H0:   ");
    for(int i=0;i<64;i++)Serial.printf("%02x",gH0[i]);Serial.println();

    uint8_t input[72];memcpy(input,gH0,64);
    for(uint32_t i=0;i<2;i++){
        input[64]=i;input[65]=0;input[66]=0;input[67]=0;
        input[68]=0;input[69]=0;input[70]=0;input[71]=0;
        b2long(input,72,gBufOut,A2_BLKSZ);
        gSdFile.seek((uint64_t)i*A2_BLKSZ);gSdFile.write(gBufOut,A2_BLKSZ);
    }
    gCurPass=0;gCurBlock=2;gDone=0;gStartMs=millis();
    gBarFilledPx=0;gLastElSec=0xFFFFFFFF;
    gInitDone=true;
    generateAddresses(0,0);
}

// ═══════════════════════════════════════════════════════════
//  ARGON2ID STEP
// ═══════════════════════════════════════════════════════════
static bool argon2Step(){
    if(gCurPass>=A2_T)return true;
    uint32_t blk=gCurBlock;
    uint32_t slice=blk/A2_SEG;
    uint32_t posInSlice=blk-slice*A2_SEG;
    bool xorMode=(gCurPass>0);
    uint32_t J1;
    bool useArgon2i=(gCurPass==0&&slice<2);
    if(useArgon2i){
        J1=gJ1Seg[posInSlice];
    }else{
        uint32_t prevIdx=(blk==0)?A2_M-1:blk-1;
        gSdFile.seek((uint64_t)prevIdx*A2_BLKSZ);gSdFile.read(gBufRef,8);
        J1=((uint32_t*)gBufRef)[0];
    }
    uint32_t prevIdx=(blk==0)?A2_M-1:blk-1;
    uint32_t refIdx=indexAlpha(gCurPass,slice,posInSlice,J1);
    fillBlock(prevIdx,refIdx,blk,xorMode);
    gDone++;
    gCurBlock++;
    if(gCurBlock>=A2_M){
        gCurBlock=0;gCurPass++;
    }else{
        uint32_t newSlice=gCurBlock/A2_SEG;
        if(newSlice!=slice&&gCurPass==0&&newSlice<2)
            generateAddresses(gCurPass,newSlice);
    }
    return(gCurPass>=A2_T);
}

// ═══════════════════════════════════════════════════════════
//  ARGON2ID FINALIZE
// ═══════════════════════════════════════════════════════════
static void argon2Finalize(){
    gSdFile.seek((uint64_t)(A2_M-1)*A2_BLKSZ);
    gSdFile.read(gBufOut,A2_BLKSZ);
    b2long(gBufOut,A2_BLKSZ,gSecret,A2_DKLEN);
    for(int i=0;i<32;i++)snprintf(gSecretHex+i*2,3,"%02x",gSecret[i]);
    gSecretHex[64]='\0';
    gElapsedSec=(millis()-gStartMs)/1000;
    deriveKeyPairs();
    nvsSave();
    Serial.println("=== RESULT ===");
    Serial.printf("Login : %s\n",gLogin);
    Serial.printf("Secret: %s\n",gSecretHex);
    for(int i=0;i<3;i++){
        Serial.printf("%s pub : %s\n",KEY_LABELS[i],gPubB58[i]);
        Serial.printf("%s priv: %s\n",KEY_LABELS[i],gPrivB58[i]);
    }
    Serial.println("=== END ===");
}

static void completeResultFromSecret(){
    for(int i=0;i<32;i++)snprintf(gSecretHex+i*2,3,"%02x",gSecret[i]);
    gSecretHex[64]='\0';
    deriveKeyPairs();
    nvsSave();
}

// ═══════════════════════════════════════════════════════════
//  UI — HELPERS
// ═══════════════════════════════════════════════════════════
static void uiText(int x,int y,const char *s,uint16_t color,uint8_t sz=2){
    gfx->setTextColor(color);gfx->setTextSize(sz);gfx->setCursor(x,y);gfx->print(s);
}
static void uiRect(int x,int y,int w,int h,uint16_t fill,uint16_t border=0,int r=4){
    gfx->fillRoundRect(x,y,w,h,r,fill);
    if(border)gfx->drawRoundRect(x,y,w,h,r,border);
}

// ═══════════════════════════════════════════════════════════
//  UI — KEYBOARD
// ═══════════════════════════════════════════════════════════
static const char *KB_ROWS[3]={"qwertyuiop","asdfghjkl","zxcvbnm"};
static const char *KB_NUMS[3]={"1234567890","@#$%^&*()-",".,:!?/;\"'="};
#define KB_Y0  200
#define KB_KH  44
#define KB_KW  44
#define KB_GAP 2

static void drawKeyboard(){
    gfx->fillRect(0,KB_Y0-4,DISP_W,DISP_H-KB_Y0+4,C_BG);
    for(int row=0;row<3;row++){
        const char *keys=gKbNums?KB_NUMS[row]:KB_ROWS[row];
        int n=strlen(keys);
        int totalW=n*(KB_KW+KB_GAP)-KB_GAP;
        int x0=(DISP_W-totalW)/2,y=KB_Y0+row*(KB_KH+KB_GAP);
        for(int k=0;k<n;k++){
            int x=x0+k*(KB_KW+KB_GAP);
            uiRect(x,y,KB_KW,KB_KH,C_KEY,C_KEY_HL);
            char tmp[3]={0};tmp[0]=keys[k];
            uiText(x+KB_KW/2-6,y+KB_KH/2-8,tmp,C_TEXT,2);
        }
    }
    int y4=KB_Y0+3*(KB_KH+KB_GAP);
    uiRect(20,y4,80,KB_KH,C_KEY_HL);uiText(28,y4+12,"123",C_TEXT,2);
    uiRect(108,y4,164,KB_KH,C_KEY,C_KEY_HL);uiText(162,y4+12,"_",C_TEXT,2);
    uiRect(280,y4,80,KB_KH,C_KEY,C_KEY_HL);uiText(296,y4+12,"<<",C_TEXT,2);
    uiRect(368,y4,92,KB_KH,C_GREEN);uiText(384,y4+12,"OK",C_TEXT,2);
}

static void drawInputScreen(const char *title,const char *buf,bool isMasked){
    gfx->fillScreen(C_BG);
    gfx->setTextSize(2);uiText(20,20,title,C_HINT);
    uiRect(20,60,DISP_W-40,52,C_FIELD,C_KEY_HL);
    char show[68];
    if(isMasked){int n=strlen(buf);if(n>64)n=64;for(int i=0;i<n;i++)show[i]='*';show[n]='\0';}
    else{strncpy(show,buf,67);show[67]='\0';}
    gfx->setTextSize(2);gfx->setTextColor(C_TEXT);gfx->setCursor(30,78);gfx->print(show);
    int cx=30+strlen(show)*12;gfx->drawFastVLine(cx,78,20,C_GREEN);
    if(gInputStatus[0]){
        gfx->setTextSize(2);gfx->setTextColor(C_RED);gfx->setCursor(20,132);gfx->print(gInputStatus);
    }
    drawKeyboard();
}

static bool kbHit(int tx,int ty,int kx,int ky,int kw,int kh){
    return tx>=kx&&tx<kx+kw&&ty>=ky&&ty<ky+kh;
}
static char kbTouch(int tx,int ty){
    for(int row=0;row<3;row++){
        const char *keys=gKbNums?KB_NUMS[row]:KB_ROWS[row];
        int n=strlen(keys);
        int totalW=n*(KB_KW+KB_GAP)-KB_GAP,x0=(DISP_W-totalW)/2;
        int y=KB_Y0+row*(KB_KH+KB_GAP);
        for(int k=0;k<n;k++){
            int x=x0+k*(KB_KW+KB_GAP);
            if(kbHit(tx,ty,x,y,KB_KW,KB_KH))return keys[k];
        }
    }
    int y4=KB_Y0+3*(KB_KH+KB_GAP);
    if(kbHit(tx,ty,20,y4,80,KB_KH))return'\x03';
    if(kbHit(tx,ty,108,y4,164,KB_KH))return' ';
    if(kbHit(tx,ty,280,y4,80,KB_KH))return'\x08';
    if(kbHit(tx,ty,368,y4,92,KB_KH))return'\x0D';
    return 0;
}

// ═══════════════════════════════════════════════════════════
//  UI — ПРОГРЕСС (антимерцание)
// ═══════════════════════════════════════════════════════════
static void drawProgressHeader(){
    gfx->fillRect(0,0,DISP_W,80,C_BG);
    gfx->setTextSize(2);uiText(20,18,"Argon2id (SD)",C_GREEN);
    char sub[80];snprintf(sub,sizeof(sub),"t=%u  m=%u KB  p=%u",A2_T,A2_M,A2_P);
    uiText(20,46,sub,C_HINT);
    // нарисовать пустую полосу один раз
    int bx=20,by=90,bw=DISP_W-40,bh=36;
    uiRect(bx,by,bw,bh,C_BAR_BG,C_SEP,4);
    gfx->fillRect(bx+1,by+1,bw-2,bh-2,C_BG);
}

static void drawProgress(){
    uint32_t elMs=millis()-gStartMs;
    uint32_t elSec=elMs/1000;

    // Прогресс-бар: только дорисовываем новую зелёную часть
    int bx=20,by=90,bw=DISP_W-40,bh=36;
    int filled=(int)((uint64_t)bw*gDone/TOTAL_FILLS);
    if(gDone>0&&filled==0)filled=1;  // at least 1px once started
    if(filled>gBarFilledPx){
        gfx->fillRect(bx+1+gBarFilledPx,by+1,filled-gBarFilledPx,bh-2,C_GREEN);
        gBarFilledPx=filled;
    }

    // Текст обновляем только раз в секунду
    if(elSec==gLastElSec)return;
    gLastElSec=elSec;

    float pct=(gDone>0)?((float)gDone/TOTAL_FILLS*100.0f):0.0f;
    uint32_t etaSec=0;
    if(gDone>10&&gDone<TOTAL_FILLS)
        etaSec=(uint32_t)((float)elMs/gDone*(TOTAL_FILLS-gDone)/1000.0f);

    gfx->fillRect(0,136,DISP_W,120,C_BG);
    char line[64];
    snprintf(line,sizeof(line),"%.1f%%  (%lu / %lu)",pct,(unsigned long)gDone,(unsigned long)TOTAL_FILLS);
    uiText(20,136,line,C_TEXT,2);

    snprintf(line,sizeof(line),"Elapsed: %lus (%lum%lus)",
             (unsigned long)elSec,(unsigned long)elSec/60,(unsigned long)elSec%60);
    uiText(20,162,line,C_GREEN,2);

    if(gDone>10){
        if(etaSec>=3600)
            snprintf(line,sizeof(line),"ETA: ~%lum%lus",(unsigned long)etaSec/60,(unsigned long)etaSec%60);
        else
            snprintf(line,sizeof(line),"ETA: ~%lus",(unsigned long)etaSec);
        uiText(20,188,line,C_HINT,2);
    }
}

// ═══════════════════════════════════════════════════════════
//  UI — РЕЗУЛЬТАТ (с прокруткой свайпом)
// ═══════════════════════════════════════════════════════════
// Контент рисуется в виртуальных координатах y_virt.
// На экран попадает только [gScrollY .. gScrollY+RES_VIEW_H).
// Кнопка "Новый" всегда внизу (фиксирована).

#define RLH  20   // высота строки (textSize 2)
#define RLH1 12   // высота строки (textSize 1)
#define RSP   6   // маленький отступ
#define RPX  20   // отступ слева

// Вспомогательный класс — рисует текст только если строка видима
struct RDraw {
    int32_t scroll;
    int32_t y;      // текущая виртуальная Y
    void line(const char *s, uint16_t col, uint8_t sz=2){
        int sy=(int)y-(int)scroll;
        int lh=(sz==1)?RLH1:RLH;
        if(sy>=-lh&&sy<RES_VIEW_H){
            gfx->setTextSize(sz);gfx->setTextColor(col);gfx->setCursor(RPX,sy);gfx->print(s);
        }
        y+=lh+2;
    }
    void sep(){
        int sy=(int)y-(int)scroll;
        if(sy>=0&&sy<RES_VIEW_H)gfx->drawFastHLine(RPX,sy,DISP_W-2*RPX,C_SEP);
        y+=10;
    }
    void gap(int px=4){y+=px;}
    // Для длинных строк (base58): разбить на куски по 38 символов при textSize=2
    void longval(const char *s, uint16_t col){
        const int CPL=38;
        int len=(int)strlen(s);
        if(len==0){line("(empty)",C_HINT,2);return;}
        for(int off=0;off<len;off+=CPL){
            char tmp[42];int take=len-off;if(take>CPL)take=CPL;
            memcpy(tmp,s+off,take);tmp[take]='\0';
            line(tmp,col,2);
        }
    }
};

static void drawResultContent(){
    // очистить область прокрутки
    gfx->fillRect(0,0,DISP_W,RES_VIEW_H,C_BG);

    RDraw d;d.scroll=gScrollY;d.y=8;

    char buf[96];

    // — заголовок —
    d.line("     RESULT",C_GREEN,2);
    snprintf(buf,sizeof(buf),"Login: %s",gLogin);
    d.line(buf,C_TEXT,2);
    snprintf(buf,sizeof(buf),"Password: %s",gPass[0]?gPass:"(empty)");
    d.line(buf,C_TEXT,2);
    d.gap(4);
    d.line("Master secret (base58):",C_HINT,2);
    d.longval(gSecretB58,C_GREEN);
    snprintf(buf,sizeof(buf),"Time: %lum %lus",
             (unsigned long)gElapsedSec/60,(unsigned long)gElapsedSec%60);
    d.line(buf,C_HINT,2);
    d.sep();

    // — 3 ключевые пары —
    for(int i=0;i<3;i++){
        d.line(KEY_LABELS[i],C_ACCENT,2);
        d.line("Pub:",C_HINT,2);
        d.longval(gPubB58[i],C_TEXT);
        d.line("Priv:",C_HINT,2);
        d.longval(gPrivB58[i],C_TEXT);
        d.sep();
    }

    // запомним общую высоту контента для ограничения прокрутки
    gScrollMax=(int32_t)d.y - RES_VIEW_H;
    if(gScrollMax<0)gScrollMax=0;
}

static void drawResultBtnBar(){
    // кнопка "Новый" — фиксирована внизу
    gfx->fillRect(0,RES_VIEW_H,DISP_W,RES_BTN_H,C_BG);
    uiRect(20,RES_VIEW_H+6,DISP_W-40,RES_BTN_H-12,C_RED,0,6);
    gfx->setTextSize(2);gfx->setTextColor(C_TEXT);
    gfx->setCursor(DISP_W/2-72,RES_VIEW_H+18);
    gfx->print("Generate new");
}

static void drawResultFull(){
    drawResultContent();
    drawResultBtnBar();
}

// ═══════════════════════════════════════════════════════════
//  UI — ДИАЛОГ ПОДТВЕРЖДЕНИЯ
// ═══════════════════════════════════════════════════════════
static void drawConfirmDialog(){
    gfx->fillRect(40,120,DISP_W-80,260,C_KEY);
    gfx->drawRoundRect(40,120,DISP_W-80,260,8,C_KEY_HL);
    gfx->setTextSize(2);gfx->setTextColor(C_TEXT);
    gfx->setCursor(60,140);gfx->print("Generate new?");
    gfx->setTextSize(1);gfx->setTextColor(C_HINT);
    gfx->setCursor(60,172);gfx->print("This will ERASE the saved");
    gfx->setCursor(60,184);gfx->print("secret and keys from memory.");
    // кнопки
    uiRect(60,240,140,56,C_RED,0,6);
    gfx->setTextSize(2);gfx->setTextColor(C_TEXT);gfx->setCursor(106,260);gfx->print("Yes");
    uiRect(280,240,140,56,C_KEY_HL,C_SEP,6);
    gfx->setCursor(316,260);gfx->print("No");
}

// ═══════════════════════════════════════════════════════════
//  TOUCH HANDLER
// ═══════════════════════════════════════════════════════════
static void handleTap(int tx,int ty){
    if(gState==ST_LOGIN||gState==ST_PASS){
        bool isPass=(gState==ST_PASS);
        char *buf=isPass?gPass:gLogin;
        char c=kbTouch(tx,ty);
        if(!c)return;
        if(c=='\x03'){gKbNums=!gKbNums;clearInputStatus();drawInputScreen(isPass?"Password:":"Login:",buf,isPass);return;}
        if(c=='\x08'){int n=strlen(buf);if(n>0)buf[n-1]='\0';clearInputStatus();}
        else if(c=='\x0D'){
            if(gState==ST_LOGIN){
                normalizeLoginInPlace(buf);
                if(!strlen(buf)){
                    setInputStatus("Логин обязателен");
                    drawInputScreen("Login:",gLogin,false);
                    return;
                }
                clearInputStatus();
                gState=ST_PASS;drawInputScreen("Password:",gPass,true);return;
            } else {
                clearInputStatus();
                gState=ST_RUNNING;
                gfx->fillScreen(C_BG);drawProgressHeader();
                SD_MMC.remove(SD_MEM_FILE);
                gSdFile=SD_MMC.open(SD_MEM_FILE,"w+");
                if(!gSdFile){
                    gfx->setTextSize(2);gfx->setTextColor(C_RED);
                    gfx->setCursor(20,200);gfx->println("SD open failed");
                    gState=ST_ERROR;return;
                }
                normalizeLoginInPlace(gLogin);
                if(!gLogin[0]){
                    gSdFile.close();
                    SD_MMC.remove(SD_MEM_FILE);
                    gState=ST_LOGIN;
                    setInputStatus("Логин обязателен");
                    drawInputScreen("Login:",gLogin,false);
                    return;
                }
                if(!gPass[0]){
                    deriveLegacyMasterSecret(gPass,gSecret);
                    gElapsedSec=0;
                    completeResultFromSecret();
                    gSdFile.close();
                    SD_MMC.remove(SD_MEM_FILE);
                    gState=ST_DONE;
                    gScrollY=0;
                    drawResultFull();
                    return;
                }
                argon2Init(gLogin,gPass);
                drawProgress();return;
            }
        } else {int n=strlen(buf);if(n<63){buf[n]=c;buf[n+1]='\0';}clearInputStatus();}
        drawInputScreen(isPass?"Password:":"Login:",buf,isPass);
        return;
    }

    if(gState==ST_DONE||gState==ST_SAVED){
        // Кнопка "Новый" внизу
        if(ty>=RES_VIEW_H){
            gState=ST_CONFIRM;
            drawConfirmDialog();
        }
        return;
    }

    if(gState==ST_CONFIRM){
        if(tx>=60&&tx<200&&ty>=240&&ty<296){
            // Да
            nvsClear();
            memset(gLogin,0,sizeof(gLogin));memset(gPass,0,sizeof(gPass));
            gKbNums=false;gScrollY=0;
            gState=ST_LOGIN;
            drawInputScreen("Login:",gLogin,false);
        } else if(tx>=280&&tx<420&&ty>=240&&ty<296){
            // Нет
            gState=ST_DONE;
            drawResultFull();
        }
        return;
    }
}

// ═══════════════════════════════════════════════════════════
//  SETUP
// ═══════════════════════════════════════════════════════════
void setup(){
    Serial.begin(115200);
    Wire.begin(PIN_I2C_SDA,PIN_I2C_SCL);

    // Display
    gfx->begin();gBus->writeC8D8(0x36,0xA0);
    gfx->setBrightness(200);gfx->fillScreen(C_BG);

    // Touch
    gTouch.setPins(PIN_TP_INT,-1);
    gTouch.begin(Wire,CST92XX_SLAVE_ADDRESS,PIN_I2C_SDA,PIN_I2C_SCL);
    gTouch.setMaxCoordinates(DISP_W,DISP_H);
    gTouch.setSwapXY(true);gTouch.setMirrorXY(true,false);
    attachInterrupt(PIN_TP_INT,onTouchIrq,FALLING);

    // SD
    gpio_reset_pin(GPIO_NUM_2);
    pinMode(PIN_SD_CMD,INPUT_PULLUP);pinMode(PIN_SD_D0,INPUT_PULLUP);
    delay(20);
    SD_MMC.setPins(PIN_SD_CLK,PIN_SD_CMD,PIN_SD_D0);
    bool sdOk=SD_MMC.begin("/sdcard",true);
    Serial.printf("SD: %s\n",sdOk?"OK":"FAIL");
    if(sdOk)Serial.printf("SD size: %llu MB\n",SD_MMC.cardSize()/(1024ULL*1024));
    if(!sdOk){
        gfx->setTextSize(2);gfx->setTextColor(C_RED);
        gfx->setCursor(20,160);gfx->println("SD CARD ERROR");
        gfx->setTextSize(1);gfx->setTextColor(C_HINT);
        gfx->setCursor(20,200);gfx->println("FAT32 card required.");
        while(true)delay(1000);
    }

    // BLAKE2b self-test: BLAKE2b-512("") = 786a02f7...
    {
        static const uint8_t EXP[64]={
            0x78,0x6a,0x02,0xf7,0x42,0x01,0x59,0x03,0xc6,0xc6,0xfd,0x85,0x25,0x52,0xd2,0x72,
            0x91,0x2f,0x47,0x40,0xe1,0x58,0x47,0x61,0x8a,0x86,0xe2,0x17,0xf7,0x1f,0x54,0x19,
            0xd2,0x5e,0x10,0x31,0xaf,0xee,0x58,0x53,0x13,0x89,0x64,0x44,0x93,0x4e,0xb0,0x4b,
            0x90,0x3a,0x68,0x5b,0x14,0x48,0xb7,0x55,0xd5,0x6f,0x70,0x1a,0xfe,0x9b,0xe2,0xce
        };
        uint8_t got[64];
        b2_init(&gB2S,64);b2_final(&gB2S,got);
        bool ok=(memcmp(got,EXP,64)==0);
        Serial.printf("BLAKE2b-512('') : %s\n",ok?"PASS":"FAIL");
        if(!ok){
            Serial.print("got: ");
            for(int i=0;i<64;i++)Serial.printf("%02x",got[i]);
            Serial.println();
        }
    }

    // PSRAM буферы
    gBufPrev=(uint8_t*)ps_malloc(A2_BLKSZ);gBufRef=(uint8_t*)ps_malloc(A2_BLKSZ);
    gBufOut=(uint8_t*)ps_malloc(A2_BLKSZ);gBufZero=(uint8_t*)ps_calloc(1,A2_BLKSZ);
    gBufAddr=(uint8_t*)ps_malloc(A2_BLKSZ);
    gJ1Seg=(uint32_t*)ps_malloc(A2_SEG*sizeof(uint32_t));
    if(!gBufPrev||!gBufRef||!gBufOut||!gBufZero||!gBufAddr||!gJ1Seg){
        gfx->setTextSize(2);gfx->setTextColor(C_RED);
        gfx->setCursor(20,200);gfx->println("PSRAM alloc failed");
        while(true)delay(1000);
    }

    // Попытка загрузить сохранённый результат из NVS
    if(nvsLoad()){
        gState=ST_SAVED;
        drawResultFull();
    }else{
        drawInputScreen("Login:",gLogin,false);
    }
}

// ═══════════════════════════════════════════════════════════
//  LOOP
// ═══════════════════════════════════════════════════════════
#define BLOCKS_PER_TICK 4

void loop(){
    // ── Touch polling (для свайпа + обычных нажатий) ──────
    int16_t tx=0,ty=0;
    bool nowTouching=(gTouch.getPoint(&tx,&ty,1)>0);

    if(nowTouching&&!gTouching){
        // начало касания
        gTouchStartY=ty;gTouchPrevY=ty;gTouchCurY=ty;gTouchCurX=tx;
        gTouching=true;
    } else if(nowTouching&&gTouching){
        int16_t delta=gTouchPrevY-ty;
        gTouchCurY=ty;gTouchPrevY=ty;gTouchCurX=tx;
        // живая прокрутка результата
        if((gState==ST_DONE||gState==ST_SAVED)&&abs(delta)>0){
            gScrollY+=delta;
            if(gScrollY<0)gScrollY=0;
            if(gScrollY>gScrollMax)gScrollY=gScrollMax;
            drawResultContent();  // перерисовать только контент (без кнопки)
        }
    } else if(!nowTouching&&gTouching){
        // конец касания: если почти не скроллили — считаем нажатием
        int16_t swipeDelta=abs(gTouchStartY-gTouchCurY);
        if(swipeDelta<15)handleTap(gTouchCurX,gTouchCurY);
        gTouching=false;
    }

    // ── Argon2id вычисление ────────────────────────────────
    if(gState==ST_RUNNING&&gInitDone){
        bool done=false;
        for(int i=0;i<BLOCKS_PER_TICK&&!done;i++)
            done=argon2Step();
        drawProgress();
        if(done){
            argon2Finalize();
            gSdFile.close();
            gState=ST_DONE;
            gScrollY=0;
            drawResultFull();
        }
    }
}
