using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UIElements;

namespace VibeCopilot.Editor
{
    // Fereastra principala — REScrisa in UI Toolkit ca sa arate pixel-perfect ca designul.
    // Toata logica (server, generare 3D, fix-error, credite) sta in VibeBackendClient / VibeSceneActions / VibeGen3D.
    public class VibeCopilotWindow : EditorWindow
    {
        [MenuItem("Tools/VibeCopilot")]
        public static void Open()
        {
            Type inspector = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.InspectorWindow");
            VibeCopilotWindow w = inspector != null
                ? GetWindow<VibeCopilotWindow>("VibeCopilot", true, inspector)
                : GetWindow<VibeCopilotWindow>("VibeCopilot");
            w.minSize = new Vector2(320f, 420f);
        }

        // ---- Date ----
        [Serializable] private class Msg
        {
            public bool FromUser;
            public bool IsAi;
            public string Text;
            public string RawReply;
            public string Code;
            public string FileName;
            public string SceneNote;   // ex: "Car (GameObject)" — chip "in scene"
            public int Tokens;
            public DirectionOption[] Directions;  // optiuni clickabile (mint-style)
            public bool DirectionsUsed;           // s-a ales o directie din setul asta -> blocam restul
            public string DirectionsPicked;       // promptul ales (pt evidentiere)
            public string ImageUrl;    // poza de concept a modelului 3D (aratata in bula)
            public VibeGen3D.PendingGen Pending;  // date pt butonul "Generate 3D" (mesh din poza)
            public bool GenDone;        // meshul a fost pornit -> ascunde butoanele de pe cardul de preview
            // Cardul SEPARAT de generare (apare sub preview cand se apasa "Generate").
            public bool GenCard;        // acest mesaj = cardul de loading, nu unul de preview
            public string GenId;        // leaga cardul de evenimentele de progres din VibeGen3D
            public int GenPct;          // procentul REAL raportat de server (0..100)
            public int GenState;        // 0 = in lucru, 1 = gata (added to scene), 2 = esuat
            // Cardul de ALEGERE (Decor / Rigid body): apare la "Generate", inainte de generare.
            // Mesaj de INTERFATA (nu conversatie): "Here's the concept...", "Sure — tell me what
            // to change...", intrebarea decor/rigid. NU intra in istoricul trimis modelului —
            // altfel modelul le vede ca replici de-ale lui si incepe sa le papagaleasca.
            public bool UiNote;
            public bool ChoiceCard;     // acest mesaj = intrebarea decor vs rigid body
            public int ChoicePick;      // 0 = inca nu s-a ales, 1 = decor, 2 = rigid body
            public bool Rigid;          // pe cardul de generare: s-a cerut taiere in piese fizice
            public int RigidState;      // 0 = n/a, 1 = de pornit dupa mesh, 2 = piese gata, 3 = esuat
            // Momentul pornirii, ca sa nu se reseteze contorul de secunde la fiecare RebuildBody.
            [System.NonSerialized] public double GenStart;
            [System.NonSerialized] public Texture2D PreviewTex;  // poza deja descarcata (cache: nu re-descarcam la fiecare rebuild)
            [System.NonSerialized] public bool Typing;           // textul AI se scrie literă cu literă (typewriter)
        }
        [Serializable] private class SaveData { public List<Msg> Messages = new List<Msg>(); }
        // Chaturi MULTIPLE (ca mint): fiecare sesiune = titlu + istoric propriu.
        [Serializable] private class ChatSession { public string id; public string title = "New chat"; public long updated; public List<Msg> Messages = new List<Msg>(); }
        [Serializable] private class ChatStore { public List<ChatSession> sessions = new List<ChatSession>(); public string activeId; }

        private const string ChatKey = "VibeCopilot.Chat";      // vechi (single) — doar pt migrare
        private const string ChatsKey = "VibeCopilot.Chats";    // nou (multi-sesiune)
        private ChatStore _store;
        private Button _chatsBtn;
        private List<Msg> _messages = new List<Msg>();
        private string _input = "";
        private VisualElement _lightbox;   // overlay poza-mare + descriere (stil mint "DETAILS")
        private VisualElement _overlay;    // overlay modal/drawer (biblioteca, sidebar chaturi, settings)
        private bool _thinking;
        private bool _awaitingPreview;   // asteptam poza de concept 3D -> tinem spinnerul, ascundem textul
        // Conceptul care se corecteaza dupa "Make a tweak". Cat timp e setat, urmatorul mesaj
        // scris de user NU e chat: e o corectie care regenereaza poza. Se goleste dupa folosire.
        private VibeGen3D.PendingGen _tweakFor;
        // Mesajul "Sure — tell me what to change...". Corectia se accepta DOAR daca acest mesaj
        // e inca ultimul din chat. Altfel un "Make a tweak" apasat si abandonat ar rapi mai
        // tarziu o intrebare normala si ar trimite-o in fluxul de generare 3D.
        private Msg _tweakNote;
        private bool _inputFocused;

        // ---- Referinte UI ----
        private VisualElement _body;
        private ScrollView _chatScroll;
        private TextField _inputField;
        private Label _placeholder;
        private VisualElement _popover;     // overlay activ (model menu / usage)
        private TextField _keyField;        // campul Access Key din popover-ul de setari (salvat la inchidere)
        private Button _modelBtn;           // butonul "Copilot Flash" (ancora pt meniul de model)
        private VisualElement _updateBanner; // banner "Update available" (auto-updater)
        private Label _updateLabel;
        private Button _updateBtn;

        // ---- Culori (design "Midnight" 1b) ----
        private static Color C(string hex) { ColorUtility.TryParseHtmlString(hex, out Color c); return c; }
        private static readonly Color Panel   = C("#111113");   // fundal de baza (sub gradient)
        private static readonly Color HeaderBg = C("#17181b");  // bara de sus
        private static readonly Color Accent  = C("#5b7cfa");   // albastru Midnight
        private static readonly Color AccentHi = C("#93a8ff");  // albastru deschis (text/hover)
        // Tonuri Midnight
        private static readonly Color BgTop    = C("#1c1c20");  // varful gradientului
        private static readonly Color BgBot    = C("#111113");  // baza gradientului
        private static readonly Color Surface  = C("#1e1f23");  // carduri / composer solid
        private static readonly Color SurfaceHi = C("#232428"); // panouri (popover/meniu)
        private static readonly Color Hairline = C("#26272d");  // linii/borduri discrete (~rgba white .07)
        private static readonly Color TextPri  = C("#eceef4");  // text principal
        private static readonly Color TextSec  = C("#b9bdcc");  // text secundar
        private static readonly Color TextMut  = C("#8f95a8");  // text estompat
        private static readonly Color TextFaint = C("#70768a"); // placeholder / hint

        private void OnEnable()
        {
            Load();
            ReconcileGenCards();
            VibeGen3D.PreviewReady -= OnPreviewReady; VibeGen3D.PreviewReady += OnPreviewReady;
            VibeGen3D.PreviewSkipped -= OnPreviewSkipped; VibeGen3D.PreviewSkipped += OnPreviewSkipped;
            VibeGen3D.GenProgress -= OnGenProgress; VibeGen3D.GenProgress += OnGenProgress;
            VibeGen3D.GenFinished -= OnGenFinished; VibeGen3D.GenFinished += OnGenFinished;
            VibeGen3D.RigidFinished -= OnRigidFinished; VibeGen3D.RigidFinished += OnRigidFinished;
        }
        private void OnDisable()
        {
            Save();
            VibeUpdater.OnChecked -= OnUpdateChecked;
            VibeGen3D.PreviewReady -= OnPreviewReady;
            VibeGen3D.PreviewSkipped -= OnPreviewSkipped;
            VibeGen3D.GenProgress -= OnGenProgress;
            VibeGen3D.GenFinished -= OnGenFinished;
            VibeGen3D.RigidFinished -= OnRigidFinished;
        }

        // Progresul REAL al meshului (acelasi procent din Console) -> bara de pe cardul de loading.
        // Doar mutam valoarea pe mesaj: scheduler-ul cardului o citeste la fiecare tick si trage
        // bara lin catre ea. FARA RebuildBody, ca sa nu se piarda animatia sau pozitia scrollului.
        private void OnGenProgress(string genId, int pct)
        {
            Msg m = FindGen(genId);
            if (m == null || m.GenState != 0) return;
            if (pct > m.GenPct) m.GenPct = pct;      // niciodata inapoi
        }

        // Modelul e in scena (sau a esuat) -> cardul trece pe starea finala "Added to scene".
        private void OnGenFinished(string genId, bool ok)
        {
            Msg m = FindGen(genId);
            if (m == null || m.GenState != 0) return;
            m.GenState = ok ? 1 : 2;
            if (ok) m.GenPct = 100;
            if (!ok && m.Rigid) m.RigidState = 3;   // fara mesh nu are ce se taia in piese
            Save();
            RebuildBody();   // starea finala schimba structura cardului (dispare bara/orbita)
        }

        // Un card de generare salvat ca "in lucru" nu poate supravietui unei recompilari de
        // scripturi (pollingul din VibeGen3D moare odata cu domain reload-ul). Daca VibeGen3D nu
        // mai stie de el, il inchidem ca esuat — altfel ar ramane agatat, cu orbita la infinit.
        private void ReconcileGenCards()
        {
            for (int i = 0; i < _messages.Count; i++)
            {
                Msg m = _messages[i];
                if (m.GenCard && m.GenState == 0 && !VibeGen3D.IsGenActive(m.GenId)) m.GenState = 2;
                // La fel pt taierea in piese: daca nu mai e in lucru, nu mai vine niciodata.
                if (m.GenCard && m.Rigid && m.RigidState == 1 && !VibeGen3D.IsRigidActive(m.GenId)) m.RigidState = 3;
            }
        }

        private Msg FindGen(string genId)
        {
            if (string.IsNullOrEmpty(genId)) return null;
            for (int i = _messages.Count - 1; i >= 0; i--)
                if (_messages[i].GenCard && _messages[i].GenId == genId) return _messages[i];
            return null;
        }

        // Serverul a facut POZA de concept. Oprim spinnerul si o aratam in chat cu butoanele
        // [Generate 3D] / [Make changes]. Meshul (care costa) se face abia la "Generate 3D".
        private void OnPreviewReady(VibeGen3D.PendingGen p)
        {
            _thinking = false; _awaitingPreview = false; _tweakFor = null; _tweakNote = null;
            if (p == null || string.IsNullOrEmpty(p.imageUrl)) { RebuildBody(); return; }
            _messages.Add(new Msg
            {
                FromUser = false, IsAi = true, ImageUrl = p.imageUrl, Pending = p, UiNote = true,
                Text = "Here's the concept. Turn it into a 3D model, or tweak it first.",
            });
            Save();
            RebuildBody();
        }

        // Fluxul 3D s-a rezolvat FARA poza de concept (model din biblioteca, blocat, sau esec).
        // Oprim spinnerul; daca avem un mesaj scurt de aratat, il punem in chat.
        private void OnPreviewSkipped(string note)
        {
            _thinking = false; _awaitingPreview = false; _tweakFor = null; _tweakNote = null;
            if (!string.IsNullOrEmpty(note))
                _messages.Add(new Msg { FromUser = false, IsAi = true, Text = note });
            Save();
            RebuildBody();
        }

        // ================= UI =================
        public void CreateGUI()
        {
            VisualElement root = rootVisualElement;
            root.style.flexGrow = 1;
            root.style.backgroundColor = Panel;
            VibeFonts.Set(root, VibeFonts.SansRegular);   // font default Midnight (mostenit de copii)

            // Fundal cu gradient vertical (varf mai deschis -> baza inchisa), sub tot continutul.
            root.Add(BuildGradientBackground());

            BuildHeader(root);

            _updateBanner = BuildUpdateBanner();
            root.Add(_updateBanner);

            // Rand "Found a bug? Report it here" — PERMANENT (sub header), ca sa fie vizibil si
            // pe ecranul gol si in chat. Sta in root, nu in _body (care se redeseneaza mereu).
            root.Add(BuildFeedbackLine());

            _body = new VisualElement();
            _body.style.flexGrow = 1;
            root.Add(_body);

            RebuildBody();

            // Auto-updater: cand verificarea de versiune se termina, redesenam bannerul.
            VibeUpdater.OnChecked -= OnUpdateChecked;
            VibeUpdater.OnChecked += OnUpdateChecked;
            RefreshUpdateBanner();

            // Click in afara popover-ului -> il inchide.
            root.RegisterCallback<PointerDownEvent>(evt =>
            {
                if (_popover != null && !(evt.target is VisualElement t && IsInside(t, _popover)))
                    ClosePopover();
            }, TrickleDown.TrickleDown);
        }

        private static bool IsInside(VisualElement node, VisualElement parent)
        {
            for (VisualElement v = node; v != null; v = v.parent)
                if (v == parent) return true;
            return false;
        }

        // Fundal Midnight: gradient vertical desenat cu un mesh (UI Toolkit nu are gradient in USS).
        // Umple toata fereastra, sta in spate, nu prinde click-uri.
        private VisualElement BuildGradientBackground()
        {
            VisualElement bg = new VisualElement();
            bg.style.position = Position.Absolute;
            bg.style.left = 0; bg.style.top = 0; bg.style.right = 0; bg.style.bottom = 0;
            bg.pickingMode = PickingMode.Ignore;
            bg.generateVisualContent += ctx =>
            {
                Rect r = ctx.visualElement.contentRect;
                if (r.width <= 0f || r.height <= 0f) return;
                MeshWriteData mesh = ctx.Allocate(4, 6);
                Color top = BgTop, bot = BgBot;
                Vertex[] v = new Vertex[4];
                v[0].position = new Vector3(r.xMin, r.yMin, Vertex.nearZ); v[0].tint = top;
                v[1].position = new Vector3(r.xMax, r.yMin, Vertex.nearZ); v[1].tint = top;
                v[2].position = new Vector3(r.xMax, r.yMax, Vertex.nearZ); v[2].tint = bot;
                v[3].position = new Vector3(r.xMin, r.yMax, Vertex.nearZ); v[3].tint = bot;
                mesh.SetAllVertices(v);
                mesh.SetAllIndices(new ushort[] { 0, 1, 2, 2, 3, 0 });
            };
            bg.RegisterCallback<GeometryChangedEvent>(_ => bg.MarkDirtyRepaint());
            return bg;
        }

        private void BuildHeader(VisualElement root)
        {
            VisualElement bar = Row();
            bar.style.height = 44;
            bar.style.backgroundColor = HeaderBg;
            bar.style.alignItems = Align.Center;
            bar.style.justifyContent = Justify.SpaceBetween;
            SetPadding(bar, 0, 16, 0, 16);
            bar.style.borderBottomWidth = 1; bar.style.borderBottomColor = Hairline;

            VisualElement left = Row(); left.style.alignItems = Align.Center;
            left.Add(MakeLogo(24f));
            Label title = new Label("vibecopilot");
            title.style.marginLeft = 9; title.style.fontSize = 13; title.style.color = TextPri;
            VibeFonts.Set(title, VibeFonts.MonoSemiBold);
            left.Add(title);
            bar.Add(left);

            VisualElement right = Row(); right.style.alignItems = Align.Center;
            Button newBtn = TextButton("＋  New", () => NewChat());
            newBtn.style.backgroundColor = Color.clear; newBtn.style.color = TextSec; newBtn.style.fontSize = 12;
            VibeFonts.Set(newBtn, VibeFonts.SansMedium);
            SetBorder(newBtn, 0, Color.clear, 5); SetPadding(newBtn, 4, 6, 4, 6);
            newBtn.RegisterCallback<MouseEnterEvent>(_ => newBtn.style.color = TextPri);
            newBtn.RegisterCallback<MouseLeaveEvent>(_ => newBtn.style.color = TextSec);
            right.Add(newBtn); right.Add(Spacer(6));
            Button chatsBtn = TextButton("Chats", () => ToggleChatsPopover());
            chatsBtn.style.backgroundColor = Color.clear; chatsBtn.style.color = TextFaint; chatsBtn.style.fontSize = 12;
            VibeFonts.Set(chatsBtn, VibeFonts.SansMedium);
            SetBorder(chatsBtn, 0, Color.clear, 5); SetPadding(chatsBtn, 4, 6, 4, 6);
            chatsBtn.RegisterCallback<MouseEnterEvent>(_ => chatsBtn.style.color = TextSec);
            chatsBtn.RegisterCallback<MouseLeaveEvent>(_ => chatsBtn.style.color = TextFaint);
            _chatsBtn = chatsBtn;
            right.Add(chatsBtn);
            bar.Add(right);

            root.Add(bar);
        }

        // ---- Banner auto-update (ascuns pana serverul confirma o versiune mai noua) ----
        private VisualElement BuildUpdateBanner()
        {
            VisualElement b = Row();
            b.style.display = DisplayStyle.None;
            b.style.height = 30;
            b.style.alignItems = Align.Center;
            b.style.justifyContent = Justify.SpaceBetween;
            b.style.backgroundColor = C("#2d4a3e");
            b.style.borderBottomWidth = 1; b.style.borderBottomColor = C("#1f3b30");
            SetPadding(b, 0, 12, 0, 12);

            _updateLabel = new Label("Update available");
            _updateLabel.style.fontSize = 12; _updateLabel.style.color = C("#cfeee0");
            b.Add(_updateLabel);

            _updateBtn = TextButton("Update now", OnUpdateClicked);
            _updateBtn.style.backgroundColor = C("#3ba776");
            _updateBtn.style.color = Color.white; _updateBtn.style.fontSize = 12;
            SetBorder(_updateBtn, 0, Color.clear, 5);
            SetPadding(_updateBtn, 4, 10, 4, 10);
            b.Add(_updateBtn);
            return b;
        }

        private void RefreshUpdateBanner()
        {
            if (_updateBanner == null) return;
            bool show = VibeUpdater.UpdateAvailable;
            _updateBanner.style.display = show ? DisplayStyle.Flex : DisplayStyle.None;
            if (show && _updateLabel != null)
                _updateLabel.text = "New version available (v" + VibeUpdater.LatestVersion + ")";
        }

        private void OnUpdateChecked() { RefreshUpdateBanner(); }

        private void OnUpdateClicked()
        {
            if (_updateLabel != null) _updateLabel.text = "Updating VibeCopilot…";
            if (_updateBtn != null) _updateBtn.SetEnabled(false);
            VibeUpdater.DownloadAndImport((ok, info) =>
            {
                if (!ok)
                {
                    if (_updateLabel != null) _updateLabel.text = "Update failed — try again";
                    if (_updateBtn != null) _updateBtn.SetEnabled(true);
                }
                // daca ok: importul pachetului recompileaza pluginul si fereastra se reincarca singura.
            });
        }

        private void RebuildBody()
        {
            _body.Clear();
            ClosePopover();
            if (_messages.Count == 0 && !_thinking) BuildEmptyView();
            else BuildChatView();
        }

        // ---- Empty state (Home): logo cu glow + titlu + composer centrat; jos Upgrade + versiune ----
        private void BuildEmptyView()
        {
            VisualElement center = new VisualElement();
            center.style.flexGrow = 1;
            center.style.alignItems = Align.Center;
            center.style.justifyContent = Justify.Center;
            SetPadding(center, 24, 30, 24, 30);

            // Logo mare 56px, curat (fara halou).
            center.Add(MakeLogo(56f, true));

            Label h = new Label("What are you creating today?");
            h.style.fontSize = 22; h.style.color = TextPri;
            VibeFonts.Set(h, VibeFonts.SansMedium);
            h.style.marginTop = 18; h.style.marginBottom = 22; h.style.unityTextAlign = TextAnchor.MiddleCenter;
            center.Add(h);

            center.Add(BuildComposer(true));

            _body.Add(center);

            // ---- Bara de jos: doar eticheta de versiune (mono). Butonul Upgrade a fost scos. ----
            VisualElement bottom = new VisualElement();
            bottom.style.flexShrink = 0;
            bottom.style.alignItems = Align.Center;
            SetPadding(bottom, 6, 14, 14, 14);

            Label ver = new Label("VIBECOPILOT v2.0");
            ver.style.fontSize = 10; ver.style.color = C("#3c4050"); ver.style.letterSpacing = 1.3f; ver.style.marginTop = 12;
            VibeFonts.Set(ver, VibeFonts.MonoRegular);
            bottom.Add(ver);

            _body.Add(bottom);
        }

        // ---- Chat: mesaje + composer jos ----
        private void BuildChatView()
        {
            _chatScroll = new ScrollView(ScrollViewMode.Vertical);
            _chatScroll.style.flexGrow = 1;
            // FIX: fara minHeight=0, ScrollView-ul creste cat tot continutul (min-height implicit
            // = auto) si IMPINGE composer-ul jos sub fereastra. Cu 0 are voie sa se micsoreze ->
            // ia doar spatiul ramas, mesajele intra in scroll, butoanele raman fixe jos.
            _chatScroll.style.minHeight = 0;
            _chatScroll.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
            _chatScroll.verticalScrollerVisibility = ScrollerVisibility.Hidden;
            SetPadding(_chatScroll.contentContainer, 16, 14, 16, 14);

            foreach (Msg m in _messages)
            {
                // Protectie per-mesaj: daca UN mesaj crapa la randare, nu mai omoara tot chat-ul
                // (inainte, o exceptie in foreach stergea toate mesajele + composer-ul).
                try { _chatScroll.Add(MessageElement(m)); }
                catch (System.Exception e) { Debug.LogError("[VibeCopilot] MessageElement failed: " + e); }
            }
            if (_thinking) _chatScroll.Add(ThinkingElement());

            _body.Add(_chatScroll);

            VisualElement footer = new VisualElement();
            footer.style.flexShrink = 0; // composer-ul nu se micsoreaza niciodata -> ramane intreg jos
            footer.style.backgroundColor = Color.clear; // se vede gradientul de fundal, fara banda separata
            SetPadding(footer, 12, 14, 14, 14);
            footer.Add(BuildComposer(false));
            _body.Add(footer);

            _chatScroll.schedule.Execute(() => _chatScroll.scrollOffset = new Vector2(0, 99999)).StartingIn(30);
        }

        // ---- Composer Midnight: o cutie rotunjita care contine textul SI randul de iconite ----
        private VisualElement BuildComposer(bool emptyState)
        {
            VisualElement wrap = new VisualElement(); wrap.style.width = Length.Percent(100);

            VisualElement box = new VisualElement();
            box.style.backgroundColor = C("#202127");   // rgba alb .045 peste fundalul inchis
            SetBorder(box, 1, emptyState ? C("#3a4468") : C("#2c2d33"), 16);
            SetPadding(box, 16, 16, 12, 16);
            box.style.minHeight = 44;
            float txtSize = emptyState ? 14f : 13f;

            // --- caseta de text (cu placeholder suprapus) ---
            VisualElement inputArea = new VisualElement();
            inputArea.style.minHeight = 22; inputArea.style.justifyContent = Justify.Center;

            _inputField = new TextField { multiline = true };
            _inputField.maxLength = 150;   // limita: intrebarea userului = max 150 caractere
            _inputField.value = _input;
            _inputField.style.backgroundColor = Color.clear;
            _inputField.style.fontSize = txtSize; _inputField.style.color = TextPri;
            VibeFonts.Set(_inputField, VibeFonts.SansRegular);
            ResetMargins(_inputField);                 // fara marginile implicite ale TextField
            var inner = _inputField.Q(className: "unity-text-field__input");
            if (inner != null)
            {
                inner.style.backgroundColor = Color.clear; SetBorder(inner, 0, Color.clear, 0);
                ResetMargins(inner);
                inner.style.paddingTop = 0; inner.style.paddingBottom = 0; inner.style.paddingLeft = 0; inner.style.paddingRight = 0;
            }
            _inputField.RegisterValueChangedCallback(e => { _input = e.newValue; UpdatePlaceholder(); });
            _inputFocused = false;
            _inputField.RegisterCallback<FocusInEvent>(_ => { _inputFocused = true; UpdatePlaceholder(); });
            _inputField.RegisterCallback<FocusOutEvent>(_ => { _inputFocused = false; UpdatePlaceholder(); });
            _inputField.RegisterCallback<KeyDownEvent>(e =>
            {
                if ((e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) && !e.shiftKey)
                { e.StopPropagation(); Send(); }
            }, TrickleDown.TrickleDown);

            _placeholder = new Label("Describe what you'd like to create…");
            _placeholder.style.position = Position.Absolute;
            _placeholder.style.left = 1; _placeholder.style.top = 3;
            _placeholder.style.fontSize = txtSize; _placeholder.style.color = TextFaint;
            VibeFonts.Set(_placeholder, VibeFonts.SansRegular);
            _placeholder.pickingMode = PickingMode.Ignore;

            inputArea.Add(_inputField);
            inputArea.Add(_placeholder);
            box.Add(inputArea);
            UpdatePlaceholder();

            // --- randul de iconite/actiuni, IN interiorul cutiei ---
            VisualElement row = Row(); row.style.alignItems = Align.Center; row.style.marginTop = 16;

            Button photo = PlainIconButton(20f, DrawPhoto, () => AttachPhoto());
            photo.tooltip = "Make a 3D model from your own photo";
            row.Add(photo); row.Add(Spacer(2));

            Button lib = PlainIconButton(20f, DrawCube, () => ToggleLibraryPopover());
            lib.tooltip = "3D model library — 1000+ ready-made models, free to add";
            row.Add(lib); row.Add(Spacer(8));

            Button model = ModelButton();
            row.Add(model); row.Add(Spacer(14));

            Button fix = FixButton("Fix error", () => FixError());
            row.Add(fix);

            VisualElement grow = new VisualElement(); grow.style.flexGrow = 1; row.Add(grow);

            Button send = SendCircle(() => Send());
            row.Add(send);

            box.Add(row);
            wrap.Add(box);
            return wrap;
        }

        private void UpdatePlaceholder()
        {
            if (_placeholder != null)
                _placeholder.style.display = (string.IsNullOrEmpty(_input) && !_inputFocused) ? DisplayStyle.Flex : DisplayStyle.None;
        }

        // ================= Mesaje =================
        private VisualElement MessageElement(Msg m)
        {
            VisualElement wrap = new VisualElement(); wrap.style.marginBottom = 16;

            if (m.FromUser)
            {
                // Bula user (design): fundal #2a2b30, colturi 16, fara chenar, dreapta.
                VisualElement bubble = new VisualElement();
                bubble.style.alignSelf = Align.FlexEnd;
                bubble.style.maxWidth = Length.Percent(78);
                bubble.style.backgroundColor = C("#2a2b30");
                SetBorder(bubble, 0, Color.clear, 16);
                SetPadding(bubble, 9, 15, 9, 15);

                Label b = new Label(m.Text);
                b.style.fontSize = 13; b.style.color = TextPri;
                b.style.whiteSpace = WhiteSpace.Normal;
                VibeFonts.Set(b, VibeFonts.SansRegular);
                bubble.Add(b);
                wrap.Add(bubble);
                return wrap;
            }

            // AI (design): logo + "VibeCopilot" sus, apoi bule flex-start #232428 + carduri sub ele.
            VisualElement col = new VisualElement(); col.style.flexGrow = 1;

            VisualElement aiHead = Row(); aiHead.style.alignItems = Align.Center; aiHead.style.marginBottom = 8;
            aiHead.Add(MakeLogo(22f));
            Label aiName = new Label("VibeCopilot");
            aiName.style.marginLeft = 8; aiName.style.fontSize = 12.5f; aiName.style.color = C("#8b8d95");
            VibeFonts.Set(aiName, VibeFonts.SansBold);
            aiHead.Add(aiName);
            col.Add(aiHead);

            if (!string.IsNullOrEmpty(m.Text))
            {
                VisualElement bubble = new VisualElement();
                bubble.style.alignSelf = Align.FlexStart;
                bubble.style.maxWidth = Length.Percent(88);
                bubble.style.backgroundColor = C("#232428");
                SetBorder(bubble, 0, Color.clear, 16);
                SetPadding(bubble, 9, 15, 9, 15);
                Label txt = new Label(m.Typing ? "" : m.Text);
                txt.style.fontSize = 13; txt.style.color = C("#c9cbd2"); txt.style.whiteSpace = WhiteSpace.Normal;
                VibeFonts.Set(txt, VibeFonts.SansRegular);
                bubble.Add(txt);
                col.Add(bubble);
                if (m.Typing) StartTypewriter(txt, m);   // se scrie litera cu litera
            }

            if (m.ChoiceCard) col.Add(RigidChoiceCard(m));
            else if (m.GenCard) col.Add(GeneratingCard(m));
            else if (!string.IsNullOrEmpty(m.ImageUrl)) col.Add(PreviewImageCard(m));
            if (!string.IsNullOrEmpty(m.Code)) col.Add(CodeCard(m.FileName, m.Code));
            if (!string.IsNullOrEmpty(m.SceneNote)) col.Add(SceneChip(m.SceneNote));
            if (m.Directions != null && m.Directions.Length > 0) col.Add(DirectionsCards(m));

            wrap.Add(col);
            return wrap;
        }

        // Titlu scurt pt cardul de preview (din numele obiectului / slug / prompt).
        private static string PreviewTitle(VibeGen3D.PendingGen p)
        {
            if (p == null) return "3D model";
            string s = p.name;
            if (string.IsNullOrEmpty(s) || s == "Model") s = p.slug;
            if (string.IsNullOrEmpty(s)) s = p.prompt;
            if (string.IsNullOrEmpty(s)) return "3D model";
            s = s.Replace('_', ' ').Trim();
            if (s.Length > 34) s = s.Substring(0, 34) + "…";
            if (s.Length > 0) s = char.ToUpper(s[0]) + s.Substring(1);
            return s;
        }

        // Card de concept 3D ORIZONTAL (design): thumbnail stanga + titlu/badge/actiuni dreapta.
        // Meshul (care costa) se face abia la "Generate" -> VibeGen3D.Commit.
        private VisualElement PreviewImageCard(Msg m)
        {
            // Generarea NU mai transforma acest card: la "Generate" apare un card SEPARAT
            // (GeneratingCard) sub el. Aici doar ascundem butoanele dupa ce s-a pornit.
            bool started = (m.Pending != null) && m.GenDone;

            VisualElement wrap = new VisualElement(); wrap.style.marginTop = 4;

            VisualElement card = new VisualElement();
            card.style.flexDirection = FlexDirection.Row;
            card.style.backgroundColor = C("#1e1f23");
            SetBorder(card, 0f, Color.clear, 16f);
            card.style.overflow = Overflow.Hidden;

            // --- thumbnail (stanga) ---
            VisualElement thumb = new VisualElement();
            thumb.style.width = 128; thumb.style.height = 128; thumb.style.flexShrink = 0;   // 128x128 (handoff)
            thumb.style.backgroundColor = C("#26272c");
            thumb.style.alignItems = Align.Center; thumb.style.justifyContent = Justify.Center;
            Image img = new Image(); img.scaleMode = ScaleMode.ScaleToFit;
            img.style.position = Position.Absolute; img.style.left = 6; img.style.top = 6; img.style.right = 6; img.style.bottom = 6;
            thumb.Add(img);
            Label loading = new Label("…");
            loading.style.fontSize = 11; loading.style.color = C("#7a8494");
            thumb.Add(loading);
            thumb.RegisterCallback<ClickEvent>(ev => OpenPreviewLightbox(m));
            thumb.tooltip = "Click to enlarge";
            LoadTextureInto(img, loading, m);
            card.Add(thumb);

            // --- info (dreapta) ---
            VisualElement info = new VisualElement();
            info.style.flexGrow = 1; info.style.flexShrink = 1; info.style.minWidth = 0;
            info.style.justifyContent = Justify.SpaceBetween;
            SetPadding(info, 13, 13, 12, 14);

            // rand sus: titlu + badge, iar la dreapta iconita de extindere
            VisualElement top = Row(); top.style.justifyContent = Justify.SpaceBetween; top.style.alignItems = Align.FlexStart;
            VisualElement titleCol = new VisualElement(); titleCol.style.flexGrow = 1; titleCol.style.flexShrink = 1; titleCol.style.minWidth = 0;
            Label tl = new Label(PreviewTitle(m.Pending));
            tl.style.fontSize = 14; tl.style.color = TextPri; VibeFonts.Set(tl, VibeFonts.SansBold);
            tl.style.whiteSpace = WhiteSpace.NoWrap; tl.style.overflow = Overflow.Hidden; tl.style.textOverflow = TextOverflow.Ellipsis;
            titleCol.Add(tl);
            Label badge = new Label("PREVIEW · TRIPO_P1");
            badge.style.fontSize = 9; badge.style.color = C("#8b8d95"); badge.style.letterSpacing = 1f; badge.style.marginTop = 6;
            VibeFonts.Set(badge, VibeFonts.MonoMedium);
            titleCol.Add(badge);
            top.Add(titleCol);

            Button expand = new Button(() => OpenPreviewLightbox(m)) { text = "" };
            expand.style.width = 28; expand.style.height = 28; ResetMargins(expand); expand.style.flexShrink = 0; expand.style.marginLeft = 8;
            expand.style.alignItems = Align.Center; expand.style.justifyContent = Justify.Center;
            expand.style.backgroundColor = Color.clear; SetBorder(expand, 1, C("#3a3b41"), 14);
            expand.tooltip = "Enlarge";
            expand.Add(MakeIcon(12f, DrawExpand));
            top.Add(expand);
            info.Add(top);

            // rand jos: butoanele — doar cat timp generarea nu a fost pornita
            if (m.Pending != null && !started)
            {
                VisualElement btnRow = Row(); btnRow.style.justifyContent = Justify.FlexEnd; btnRow.style.alignItems = Align.Center; btnRow.style.marginTop = 10;

                Button change = new Button(() => MakeChanges(m.Pending)) { text = "Make a tweak  ▾" };
                change.style.height = 32; SetPadding(change, 0, 14, 0, 14); ResetMargins(change);
                change.style.fontSize = 12; change.style.color = C("#c9cbd2");
                VibeFonts.Set(change, VibeFonts.SansMedium);
                change.style.backgroundColor = C("#232428"); SetBorder(change, 1, C("#3a3b41"), 8);
                change.RegisterCallback<MouseEnterEvent>(_ => change.style.backgroundColor = C("#2b2c31"));
                change.RegisterCallback<MouseLeaveEvent>(_ => change.style.backgroundColor = C("#232428"));

                Button gen = new Button(() =>
                {
                    if (m.GenDone) return;
                    m.GenDone = true;            // cardul de preview isi ascunde butoanele
                    _tweakFor = null; _tweakNote = null;   // s-a apasat Generate: nu mai asteptam o corectie
                    SaveMyThumb(m);              // poza de concept -> miniatura in "My Assets"

                    // Cardul de preview RAMANE; sub el apare INTAI intrebarea decor vs rigid body.
                    // Generarea porneste abia dupa alegere (StartGeneration), ca sa stim de la
                    // inceput costul intregului drum, nu doar al meshului.
                    Msg ask = new Msg
                    {
                        FromUser = false, IsAi = true, Text = "How should this behave in the scene?",
                        ImageUrl = m.ImageUrl, Pending = m.Pending,
                        ChoiceCard = true, ChoicePick = 0, UiNote = true,
                    };
                    ask.PreviewTex = m.PreviewTex;   // refolosim poza deja descarcata
                    _messages.Add(ask);
                    Save();
                    RebuildBody();
                }) { text = "Generate" };
                gen.style.height = 32; SetPadding(gen, 0, 18, 0, 18); ResetMargins(gen); gen.style.marginLeft = 8;
                gen.style.backgroundColor = Accent; gen.style.color = C("#0c0d12");
                VibeFonts.Set(gen, VibeFonts.SansBold); gen.style.fontSize = 12;
                SetBorder(gen, 0, Color.clear, 8);
                gen.RegisterCallback<MouseEnterEvent>(_ => gen.style.backgroundColor = AccentHi);
                gen.RegisterCallback<MouseLeaveEvent>(_ => gen.style.backgroundColor = Accent);

                btnRow.Add(change); btnRow.Add(gen);
                info.Add(btnRow);
            }

            card.Add(info);
            wrap.Add(card);
            return wrap;
        }

        // ---- Costul in tokeni al fiecarui pas. NU se afiseaza pe cartonase (decizia userului):
        // pe site scrie doar rezervorul de 35.000, omul descopera singur cate modele ies.
        // Cifrele stau aici pt pasul de scadere efectiva a tokenilor.
        private const int MeshTokens  = 2000;   // generarea meshului 3D
        private const int RigidTokens = 2000;   // taierea in piese fizice (peste mesh)

        // CARDUL DE ALEGERE: apare dupa "Generate", inainte sa porneasca generarea.
        // Decor = doar meshul. Rigid body = mesh + taiere in piese, fiecare cu Rigidbody.
        // Alegem INAINTE de generare ca sa stim costul intregului drum dintr-un singur loc.
        private VisualElement RigidChoiceCard(Msg m)
        {
            bool used = m.ChoicePick != 0;

            VisualElement wrap = new VisualElement(); wrap.style.marginTop = 4;

            Label q = new Label("How should this behave in the scene?");
            q.style.fontSize = 12.5f; q.style.color = TextSec; q.style.whiteSpace = WhiteSpace.Normal;
            VibeFonts.Set(q, VibeFonts.SansMedium);
            wrap.Add(q);

            VisualElement opts = new VisualElement(); opts.style.marginTop = 9;
            opts.Add(ChoiceOption(m, false, "Decor",
                "A single solid mesh. Best for props, scenery and anything that just sits there.", used));
            opts.Add(ChoiceOption(m, true, "Rigid body",
                "Split into moving parts — each piece gets a Rigidbody and a Collider, so it reacts to physics.", used));
            wrap.Add(opts);

            return wrap;
        }

        // Un cartonas de optiune, in acelasi stil ca "directions" (mint): rama subtire care se
        // aprinde la hover, bifa albastra pe cel ales, restul se estompeaza dupa alegere.
        private VisualElement ChoiceOption(Msg m, bool rigid, string title, string desc, bool used)
        {
            bool picked = used && m.ChoicePick == (rigid ? 2 : 1);

            VisualElement card = new VisualElement();
            card.style.marginBottom = 8;
            card.style.backgroundColor = picked ? C("#1e1f23") : C("#1a1b1f");
            SetBorder(card, 1, picked ? new Color(Accent.r, Accent.g, Accent.b, 0.6f) : C("#2e2f34"), 14);
            SetPadding(card, 12, 15, 12, 15);
            if (used && !picked) card.style.opacity = 0.4f;

            VisualElement titleRow = Row(); titleRow.style.justifyContent = Justify.SpaceBetween; titleRow.style.alignItems = Align.Center;

            VisualElement left = Row(); left.style.alignItems = Align.Center; left.style.flexShrink = 1;
            left.Add(MakeIcon(15f, rigid ? (Action<UnityEngine.UIElements.Painter2D, float>)DrawParts : DrawCube));
            Label tl = new Label(title);
            tl.style.fontSize = 13; tl.style.marginLeft = 8; tl.style.color = picked ? TextPri : C("#9a9ca4");
            VibeFonts.Set(tl, VibeFonts.SansBold);
            tl.style.whiteSpace = WhiteSpace.Normal; tl.style.flexShrink = 1;
            left.Add(tl);
            titleRow.Add(left);

            if (picked)
            {
                VisualElement check = new VisualElement();
                check.style.width = 16; check.style.height = 16; check.style.flexShrink = 0; check.style.marginLeft = 8;
                check.style.backgroundColor = Accent; SetBorder(check, 0, Color.clear, 8);
                check.style.alignItems = Align.Center; check.style.justifyContent = Justify.Center;
                check.Add(MakeIcon(9f, DrawCheck));
                titleRow.Add(check);
            }
            card.Add(titleRow);

            Label dl = new Label(desc);
            dl.style.fontSize = 11.5f; dl.style.color = picked ? C("#8b8d95") : C("#6f7178");
            dl.style.whiteSpace = WhiteSpace.Normal; dl.style.marginTop = 5;
            VibeFonts.Set(dl, VibeFonts.SansRegular);
            card.Add(dl);

            if (!used)
            {
                card.RegisterCallback<MouseEnterEvent>(e => SetBorderColor(card, new Color(Accent.r, Accent.g, Accent.b, 0.6f)));
                card.RegisterCallback<MouseLeaveEvent>(e => SetBorderColor(card, C("#2e2f34")));
                card.RegisterCallback<ClickEvent>(e => StartGeneration(m, rigid));
            }

            return card;
        }

        // Alegerea e facuta -> blocam cartonasele si pornim generarea meshului.
        private void StartGeneration(Msg ask, bool rigid)
        {
            if (ask.ChoicePick != 0) return;      // dublu-click / rebuild: o singura data
            ask.ChoicePick = rigid ? 2 : 1;

            string genId = Guid.NewGuid().ToString("N");
            Msg genMsg = new Msg
            {
                FromUser = false, IsAi = true, Text = "Generating your 3D model...",
                ImageUrl = ask.ImageUrl, Pending = ask.Pending,
                GenCard = true, GenId = genId, GenPct = 0, GenState = 0,
                Rigid = rigid, RigidState = rigid ? 1 : 0,
            };
            genMsg.PreviewTex = ask.PreviewTex;
            _messages.Add(genMsg);
            Save();

            VibeGen3D.Commit(ask.Pending, genId, rigid);
            RebuildBody();
        }

        // Taierea in piese s-a terminat (o porneste VibeGen3D singur, dupa ce meshul e in scena).
        // Daca a picat, modelul intreg ramane in scena — pasul 1 a reusit si a fost platit.
        private void OnRigidFinished(string genId, bool ok)
        {
            Msg m = FindGen(genId);
            if (m == null || !m.Rigid || m.RigidState != 1) return;
            m.RigidState = ok ? 2 : 3;
            Save();
            RebuildBody();
        }

        // CARDUL DE GENERARE (separat de cel de preview): apare la apasarea "Generate".
        // Cat timp se lucreaza are inelul de 2px cu orbita rotitoare + bara legata de progresul
        // REAL din server. Cand modelul e in scena, devine starea finala "Added to scene".
        private VisualElement GeneratingCard(Msg m)
        {
            bool running = m.GenState == 0;
            bool failed = m.GenState == 2;

            VisualElement wrap = new VisualElement(); wrap.style.marginTop = 4;
            // Inelul de 2px in care se roteste orbita: wrapper cu padding 2 + overflow hidden,
            // disc conic dedesubt, card opac deasupra care il acopera tot in afara de rama.
            if (running)
            {
                SetPadding(wrap, 2, 2, 2, 2);            // cei 2px = latimea ramei vizibile
                SetBorder(wrap, 0, Color.clear, 16);
                wrap.style.overflow = Overflow.Hidden;   // CRUCIAL: taie discul rotitor la rama
                wrap.Add(BuildOrbit(wrap));
            }

            VisualElement card = new VisualElement();
            card.style.flexDirection = FlexDirection.Row;
            card.style.backgroundColor = C("#1e1f23");
            SetBorder(card, 0f, Color.clear, running ? 14f : 16f);
            card.style.overflow = Overflow.Hidden;

            // --- thumbnail (stanga) ---
            VisualElement thumb = new VisualElement();
            thumb.style.width = 128; thumb.style.height = 128; thumb.style.flexShrink = 0;
            thumb.style.backgroundColor = C("#26272c");
            thumb.style.alignItems = Align.Center; thumb.style.justifyContent = Justify.Center;
            Image img = new Image(); img.scaleMode = ScaleMode.ScaleToFit;
            img.style.position = Position.Absolute; img.style.left = 6; img.style.top = 6; img.style.right = 6; img.style.bottom = 6;
            thumb.Add(img);
            Label ld = new Label("…");
            ld.style.fontSize = 11; ld.style.color = C("#7a8494");
            thumb.Add(ld);
            LoadTextureInto(img, ld, m);
            card.Add(thumb);

            // --- info (dreapta) ---
            VisualElement info = new VisualElement();
            info.style.flexGrow = 1; info.style.flexShrink = 1; info.style.minWidth = 0;
            info.style.justifyContent = Justify.SpaceBetween;
            SetPadding(info, 13, 13, 12, 14);

            VisualElement titleCol = new VisualElement();
            titleCol.style.flexGrow = 1; titleCol.style.flexShrink = 1; titleCol.style.minWidth = 0;
            Label tl = new Label(PreviewTitle(m.Pending));
            tl.style.fontSize = 14; tl.style.color = TextPri; VibeFonts.Set(tl, VibeFonts.SansBold);
            tl.style.whiteSpace = WhiteSpace.NoWrap; tl.style.overflow = Overflow.Hidden; tl.style.textOverflow = TextOverflow.Ellipsis;
            titleCol.Add(tl);
            Label badge = new Label(m.Rigid ? "RIGID BODY · TRIPO_P1" : "3D MODEL · TRIPO_P1");
            badge.style.fontSize = 9; badge.style.color = C("#8b8d95"); badge.style.letterSpacing = 1f; badge.style.marginTop = 6;
            VibeFonts.Set(badge, VibeFonts.MonoMedium);
            titleCol.Add(badge);
            info.Add(titleCol);

            if (running)
            {
                // --- bara de progres (0..100 REAL) + procent + secunde scurse ---
                VisualElement bar = new VisualElement(); bar.style.marginTop = 10;

                VisualElement row = Row(); row.style.alignItems = Align.Center;
                VisualElement track = new VisualElement();
                track.style.height = 6; track.style.flexGrow = 1; track.style.backgroundColor = C("#2a2b30");
                SetBorder(track, 0, Color.clear, 999); track.style.overflow = Overflow.Hidden;
                VisualElement fill = new VisualElement();
                fill.style.position = Position.Absolute; fill.style.left = 0; fill.style.top = 0; fill.style.bottom = 0;
                fill.style.width = Length.Percent(m.GenPct); fill.style.backgroundColor = Accent;
                SetBorder(fill, 0, Color.clear, 999);
                track.Add(fill);
                row.Add(track);

                Label pctL = new Label(m.GenPct + "%");
                pctL.style.fontSize = 11.5f; pctL.style.color = C("#8b8d95"); pctL.style.marginLeft = 10; pctL.style.flexShrink = 0;
                VibeFonts.Set(pctL, VibeFonts.MonoMedium);
                row.Add(pctL);
                bar.Add(row);

                Label secs = new Label("0s");
                secs.style.fontSize = 10.5f; secs.style.color = C("#6f7178"); secs.style.marginTop = 6;
                VibeFonts.Set(secs, VibeFonts.MonoRegular);
                bar.Add(secs);
                info.Add(bar);

                // Serverul raporteaza in trepte (la ~5s). Ca sa nu sara bara, tinem procentul
                // AFISAT si il tragem lin catre cel real; secundele curg continuu.
                // Startul sta pe MESAJ, nu aici: altfel un RebuildBody ar reseta contorul la 0s.
                if (m.GenStart <= 0.0) m.GenStart = EditorApplication.timeSinceStartup;
                float shown = m.GenPct;
                track.schedule.Execute(() =>
                {
                    shown = Mathf.MoveTowards(shown, m.GenPct, 40f * 0.033f);   // ~40%/s spre tinta
                    fill.style.width = Length.Percent(shown);
                    pctL.text = Mathf.RoundToInt(shown) + "%";
                    secs.text = Mathf.FloorToInt((float)(EditorApplication.timeSinceStartup - m.GenStart)) + "s";
                }).Every(33);
            }
            else
            {
                // --- starea finala ---
                VisualElement done = Row(); done.style.alignItems = Align.Center; done.style.marginTop = 10;
                if (!failed) done.Add(MakeIcon(13f, DrawCheckGreen));
                Label dl = new Label(failed ? "Generation failed" : "Added to scene");
                dl.style.fontSize = 12; dl.style.marginLeft = failed ? 0 : 7;
                dl.style.color = failed ? C("#d98b8b") : C("#84cf9a");
                VibeFonts.Set(dl, VibeFonts.SansMedium);
                done.Add(dl);
                info.Add(done);

                // A doua linie, doar pt rigid body: starea taierii in piese.
                if (!failed && m.Rigid)
                {
                    Label rl = new Label(
                        m.RigidState == 2 ? "Split into physics parts" :
                        m.RigidState == 3 ? "Could not split into parts" :
                                            "Splitting into parts…");
                    rl.style.fontSize = 11; rl.style.marginTop = 5;
                    rl.style.color = m.RigidState == 2 ? C("#84cf9a") : m.RigidState == 3 ? C("#d98b8b") : C("#8b8d95");
                    VibeFonts.Set(rl, VibeFonts.SansRegular);
                    info.Add(rl);
                }
            }

            card.Add(info);
            wrap.Add(card);
            return wrap;
        }

        // "Orbita" = metoda din handoff: un patrat supradimensionat cu o textura
        // conica (dara albastra de ~100°) care se roteste 360° la fiecare 2.6s. Wrapperul are
        // padding 2px + overflow hidden, iar cardul opac de deasupra acopera tot discul in afara
        // de rama de 2px — deci se vede DOAR dara care alearga pe margine.
        private VisualElement BuildOrbit(VisualElement host)
        {
            VisualElement orbit = new VisualElement();
            orbit.style.position = Position.Absolute;
            orbit.pickingMode = PickingMode.Ignore;
            orbit.style.backgroundImage = new StyleBackground(OrbitTexture());
            // Unity 6: background-size implicit e "auto" (textura s-ar desena la 256px nativi).
            // O intindem explicit pe tot patratul, altfel dara nu ajunge pe margini.
            orbit.style.backgroundSize = new BackgroundSize(Length.Percent(100), Length.Percent(100));

            // Handoff-ul zice 200%x200%, dar pe un card LAT ala e un dreptunghi: rotit la 90° devine
            // mai ingust decat cardul si dara dispare pe laterale. Il facem PATRAT cu latura = diagonala,
            // centrat — acopera colturile la orice unghi si nu distorsioneaza unghiular conicul.
            host.RegisterCallback<GeometryChangedEvent>(ev =>
            {
                float w = ev.newRect.width, h = ev.newRect.height;
                if (w <= 1f || h <= 1f) return;
                float side = Mathf.Sqrt(w * w + h * h) * 1.05f;
                orbit.style.width = side; orbit.style.height = side;
                orbit.style.left = (w - side) * 0.5f; orbit.style.top = (h - side) * 0.5f;
            });

            double start = EditorApplication.timeSinceStartup;
            orbit.schedule.Execute(() =>
            {
                // 360° / 2.6s ≈ 138.5°/s, liniar, la infinit
                float ang = (float)((((EditorApplication.timeSinceStartup - start) / 2.6) % 1.0) * 360.0);
                orbit.style.rotate = new Rotate(new Angle(ang, AngleUnit.Degree));
            }).Every(16);
            return orbit;
        }

        // Textura conica a darei: transparent -> rgba(91,124,250,.25) -> #5b7cfa -> #9db4ff -> transparent,
        // pe intervalul 260°..360° (restul discului = gol). Construita o singura data, in memorie.
        private static Texture2D _orbitTex;
        private static Texture2D OrbitTexture()
        {
            if (_orbitTex != null) return _orbitTex;

            const int S = 256;
            Color soft = new Color(0.357f, 0.486f, 0.980f, 0.25f);   // #5b7cfa @ 25%
            Color full = new Color(0.357f, 0.486f, 0.980f, 1f);      // #5b7cfa
            Color peak = new Color(0.616f, 0.706f, 1.000f, 1f);      // #9db4ff (varful luminos)
            Color fade = new Color(0.616f, 0.706f, 1.000f, 0f);      // se stinge
            Color none = new Color(0.357f, 0.486f, 0.980f, 0f);

            Color32[] px = new Color32[S * S];
            float c = S / 2f;
            for (int y = 0; y < S; y++)
            {
                for (int x = 0; x < S; x++)
                {
                    // Texturile Unity sunt y-up, UI Toolkit deseneaza y-down: daca luam unghiul
                    // direct, conicul iese OGLINDIT si varful luminos ar ramane in urma in loc sa
                    // conduca cometa. (c - y) ne da unghiul in SENS ORAR asa cum apare pe ecran.
                    float ang = Mathf.Atan2(c - y, x - c) * Mathf.Rad2Deg;
                    if (ang < 0f) ang += 360f;

                    Color col;
                    if (ang < 260f) col = none;
                    else if (ang < 300f) col = Color.Lerp(none, soft, (ang - 260f) / 40f);
                    else if (ang < 340f) col = Color.Lerp(soft, full, (ang - 300f) / 40f);
                    else if (ang < 355f) col = Color.Lerp(full, peak, (ang - 340f) / 15f);
                    else col = Color.Lerp(peak, fade, (ang - 355f) / 5f);
                    px[y * S + x] = col;
                }
            }

            Texture2D tex = new Texture2D(S, S, TextureFormat.RGBA32, false);
            tex.hideFlags = HideFlags.HideAndDontSave;   // nu se salveaza ca asset, nu murdareste proiectul
            tex.filterMode = FilterMode.Bilinear;
            tex.wrapMode = TextureWrapMode.Clamp;
            tex.SetPixels32(px);
            tex.Apply();
            _orbitTex = tex;
            return tex;
        }

        // Click pe poza de concept -> overlay pe tot ecranul: poza MARE la stanga + panou
        // "DETAILS" cu promptul la dreapta (stil mint.gg). Click pe fundal sau ✕ = inchide.
        private void OpenPreviewLightbox(Msg m)
        {
            if (m == null || m.PreviewTex == null) return;
            CloseLightbox();
            VisualElement root = rootVisualElement;

            VisualElement overlay = new VisualElement();
            overlay.style.position = Position.Absolute;
            overlay.style.left = 0; overlay.style.top = 0; overlay.style.right = 0; overlay.style.bottom = 0;
            overlay.style.backgroundColor = C("#1c1c1e");
            overlay.RegisterCallback<ClickEvent>(ev => { if (ev.target == overlay) CloseLightbox(); });

            // ---- poza umple toata suprafata (object-fit: contain) ----
            Image big = new Image();
            big.image = m.PreviewTex;
            big.scaleMode = ScaleMode.ScaleToFit;
            big.style.position = Position.Absolute;
            big.style.left = 0; big.style.top = 0; big.style.right = 0; big.style.bottom = 0;
            overlay.Add(big);

            // ---- panou "DETAILS" plutitor, dreapta-sus ----
            // UI Toolkit nu are backdrop-filter: blur — aproximam cu un negru semi-transparent.
            VisualElement panel = new VisualElement();
            panel.style.position = Position.Absolute;
            panel.style.top = 14; panel.style.right = 14; panel.style.width = 220;
            panel.style.backgroundColor = new Color(0.055f, 0.055f, 0.062f, 0.62f);
            SetBorder(panel, 1, new Color(1f, 1f, 1f, 0.09f), 16);
            SetPadding(panel, 14, 14, 14, 14);

            // butonul-cerc care ramane cand panoul e colapsat (creat mai jos, referit aici)
            VisualElement pill = null;

            VisualElement head = Row();
            head.style.justifyContent = Justify.SpaceBetween; head.style.alignItems = Align.Center;
            Label title = new Label("DETAILS");
            title.style.fontSize = 10.5f; title.style.color = C("#8b8d95"); title.style.letterSpacing = 1.6f;
            VibeFonts.Set(title, VibeFonts.MonoMedium);
            head.Add(title);

            VisualElement headBtns = Row(); headBtns.style.alignItems = Align.Center;
            Button collapse = CircleIconBtn(28f, C("#2a2b30"), DrawPanel, 12f, () =>
            {
                panel.style.display = DisplayStyle.None;
                if (pill != null) pill.style.display = DisplayStyle.Flex;
            }, "Collapse");
            headBtns.Add(collapse);
            Button close = CircleIconBtn(28f, C("#2a2b30"), DrawClose, 11f, CloseLightbox, "Close");
            close.style.marginLeft = 6;
            headBtns.Add(close);
            head.Add(headBtns);
            panel.Add(head);

            // ---- randul cu miniatura + titlu / tip / data ----
            VisualElement item = Row(); item.style.alignItems = Align.Center; item.style.marginTop = 14;
            VisualElement th = new VisualElement();
            th.style.width = 52; th.style.height = 44; th.style.flexShrink = 0;
            th.style.backgroundColor = C("#232428"); SetBorder(th, 0, Color.clear, 8);
            th.style.overflow = Overflow.Hidden;
            Image thImg = new Image(); thImg.image = m.PreviewTex; thImg.scaleMode = ScaleMode.ScaleToFit;
            thImg.style.position = Position.Absolute;
            thImg.style.left = 3; thImg.style.top = 3; thImg.style.right = 3; thImg.style.bottom = 3;
            th.Add(thImg);
            item.Add(th);

            VisualElement itemCol = new VisualElement();
            itemCol.style.marginLeft = 10; itemCol.style.flexGrow = 1; itemCol.style.flexShrink = 1; itemCol.style.minWidth = 0;
            Label nm = new Label(PreviewTitle(m.Pending));
            nm.style.fontSize = 13; nm.style.color = TextPri; VibeFonts.Set(nm, VibeFonts.SansBold);
            nm.style.whiteSpace = WhiteSpace.NoWrap; nm.style.overflow = Overflow.Hidden; nm.style.textOverflow = TextOverflow.Ellipsis;
            itemCol.Add(nm);
            Label kind = new Label("Preview");
            kind.style.fontSize = 11.5f; kind.style.color = C("#8b8d95"); kind.style.marginTop = 2;
            VibeFonts.Set(kind, VibeFonts.SansRegular);
            itemCol.Add(kind);
            Label made = new Label("Created " + System.DateTime.Now.ToString("MMM d, yyyy"));
            made.style.fontSize = 10.5f; made.style.color = C("#70768a"); made.style.marginTop = 2;
            VibeFonts.Set(made, VibeFonts.SansRegular);
            itemCol.Add(made);
            item.Add(itemCol);
            panel.Add(item);

            // ---- cardul cu promptul (+ buton de copiere) ----
            string txt = (m.Pending != null && !string.IsNullOrEmpty(m.Pending.prompt)) ? m.Pending.prompt : "(no description)";
            VisualElement box = new VisualElement();
            box.style.marginTop = 14;
            box.style.backgroundColor = new Color(0.075f, 0.075f, 0.082f, 0.85f);
            SetBorder(box, 1, new Color(1f, 1f, 1f, 0.07f), 12);
            SetPadding(box, 10, 11, 11, 11);

            VisualElement boxHead = Row(); boxHead.style.alignItems = Align.Center; boxHead.style.justifyContent = Justify.SpaceBetween;
            Label pl = new Label("PROMPT");
            pl.style.fontSize = 9; pl.style.color = C("#70768a"); pl.style.letterSpacing = 1.2f;
            VibeFonts.Set(pl, VibeFonts.MonoMedium);
            boxHead.Add(pl);
            Button copy = new Button(() => { EditorGUIUtility.systemCopyBuffer = txt; }) { text = "" };
            copy.style.width = 20; copy.style.height = 20; ResetMargins(copy); copy.style.flexShrink = 0;
            copy.style.alignItems = Align.Center; copy.style.justifyContent = Justify.Center;
            copy.style.backgroundColor = Color.clear; SetBorder(copy, 0, Color.clear, 6);
            copy.tooltip = "Copy prompt";
            copy.Add(MakeIcon(13f, DrawCopy));
            boxHead.Add(copy);
            box.Add(boxHead);

            Label prompt = new Label(txt);
            prompt.style.fontSize = 11.5f; prompt.style.color = C("#c9cbd2");
            prompt.style.whiteSpace = WhiteSpace.Normal; prompt.style.marginTop = 8;
            VibeFonts.Set(prompt, VibeFonts.SansRegular);
            box.Add(prompt);
            panel.Add(box);

            overlay.Add(panel);

            // ---- varianta colapsata: doar un cerc de 32px in acelasi colt ----
            pill = CircleIconBtn(32f, C("#2a2b30"), DrawPanel, 14f, () =>
            {
                panel.style.display = DisplayStyle.Flex;
                if (pill != null) pill.style.display = DisplayStyle.None;
            }, "Show details");
            pill.style.position = Position.Absolute;
            pill.style.top = 14; pill.style.right = 14;
            pill.style.display = DisplayStyle.None;
            overlay.Add(pill);

            root.Add(overlay);
            _lightbox = overlay;
        }

        // Buton rotund cu iconita vectoriala (folosit in panoul DETAILS).
        private Button CircleIconBtn(float size, Color bg, Action<UnityEngine.UIElements.Painter2D, float> icon, float iconSize, Action onClick, string tip)
        {
            Button b = new Button(onClick) { text = "" };
            b.style.width = size; b.style.height = size; ResetMargins(b); b.style.flexShrink = 0;
            b.style.alignItems = Align.Center; b.style.justifyContent = Justify.Center;
            b.style.backgroundColor = bg; SetBorder(b, 0, Color.clear, size * 0.5f);
            b.tooltip = tip;
            b.Add(MakeIcon(iconSize, icon));
            return b;
        }

        private void CloseLightbox()
        {
            if (_lightbox != null) { _lightbox.RemoveFromHierarchy(); _lightbox = null; }
        }

        // "Make changes": userul vrea sa modifice conceptul inainte de mesh. Ii cerem sa descrie
        // schimbarea si focusam caseta de input; urmatorul mesaj re-genereaza o poza noua.
        private void MakeChanges(VibeGen3D.PendingGen p)
        {
            // Retinem CE concept se corecteaza. Fara asta, textul urmator pleca la modelul de
            // chat ca o replica oarecare — modelul raspundea cu vorbe si poza nu se regenera
            // niciodata. Acum urmatorul mesaj intra direct in fluxul de preview (vezi Send).
            _tweakFor = p;
            _tweakNote = new Msg
            {
                FromUser = false, IsAi = true, UiNote = true,
                Text = "Sure — tell me what to change (e.g. \"make it red\", \"lower the roof\") and I'll update the concept.",
            };
            _messages.Add(_tweakNote);
            Save();
            RebuildBody();
            if (_inputField != null)
            {
                _inputField.schedule.Execute(() => _inputField.Focus()).StartingIn(30);
            }
        }

        // Descarca imaginea de la URL, ii scoate fundalul verde (chroma key) si o pune in Image.
        // EditorApplication.update = asteptam terminarea request-ului pe main thread.
        private void LoadTextureInto(Image img, Label loading, Msg m)
        {
            // Deja descarcata (rebuild) -> o refolosim INSTANT, fara re-download. Asta opreste cele
            // ~25 de descarcari repetate + palpaiala care faceau cardul sa para gol.
            if (m.PreviewTex != null)
            {
                img.image = m.PreviewTex;
                if (loading != null) loading.style.display = DisplayStyle.None;
                return;
            }
            bool preCut = (m.Pending != null) && m.Pending.preCut;   // Photoroom: deja transparenta
            string blackUrl = (m.Pending != null) ? m.Pending.blackUrl : null;
            DownloadTex(m.ImageUrl, white =>
            {
                if (white == null) { if (loading != null) loading.text = "Preview unavailable"; return; }
                Debug.Log("[VibeCopilot] concept image " + white.width + "x" + white.height + (preCut ? " (pre-cut)" : ""));
                if (preCut)
                {
                    // Photoroom a scos deja fundalul (PNG transparent) -> o afisam ca atare, fara matting.
                    m.PreviewTex = white; img.image = white;
                    if (loading != null) loading.style.display = DisplayStyle.None;
                }
                else if (!string.IsNullOrEmpty(blackUrl))
                {
                    // DIFFERENCE MATTING: aceeasi poza pe NEGRU -> alpha EXACT (decupaj perfect).
                    DownloadTex(blackUrl, black =>
                    {
                        Texture2D shown = (black != null) ? VibeMatting.DifferenceMatte(white, black) : VibeMatting.Cutout(white);
                        m.PreviewTex = shown; img.image = shown;
                        if (loading != null) loading.style.display = DisplayStyle.None;
                    });
                }
                else
                {
                    // Fara pereche neagra (ex. poza userului) -> u2netp ca plasa de siguranta.
                    Texture2D shown = VibeMatting.Cutout(white);
                    m.PreviewTex = shown; img.image = shown;
                    if (loading != null) loading.style.display = DisplayStyle.None;
                }
            });
        }

        // Descarca o textura de la URL pe main thread (EditorApplication.update). onDone(null) = esec.
        private void DownloadTex(string url, System.Action<Texture2D> onDone)
        {
            UnityWebRequest req = UnityWebRequestTexture.GetTexture(url);
            UnityWebRequestAsyncOperation op = req.SendWebRequest();
            EditorApplication.CallbackFunction tick = null;
            tick = () =>
            {
                if (!op.isDone) return;
                EditorApplication.update -= tick;
                Texture2D tex = null;
                if (req.result == UnityWebRequest.Result.Success) tex = DownloadHandlerTexture.GetContent(req);
                req.Dispose();
                try { onDone(tex); } catch (System.Exception e) { Debug.LogError("[VibeCopilot] tex cb: " + e); }
            };
            EditorApplication.update += tick;
        }

        // Cartonase clickabile cu "directii" (stil mint.gg): titlu + descriere.
        // Click pe unul -> trimite promptul bogat in chat (auto-send) via QueuePrompt.
        private VisualElement DirectionsCards(Msg m)
        {
            DirectionOption[] dirs = m.Directions;
            bool used = m.DirectionsUsed;   // dupa ce ai ales una, setul e blocat (nu mai clic pe altele)
            VisualElement wrap = new VisualElement(); wrap.style.marginTop = 9;
            for (int i = 0; i < dirs.Length; i++)
            {
                DirectionOption d = dirs[i];
                if (d == null || string.IsNullOrEmpty(d.prompt)) continue;
                string sendText = d.prompt;   // captura per-iteratie pt closure
                bool picked = used && sendText == m.DirectionsPicked;

                VisualElement card = new VisualElement();
                card.style.marginBottom = 8;
                card.style.backgroundColor = picked ? C("#1e1f23") : C("#1a1b1f");
                SetBorder(card, 1, picked ? new Color(Accent.r, Accent.g, Accent.b, 0.6f) : C("#2e2f34"), 14);
                SetPadding(card, 12, 15, 12, 15);
                if (used && !picked) card.style.opacity = 0.4f;   // celelalte se estompeaza

                // Rand titlu (+ bifa in cerc pe cel ales, ca in design).
                VisualElement titleRow = Row(); titleRow.style.justifyContent = Justify.SpaceBetween; titleRow.style.alignItems = Align.Center;
                Label title = new Label(string.IsNullOrEmpty(d.title) ? ("Option " + (i + 1)) : d.title);
                title.style.fontSize = 13; title.style.color = picked ? TextPri : C("#9a9ca4");
                VibeFonts.Set(title, VibeFonts.SansBold);
                title.style.whiteSpace = WhiteSpace.Normal; title.style.flexShrink = 1;
                titleRow.Add(title);
                if (picked)
                {
                    VisualElement check = new VisualElement();
                    check.style.width = 16; check.style.height = 16; check.style.flexShrink = 0; check.style.marginLeft = 8;
                    check.style.backgroundColor = Accent; SetBorder(check, 0, Color.clear, 8);
                    check.style.alignItems = Align.Center; check.style.justifyContent = Justify.Center;
                    check.Add(MakeIcon(9f, DrawCheck));
                    titleRow.Add(check);
                }
                card.Add(titleRow);

                Label desc = new Label(d.prompt);
                desc.style.fontSize = 11.5f; desc.style.color = picked ? C("#8b8d95") : C("#6f7178");
                desc.style.whiteSpace = WhiteSpace.Normal; desc.style.marginTop = 5;
                VibeFonts.Set(desc, VibeFonts.SansRegular);
                card.Add(desc);

                if (!used)
                {
                    // Hover + click: alege directia, BLOCHEAZA setul, apoi trimite.
                    card.RegisterCallback<MouseEnterEvent>(e => SetBorderColor(card, new Color(Accent.r, Accent.g, Accent.b, 0.6f)));
                    card.RegisterCallback<MouseLeaveEvent>(e => SetBorderColor(card, C("#2e2f34")));
                    card.RegisterCallback<ClickEvent>(e =>
                    {
                        if (_thinking || m.DirectionsUsed) return;
                        m.DirectionsUsed = true; m.DirectionsPicked = sendText;
                        Save(); RebuildBody();
                        QueuePrompt(sendText);
                    });
                }

                wrap.Add(card);
            }
            return wrap;
        }

        private VisualElement CodeCard(string fileName, string code)
        {
            VisualElement card = new VisualElement(); card.style.marginTop = 6;
            card.style.backgroundColor = C("#1e1f23");
            SetBorder(card, 0, Color.clear, 14);
            card.style.overflow = Overflow.Hidden;

            VisualElement head = Row(); head.style.alignItems = Align.Center;
            head.style.backgroundColor = C("#26272c");
            SetPadding(head, 9, 14, 9, 14);
            VisualElement chk = MakeIcon(12f, DrawCheckGreen); chk.style.marginRight = 8;
            Label fn = new Label(string.IsNullOrEmpty(fileName) ? "script.cs" : fileName);
            fn.style.fontSize = 11.5f; fn.style.color = TextPri; VibeFonts.Set(fn, VibeFonts.MonoMedium);
            Label added = new Label("added"); added.style.fontSize = 11; added.style.color = C("#6f7178");
            added.style.marginLeft = StyleKeyword.Auto;
            head.Add(chk); head.Add(fn); head.Add(added);
            card.Add(head);

            ScrollView codeSv = new ScrollView(ScrollViewMode.Vertical);
            codeSv.style.maxHeight = 150;
            // fara bara de scroll vizibila (se deruleaza tot cu rotita)
            codeSv.verticalScrollerVisibility = ScrollerVisibility.Hidden;
            codeSv.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
            Label codeLbl = new Label(code);
            codeLbl.style.fontSize = 11; codeLbl.style.color = C("#c9cbd2"); codeLbl.style.whiteSpace = WhiteSpace.Normal;
            VibeFonts.Set(codeLbl, VibeFonts.MonoRegular);
            SetPadding(codeLbl, 12, 14, 12, 16);
            codeSv.Add(codeLbl);
            card.Add(codeSv);
            return card;
        }

        private VisualElement SceneChip(string text)
        {
            VisualElement chip = Row(); chip.style.alignItems = Align.Center; chip.style.marginTop = 8; chip.style.alignSelf = Align.FlexStart;
            chip.style.backgroundColor = C("#1e1f23");
            SetBorder(chip, 1, Hairline, 8);
            SetPadding(chip, 7, 11, 7, 10);
            VisualElement ch = MakeIcon(12f, DrawCheckGreen); ch.style.marginRight = 8;
            Label t = new Label(text); t.style.fontSize = 11.5f; t.style.color = TextSec;
            Label note = new Label("in scene"); note.style.fontSize = 10.5f; note.style.color = C("#6f7178"); note.style.marginLeft = 10;
            chip.Add(ch); chip.Add(t); chip.Add(note);
            return chip;
        }

        // Starea de gandire (design): logo + "VibeCopilot" sus, apoi "Thinking…" cu SHIMMER.
        // UI Toolkit n-are background-clip:text, deci facem fiecare litera un Label si-i animam culoarea.
        private VisualElement ThinkingElement()
        {
            VisualElement col = new VisualElement();

            // rand sus: logo + nume
            VisualElement head = Row(); head.style.alignItems = Align.Center;
            head.Add(MakeLogo(22f));
            Label name = new Label("VibeCopilot");
            name.style.marginLeft = 8; name.style.fontSize = 12.5f; name.style.color = C("#8b8d95");
            VibeFonts.Set(name, VibeFonts.SansBold);
            head.Add(name);
            col.Add(head);

            // "Thinking…" = UN singur cuvant (litere unite). Shimmer MOALE: text dim dedesubt +
            // mai multe copii luminoase, fiecare clipuita intr-o fereastra de latime/opacitate diferita,
            // toate centrate pe aceeasi pozitie care aluneca stanga -> dreapta -> halou lin (fara margini dure).
            const string word = "Thinking…";
            VisualElement holder = new VisualElement(); holder.style.marginTop = 10; holder.style.alignSelf = Align.FlexStart;

            Label baseL = new Label(word);
            baseL.style.fontSize = 13.5f; baseL.style.color = C("#6f7178"); baseL.style.whiteSpace = WhiteSpace.NoWrap;
            VibeFonts.Set(baseL, VibeFonts.SansMedium);
            holder.Add(baseL);

            // straturi: (latime fereastra, opacitate) — de la halou larg slab spre miez ingust intens
            float[] widths = { 58f, 38f, 22f };
            float[] alphas = { 0.28f, 0.5f, 1f };
            VisualElement[] clips = new VisualElement[widths.Length];
            for (int i = 0; i < widths.Length; i++)
            {
                VisualElement clip = new VisualElement();
                clip.style.position = Position.Absolute; clip.style.top = 0; clip.style.bottom = 0;
                clip.style.overflow = Overflow.Hidden; clip.pickingMode = PickingMode.Ignore;
                clip.style.opacity = alphas[i];
                Label brightL = new Label(word);
                brightL.style.position = Position.Absolute; brightL.style.top = 0; brightL.style.left = 0;
                brightL.style.fontSize = 13.5f; brightL.style.color = C("#eceef4"); brightL.style.whiteSpace = WhiteSpace.NoWrap;
                VibeFonts.Set(brightL, VibeFonts.SansMedium);
                clip.Add(brightL);
                holder.Add(clip);
                clips[i] = clip;
            }
            col.Add(holder);

            double start = EditorApplication.timeSinceStartup;
            holder.schedule.Execute(() =>
            {
                float W = holder.resolvedStyle.width;
                if (W <= 0f) return;
                double t = EditorApplication.timeSinceStartup - start;
                const double cycle = 1.7;                          // viteza sweep
                float phase = (float)((t % cycle) / cycle);        // 0..1
                float cx = -30f + phase * (W + 60f);               // centrul haloului aluneca peste text
                for (int i = 0; i < clips.Length; i++)
                {
                    float wI = widths[i];
                    float leftI = cx - wI * 0.5f;
                    clips[i].style.left = leftI; clips[i].style.width = wI;
                    clips[i].ElementAt(0).style.left = -leftI;      // tine textul luminos aliniat cu cel dim
                }
            }).Every(16);
            return col;
        }

        // Scrie textul din label literă cu literă (~22ms/car), fara cursor (ca in design).
        private void StartTypewriter(Label label, Msg m)
        {
            string full = m.Text ?? "";
            if (full.Length == 0) { m.Typing = false; return; }
            int[] i = { 0 };
            UnityEngine.UIElements.IVisualElementScheduledItem item = null;
            item = label.schedule.Execute(() =>
            {
                i[0] += 1;
                if (i[0] >= full.Length) { label.text = full; m.Typing = false; if (item != null) item.Pause(); return; }
                label.text = full.Substring(0, i[0]);
                if (_chatScroll != null) _chatScroll.scrollOffset = new Vector2(0, 99999);
            }).Every(22);
        }

        // ================= Butoane / helpers vizuale =================
        private static VisualElement Row()
        {
            VisualElement r = new VisualElement(); r.style.flexDirection = FlexDirection.Row; return r;
        }
        private static VisualElement Spacer(float w) { VisualElement s = new VisualElement(); s.style.width = w; return s; }

        private static Button TextButton(string text, Action onClick)
        {
            Button b = new Button(onClick) { text = text };
            b.style.marginLeft = 0; b.style.marginRight = 0; b.style.marginTop = 0; b.style.marginBottom = 0;
            return b;
        }

        private Button IconButton(VisualElement icon, Action onClick)
        {
            Button b = new Button(onClick) { text = "" };
            b.style.width = 30; b.style.height = 30;
            b.style.alignItems = Align.Center; b.style.justifyContent = Justify.Center;
            ResetMargins(b); SetBorder(b, 1, C("#2b2b2b"), 6);
            Hover(b, C("#404040"), C("#4a4a4a"));
            b.Add(icon);
            return b;
        }

        // "Fix error" — text simplu estompat (fara chenar), ca in design. Hover = albastru deschis.
        private Button FixButton(string text, Action onClick)
        {
            Button b = new Button(onClick) { text = text };
            b.style.height = 22; SetPadding(b, 0, 0, 0, 0); b.style.fontSize = 12.5f;
            b.style.color = TextMut; b.style.backgroundColor = Color.clear;
            VibeFonts.Set(b, VibeFonts.SansMedium);
            ResetMargins(b); SetBorder(b, 0, Color.clear, 0);
            b.tooltip = "Auto-fix the last C# compile error";
            b.RegisterCallback<MouseEnterEvent>(_ => b.style.color = AccentHi);
            b.RegisterCallback<MouseLeaveEvent>(_ => b.style.color = TextMut);
            return b;
        }

        // Rand discret sub header: "Found a bug?  Report it here" (link -> pagina de feedback).
        private VisualElement BuildFeedbackLine()
        {
            VisualElement row = Row();
            row.style.width = Length.Percent(100);
            row.style.justifyContent = Justify.Center; row.style.alignItems = Align.Center;
            SetPadding(row, 10, 12, 2, 12);

            Label t = new Label("Found a bug? Report it");
            t.style.fontSize = 11; t.style.color = TextFaint;
            VibeFonts.Set(t, VibeFonts.SansRegular);
            row.Add(t);

            Button here = new Button(() => Application.OpenURL("https://vibecopilot.shop/feedback.html")) { text = "here" };
            here.style.backgroundColor = Color.clear; here.style.color = AccentHi;
            here.style.fontSize = 11;
            VibeFonts.Set(here, VibeFonts.SansBold);
            ResetMargins(here); SetBorder(here, 0, Color.clear, 0); SetPadding(here, 0, 4, 0, 4);
            here.tooltip = "Send feedback or report a bug";
            here.RegisterCallback<MouseEnterEvent>(_ => here.style.color = C("#a9c0ff"));
            here.RegisterCallback<MouseLeaveEvent>(_ => here.style.color = AccentHi);
            row.Add(here);
            return row;
        }

        // Buton proeminent (full-width) pe ecranul welcome: cub 3D + text "Browse 3D library".
        // Face descoperibila biblioteca de modele gata-facute; deschide acelasi popover ca cub-ul.
        private Button LibraryButton()
        {
            Button b = new Button(() => ToggleLibraryPopover()) { text = "" };
            b.style.width = Length.Percent(100); b.style.height = 36;
            b.style.flexDirection = FlexDirection.Row;
            b.style.alignItems = Align.Center; b.style.justifyContent = Justify.Center;
            ResetMargins(b); SetBorder(b, 1, C("#3a3a3a"), 7);
            b.style.backgroundColor = C("#333333");
            b.tooltip = "Browse the 3D model library — adding a model is instant and free";

            VisualElement icon = MakeIcon(15f, DrawCube); icon.style.marginRight = 8;
            Label lbl = new Label("Browse 3D library (1000+)");
            lbl.style.fontSize = 12; lbl.style.color = C("#d0d0d0");
            lbl.style.unityFontStyleAndWeight = FontStyle.Bold; lbl.pickingMode = PickingMode.Ignore;
            b.Add(icon); b.Add(lbl);

            b.RegisterCallback<MouseEnterEvent>(_ => { b.style.backgroundColor = C("#3d3d3d"); SetBorderColor(b, Accent); });
            b.RegisterCallback<MouseLeaveEvent>(_ => { b.style.backgroundColor = C("#333333"); SetBorderColor(b, C("#3a3a3a")); });
            return b;
        }

        // Ca IconButton, dar cu eticheta text langa iconita (folosit pt cub-ul "Library" din bara).
        private Button IconTextButton(VisualElement icon, string text, Action onClick)
        {
            Button b = new Button(onClick) { text = "" };
            b.style.height = 30; b.style.flexDirection = FlexDirection.Row;
            b.style.alignItems = Align.Center; b.style.justifyContent = Justify.Center;
            SetPadding(b, 0, 9, 0, 9);
            ResetMargins(b); SetBorder(b, 1, C("#2b2b2b"), 6);
            Hover(b, C("#404040"), C("#4a4a4a"));
            icon.style.marginRight = 6;
            Label l = new Label(text); l.style.fontSize = 12; l.style.color = C("#b0b0b0"); l.pickingMode = PickingMode.Ignore;
            b.Add(icon); b.Add(l);
            return b;
        }

        // Selector de model — text simplu "Copilot Flash ▾" (fara chenar), ca in design.
        private Button ModelButton()
        {
            Button b = new Button(() => ToggleModelPopover()) { text = VibeBackendClient.Brain + "  ▾" };
            b.style.height = 22; SetPadding(b, 0, 0, 0, 0); b.style.fontSize = 12.5f; b.style.color = TextSec;
            b.style.backgroundColor = Color.clear;
            VibeFonts.Set(b, VibeFonts.SansMedium);
            ResetMargins(b); SetBorder(b, 0, Color.clear, 0);
            b.RegisterCallback<MouseEnterEvent>(_ => b.style.color = TextPri);
            b.RegisterCallback<MouseLeaveEvent>(_ => b.style.color = TextSec);
            _modelBtn = b;
            return b;
        }

        // Buton "Send" rotund (albastru) cu sageata in sus, ca in design.
        private Button SendCircle(Action onClick)
        {
            Button b = new Button(onClick) { text = "" };
            b.style.width = 32; b.style.height = 32; ResetMargins(b);
            b.style.alignItems = Align.Center; b.style.justifyContent = Justify.Center;
            b.style.backgroundColor = Accent;
            SetBorder(b, 0, Color.clear, 16);
            b.tooltip = "Send";
            b.Add(MakeIcon(14f, DrawArrowUp));
            b.RegisterCallback<MouseEnterEvent>(_ => b.style.backgroundColor = AccentHi);
            b.RegisterCallback<MouseLeaveEvent>(_ => b.style.backgroundColor = Accent);
            return b;
        }

        // Iconita plata (fara chenar/fundal): doar desenul, click-abila. Hover = fundal discret.
        private Button PlainIconButton(float size, Action<UnityEngine.UIElements.Painter2D, float> paint, Action onClick)
        {
            Button b = new Button(onClick) { text = "" };
            b.style.width = 30; b.style.height = 30; ResetMargins(b);
            b.style.alignItems = Align.Center; b.style.justifyContent = Justify.Center;
            b.style.backgroundColor = Color.clear; SetBorder(b, 0, Color.clear, 7);
            b.Add(MakeIcon(size, paint));
            b.RegisterCallback<MouseEnterEvent>(_ => b.style.backgroundColor = C("#26272d"));
            b.RegisterCallback<MouseLeaveEvent>(_ => b.style.backgroundColor = Color.clear);
            return b;
        }

        private Button Chip(string text)
        {
            Button b = new Button(() => QueuePrompt(text)) { text = text };
            b.style.flexGrow = 1; b.style.flexBasis = 0; b.style.flexShrink = 1;  // latime egala, indiferent de text
            b.style.height = 34; b.style.fontSize = 12; b.style.color = C("#c2c2c2");
            ResetMargins(b); SetBorder(b, 1, C("#2c2c2c"), 7);
            b.style.backgroundColor = C("#424242");
            b.RegisterCallback<MouseEnterEvent>(_ => { b.style.backgroundColor = C("#4a4a4a"); SetBorderColor(b, Accent); });
            b.RegisterCallback<MouseLeaveEvent>(_ => { b.style.backgroundColor = C("#424242"); SetBorderColor(b, C("#2c2c2c")); });
            return b;
        }

        private static void Hover(VisualElement ve, Color normal, Color hover)
        {
            ve.style.backgroundColor = normal;
            ve.RegisterCallback<MouseEnterEvent>(_ => ve.style.backgroundColor = hover);
            ve.RegisterCallback<MouseLeaveEvent>(_ => ve.style.backgroundColor = normal);
        }

        // ================= Popovere =================
        private void ToggleModelPopover()
        {
            if (_popover != null) { ClosePopover(); return; }
            VisualElement pop = PopoverBase();
            pop.style.width = 178;
            foreach (string opt in VibeBackendClient.BrainOptions)
            {
                string o = opt;
                pop.Add(ModelItem(o, VibeBackendClient.Brain == o,
                    () => { VibeBackendClient.Brain = o; ClosePopover(); RefreshModelButton(); }));
            }
            // Deasupra butonului "Copilot Flash", aliniat la marginea lui stanga (ca in design).
            if (_modelBtn != null)
            {
                Rect rr = rootVisualElement.worldBound;
                Rect br = _modelBtn.worldBound;
                pop.style.bottom = rr.yMax - br.yMin + 6f;
                pop.style.left = br.xMin - rr.xMin - 8f;
            }
            else { pop.style.left = 12; pop.style.bottom = 56; }
            ShowPopover(pop);
        }

        // Rand din meniul de model (design): nume; cel ales = fundal #2f3036 + bifa alba la dreapta.
        private VisualElement ModelItem(string title, bool selected, Action onClick)
        {
            Button b = new Button(onClick) { text = "" };
            b.style.flexDirection = FlexDirection.Row; b.style.alignItems = Align.Center; b.style.justifyContent = Justify.SpaceBetween;
            ResetMargins(b);
            b.style.backgroundColor = selected ? C("#2f3036") : Color.clear;
            SetBorder(b, 0, Color.clear, 8);
            SetPadding(b, 9, 12, 9, 12);
            if (!selected)
            {
                b.RegisterCallback<MouseEnterEvent>(_ => b.style.backgroundColor = C("#2a2b30"));
                b.RegisterCallback<MouseLeaveEvent>(_ => b.style.backgroundColor = Color.clear);
            }
            Label t = new Label(title); t.style.fontSize = 12.5f; t.style.color = selected ? TextPri : C("#c9cbd2");
            VibeFonts.Set(t, VibeFonts.SansMedium);
            b.Add(t);
            if (selected) b.Add(MakeIcon(12f, DrawCheckWhite));
            return b;
        }

        // Settings = MODAL centrat (design): ACCESS KEY + "Plan usage limits" cu 2 bare.
        private void ToggleUsagePopover()
        {
            if (_overlay != null) { CloseOverlay(); return; }
            VisualElement ov = OverlayRoot(0.55f);
            ov.style.alignItems = Align.Center; ov.style.justifyContent = Justify.Center;

            VisualElement modal = new VisualElement();
            modal.style.width = 400; modal.style.maxWidth = Length.Percent(93); modal.style.maxHeight = Length.Percent(88);
            modal.style.backgroundColor = C("#1a1b1f");
            SetBorder(modal, 1, C("#2a2b30"), 16);
            SetPadding(modal, 18, 16, 18, 16);

            // ---- header: SETTINGS + x ----
            VisualElement head = Row(); head.style.justifyContent = Justify.SpaceBetween; head.style.alignItems = Align.Center;
            Label hd = new Label("SETTINGS"); hd.style.fontSize = 10.5f; hd.style.color = C("#c9cbd2"); hd.style.letterSpacing = 1.6f; VibeFonts.Set(hd, VibeFonts.MonoSemiBold);
            head.Add(hd);
            Button x = new Button(() => CloseOverlay()) { text = "" };
            x.style.width = 26; x.style.height = 26; ResetMargins(x); x.style.alignItems = Align.Center; x.style.justifyContent = Justify.Center;
            x.style.backgroundColor = C("#2a2b30"); SetBorder(x, 0, Color.clear, 13);
            x.Add(MakeIcon(11f, DrawClose));
            head.Add(x);
            modal.Add(head);

            // corp cu scroll (poate depasi inaltimea pe ferestre mici)
            ScrollView body = new ScrollView(ScrollViewMode.Vertical);
            body.style.flexGrow = 1; body.style.minHeight = 0; body.style.marginTop = 14;
            body.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
            body.verticalScrollerVisibility = ScrollerVisibility.Hidden;

            // ---- ACCESS KEY ----
            Label keyLbl = new Label("ACCESS KEY"); keyLbl.style.fontSize = 9; keyLbl.style.color = C("#6f7178"); keyLbl.style.letterSpacing = 1.6f; keyLbl.style.marginBottom = 6; VibeFonts.Set(keyLbl, VibeFonts.MonoMedium);
            VisualElement keyBox = new VisualElement(); keyBox.style.height = 36; keyBox.style.justifyContent = Justify.Center;
            keyBox.style.backgroundColor = C("#232428"); SetBorder(keyBox, 1, C("#2f3036"), 10); SetPadding(keyBox, 0, 12, 0, 12);
            TextField keyField = new TextField(); keyField.value = VibeBackendClient.UserToken; keyField.isPasswordField = false; ResetMargins(keyField);
            keyField.style.backgroundColor = Color.clear; keyField.style.fontSize = 11; keyField.style.color = C("#c9cbd2"); VibeFonts.Set(keyField, VibeFonts.MonoRegular);
            SetBorder(keyField, 0, Color.clear, 0);
            var kInner = keyField.Q(className: "unity-text-field__input");
            if (kInner != null) { kInner.style.backgroundColor = Color.clear; SetBorder(kInner, 0, Color.clear, 0); ResetMargins(kInner); kInner.style.paddingLeft = 0; kInner.style.paddingRight = 0; }
            keyField.RegisterValueChangedCallback(evt => { VibeBackendClient.UserToken = evt.newValue; });
            keyField.RegisterCallback<FocusOutEvent>(_ => { VibeBackendClient.UserToken = keyField.value; });
            keyBox.Add(keyField);
            Label keyHint = new Label("Paste the key from your purchase page. Get one at vibecopilot.shop.");
            keyHint.style.fontSize = 11; keyHint.style.color = C("#6f7178"); keyHint.style.marginTop = 6; keyHint.style.whiteSpace = WhiteSpace.Normal; VibeFonts.Set(keyHint, VibeFonts.SansRegular);

            Label title = new Label("Plan usage limits"); title.style.fontSize = 14; title.style.color = TextPri; title.style.marginTop = 18; VibeFonts.Set(title, VibeFonts.SansBold);

            float left = VibeBackendClient.TokensLeft; int max = VibeBackendClient.Max;
            float pct = max > 0 ? Mathf.Clamp01((max - left) / max) : 0f;

            body.Add(keyLbl); body.Add(keyBox); body.Add(keyHint); body.Add(title);
            body.Add(UsageMeter("AI TOKENS (PRO/ULTRA)", pct,
                "Used: " + (max - left).ToString("N0") + " tokens",
                "Left: " + left.ToString("N0") + " / " + max.ToString("N0")));
            // Bara "3D GENERATIONS" a fost SCOASA: numara pe bucata (50/100), dar rezervorul
            // se consuma acum in tokeni (preview 300, mesh 2000, piese 2000), deci cifra aia
            // nu mai spunea adevarul. Soldul real e in sertar, la "Usage credits".

            modal.Add(body);
            ov.Add(modal);
            ShowOverlay(ov);
            _keyField = keyField; // salvat la inchidere (CloseOverlay)
        }

        // O "bara de usage" (design): eticheta mono + track cu fill albastru + "% used" + Used/Left.
        private VisualElement UsageMeter(string label, float pct, string usedText, string leftText)
        {
            VisualElement col = new VisualElement(); col.style.marginTop = 16;
            Label lb = new Label(label); lb.style.fontSize = 9; lb.style.color = C("#8b8d95"); lb.style.letterSpacing = 1.4f; lb.style.marginBottom = 8; VibeFonts.Set(lb, VibeFonts.MonoMedium);
            col.Add(lb);
            VisualElement track = new VisualElement(); track.style.height = 24; track.style.backgroundColor = C("#232428"); SetBorder(track, 1, C("#2f3036"), 8); track.style.overflow = Overflow.Hidden; track.style.justifyContent = Justify.Center;
            VisualElement fill = new VisualElement(); fill.style.position = Position.Absolute; fill.style.top = 0; fill.style.bottom = 0; fill.style.left = 0; fill.style.width = Length.Percent(pct * 100f); fill.style.backgroundColor = Accent;
            Label pctTxt = new Label((pct * 100f).ToString(pct < 0.1f ? "0.0" : "0") + "% used"); pctTxt.style.fontSize = 11; pctTxt.style.color = C("#c9cbd2"); pctTxt.style.unityTextAlign = TextAnchor.MiddleCenter; VibeFonts.Set(pctTxt, VibeFonts.SansMedium);
            track.Add(fill); track.Add(pctTxt);
            col.Add(track);
            Label used = new Label(usedText); used.style.fontSize = 12.5f; used.style.color = TextPri; used.style.marginTop = 10; VibeFonts.Set(used, VibeFonts.SansBold);
            Label leftL = new Label(leftText); leftL.style.fontSize = 11.5f; leftL.style.color = C("#8b8d95"); leftL.style.marginTop = 3; VibeFonts.Set(leftL, VibeFonts.SansRegular);
            col.Add(used); col.Add(leftL);
            return col;
        }

        // ---- Biblioteca de modele 3D: lista partajata de pe server, "Add" = pune in scena ----
        // Modelele vin din biblioteca comuna VibeCopilot (tot ce a generat orice user).
        // Adaugarea e INSTANT si GRATIS — nu consuma generari 3D.
        private void ToggleLibraryPopover()
        {
            if (_overlay != null) { CloseOverlay(); return; }
            VisualElement ov = OverlayRoot(0.5f);
            ov.style.alignItems = Align.Center; ov.style.justifyContent = Justify.Center;

            VisualElement modal = new VisualElement();
            modal.style.width = 390; modal.style.maxWidth = Length.Percent(92); modal.style.maxHeight = Length.Percent(80);
            modal.style.backgroundColor = C("#1a1b1f");
            SetBorder(modal, 1, C("#2a2b30"), 16);
            SetPadding(modal, 16, 16, 16, 16);

            Label title = new Label("3D model library");
            title.style.fontSize = 15; title.style.color = TextPri; VibeFonts.Set(title, VibeFonts.SansBold);
            Label sub = new Label("");
            sub.style.fontSize = 11.5f; sub.style.color = C("#8b8d95"); sub.style.marginTop = 4; sub.style.whiteSpace = WhiteSpace.Normal;
            VibeFonts.Set(sub, VibeFonts.SansRegular);

            // ---- comutator cu doua tab-uri: My Assets / Free models ----
            VisualElement tabs = Row();
            tabs.style.marginTop = 12; tabs.style.height = 40;
            tabs.style.backgroundColor = C("#202127"); SetBorder(tabs, 1, C("#2a2b30"), 20);
            SetPadding(tabs, 3, 3, 3, 3);
            Button tabMine = LibTab("My Assets", DrawBars, DrawBarsOn);
            Button tabFree = LibTab("Free models", DrawCube, DrawCubeOn);
            tabs.Add(tabMine); tabs.Add(tabFree);

            // caseta de cautare (inaltime fixa, text centrat vertical)
            VisualElement searchWrap = new VisualElement(); searchWrap.style.marginTop = 12;
            searchWrap.style.height = 38; searchWrap.style.justifyContent = Justify.Center;
            searchWrap.style.backgroundColor = C("#232428"); SetBorder(searchWrap, 1, C("#2f3036"), 10);
            SetPadding(searchWrap, 0, 12, 0, 12);
            TextField search = new TextField(); ResetMargins(search); search.style.backgroundColor = Color.clear;
            search.style.fontSize = 12.5f; search.style.color = TextPri; VibeFonts.Set(search, VibeFonts.SansRegular);
            SetBorder(search, 0, Color.clear, 0);
            var sInner = search.Q(className: "unity-text-field__input");
            if (sInner != null) { sInner.style.backgroundColor = Color.clear; SetBorder(sInner, 0, Color.clear, 0); ResetMargins(sInner); sInner.style.paddingTop = 0; sInner.style.paddingBottom = 0; sInner.style.paddingLeft = 0; sInner.style.paddingRight = 0; }
            Label searchHint = new Label("Search…");
            searchHint.style.position = Position.Absolute; searchHint.style.left = 12; searchHint.style.top = 0; searchHint.style.bottom = 0;
            searchHint.style.unityTextAlign = TextAnchor.MiddleLeft;
            searchHint.style.fontSize = 12.5f; searchHint.style.color = TextFaint; searchHint.pickingMode = PickingMode.Ignore;
            VibeFonts.Set(searchHint, VibeFonts.SansRegular);
            searchWrap.Add(search); searchWrap.Add(searchHint);

            Label status = new Label("Loading library…");
            status.style.fontSize = 11.5f; status.style.color = C("#8b8d95"); status.style.marginTop = 10; status.style.whiteSpace = WhiteSpace.Normal;
            VibeFonts.Set(status, VibeFonts.SansRegular);

            ScrollView list = new ScrollView(ScrollViewMode.Vertical);
            list.style.flexGrow = 1; list.style.minHeight = 0; list.style.marginTop = 8;
            list.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
            list.verticalScrollerVisibility = ScrollerVisibility.Hidden;

            modal.Add(title); modal.Add(sub); modal.Add(tabs); modal.Add(searchWrap); modal.Add(status); modal.Add(list);
            ov.Add(modal);
            ShowOverlay(ov);

            VibeGen3D.LibModel[] all = null;
            bool[] mineTab = { true };   // tab-ul activ (true = My Assets)
            Action refresh = () =>
            {
                searchHint.style.display = string.IsNullOrEmpty(search.value) ? DisplayStyle.Flex : DisplayStyle.None;
                StyleLibTab(tabMine, mineTab[0]);
                StyleLibTab(tabFree, !mineTab[0]);
                sub.text = mineTab[0]
                    ? "Models you generated. Adding one is instant."
                    : "Ready-made models. Adding one is instant and doesn't use your 3D generations.";
                // "My Assets" e local — se deseneaza imediat, fara sa astepte serverul
                BuildLibraryList(list, status, all ?? new VibeGen3D.LibModel[0], search.value, mineTab[0]);
                if (!mineTab[0] && all == null && status.style.display.value == DisplayStyle.Flex) status.text = "Loading library…";
            };
            tabMine.clicked += () => { mineTab[0] = true; refresh(); };
            tabFree.clicked += () => { mineTab[0] = false; refresh(); };
            search.RegisterValueChangedCallback(_ => refresh());
            refresh();
            VibeGen3D.FetchLibrary((models, reply) =>
            {
                if (_overlay != ov) return; // userul a inchis modalul intre timp
                all = models;
                if (models.Length == 0 && !string.IsNullOrEmpty(reply)) status.text = reply;
                refresh();
            });
        }

        // Modelele care stau DEJA in proiect (tot ce a generat userul + ce a descarcat din biblioteca).
        // Le citim direct de pe disc — nu atingem deloc VibeGen3D/backend-ul.
        private const string LocalModelDir = "Assets/VibeCopilot/Generated3D";
        private const string ThumbDir = "Assets/VibeCopilot/Generated3D/Thumbs";

        // Un model GENERAT de user primeste de la Commit un nume unic "<slug>_<timestamp>.glb",
        // in timp ce un model DESCARCAT din biblioteca se salveaza simplu "<slug>.glb".
        // Deci sufixul numeric = "l-am facut eu" — asa separam "My Assets" de "Free models".
        private static bool IsMineFile(string fileName)
        {
            int i = fileName.LastIndexOf('_');
            if (i <= 0 || i == fileName.Length - 1) return false;
            for (int k = i + 1; k < fileName.Length; k++)
                if (!char.IsDigit(fileName[k])) return false;
            return true;
        }

        // Numele fara sufixul de timp (pt afisare si pt gasirea miniaturii).
        private static string BaseSlug(string fileName)
        {
            if (!IsMineFile(fileName)) return fileName;
            return fileName.Substring(0, fileName.LastIndexOf('_'));
        }

        private static Texture2D ThumbFor(string fileName)
        {
            return AssetDatabase.LoadAssetAtPath<Texture2D>(ThumbDir + "/" + BaseSlug(fileName) + ".png");
        }

        // La "Generate" salvam poza de concept ca miniatura pt lista "My Assets".
        private void SaveMyThumb(Msg m)
        {
            try
            {
                if (m == null || m.Pending == null || string.IsNullOrEmpty(m.Pending.slug)) return;
                if (m.PreviewTex == null) return;
                byte[] png = m.PreviewTex.EncodeToPNG();
                if (png == null || png.Length == 0) return;
                string absDir = Path.Combine(Directory.GetCurrentDirectory(), ThumbDir);
                Directory.CreateDirectory(absDir);
                string rel = ThumbDir + "/" + m.Pending.slug + ".png";
                File.WriteAllBytes(Path.Combine(Directory.GetCurrentDirectory(), rel), png);
                AssetDatabase.ImportAsset(rel);
            }
            catch (System.Exception e) { Debug.LogWarning("[VibeCopilot] Could not save thumbnail: " + e.Message); }
        }

        // mineOnly = doar modelele generate de user; altfel doar cele descarcate din biblioteca.
        private static List<VibeGen3D.LibModel> LocalModels(bool mineOnly)
        {
            List<VibeGen3D.LibModel> outp = new List<VibeGen3D.LibModel>();
            try
            {
                string abs = Path.Combine(Directory.GetCurrentDirectory(), LocalModelDir);
                if (!Directory.Exists(abs)) return outp;
                string[] files = Directory.GetFiles(abs, "*.glb", SearchOption.TopDirectoryOnly);
                System.Array.Sort(files);
                foreach (string f in files)
                {
                    string name = Path.GetFileNameWithoutExtension(f);
                    if (IsMineFile(name) != mineOnly) continue;
                    outp.Add(new VibeGen3D.LibModel { slug = name, url = null });
                }
            }
            catch (System.Exception e) { Debug.LogWarning("[VibeCopilot] Could not read local models: " + e.Message); }
            return outp;
        }

        // Un tab din comutatorul bibliotecii: iconita (varianta stinsa + aprinsa) + text.
        private Button LibTab(string text, Action<UnityEngine.UIElements.Painter2D, float> iconOff,
                                           Action<UnityEngine.UIElements.Painter2D, float> iconOn)
        {
            Button b = new Button(() => { }) { text = "" };
            b.style.flexGrow = 1; b.style.height = 34; ResetMargins(b);
            b.style.flexDirection = FlexDirection.Row;
            b.style.alignItems = Align.Center; b.style.justifyContent = Justify.Center;
            b.style.backgroundColor = Color.clear; SetBorder(b, 0, Color.clear, 17);

            VisualElement off = MakeIcon(13f, iconOff); off.name = "ic-off";
            VisualElement on = MakeIcon(13f, iconOn); on.name = "ic-on";
            b.Add(off); b.Add(on);

            Label l = new Label(text); l.name = "lbl";
            l.style.marginLeft = 7; l.style.fontSize = 12.5f;
            VibeFonts.Set(l, VibeFonts.SansBold);
            b.Add(l);
            return b;
        }

        private static void StyleLibTab(Button b, bool active)
        {
            b.style.backgroundColor = active ? Accent : Color.clear;
            Label l = b.Q<Label>("lbl");
            if (l != null) l.style.color = active ? C("#f2f4fb") : C("#8b8d95");
            VisualElement on = b.Q("ic-on"), off = b.Q("ic-off");
            if (on != null) on.style.display = active ? DisplayStyle.Flex : DisplayStyle.None;
            if (off != null) off.style.display = active ? DisplayStyle.None : DisplayStyle.Flex;
        }

        // Lista bibliotecii pentru tab-ul activ. mineTab = modelele mele (locale), altfel cele de pe server.
        private void BuildLibraryList(ScrollView list, Label status, VibeGen3D.LibModel[] models, string filter, bool mineTab)
        {
            list.Clear();
            filter = (filter ?? "").Trim().ToLowerInvariant();

            List<VibeGen3D.LibModel> src;
            if (mineTab) src = LocalModels(true);   // doar ce am generat eu
            else
            {
                // pe "Free models" nu repetam ce am generat eu (apare deja in "My Assets")
                HashSet<string> mineSlugs = new HashSet<string>();
                foreach (VibeGen3D.LibModel own in LocalModels(true)) mineSlugs.Add(BaseSlug(own.slug));
                src = new List<VibeGen3D.LibModel>();
                foreach (VibeGen3D.LibModel m in models)
                {
                    if (m == null || string.IsNullOrEmpty(m.slug)) continue;
                    if (mineSlugs.Contains(m.slug)) continue;
                    src.Add(m);
                }
            }

            int shown = 0;
            foreach (VibeGen3D.LibModel m in src)
            {
                if (m == null || string.IsNullOrEmpty(m.slug)) continue;
                string nice = (mineTab ? BaseSlug(m.slug) : m.slug).Replace('_', ' ');
                if (filter.Length > 0 && !nice.ToLowerInvariant().Contains(filter)) continue;
                shown++;
                list.Add(LibraryRow(m, nice, mineTab));
            }

            if (shown == 0)
            {
                if (filter.Length > 0) status.text = "No model matches \"" + filter + "\".";
                else if (mineTab) status.text = "You haven't generated any 3D model yet. Everything you create shows up here.";
                else status.text = "The free library is empty for now.";
                status.style.display = DisplayStyle.Flex;
            }
            else status.style.display = DisplayStyle.None;
        }

        // Un rand din biblioteca. Pe "My Assets" are si miniatura + autorul.
        private VisualElement LibraryRow(VibeGen3D.LibModel m, string nice, bool mineTab)
        {
            VisualElement rowEl = Row(); rowEl.style.alignItems = Align.Center;
            SetPadding(rowEl, 9, 2, 9, 2);
            rowEl.style.borderBottomWidth = 1; rowEl.style.borderBottomColor = C("#232428");

            // miniatura DOAR daca avem poza de concept salvata; altfel nu punem patrat gol
            Texture2D tex = mineTab ? ThumbFor(m.slug) : null;
            if (tex != null)
            {
                VisualElement th = new VisualElement();
                th.style.width = 34; th.style.height = 34; th.style.flexShrink = 0; th.style.marginRight = 11;
                th.style.backgroundColor = C("#232428"); SetBorder(th, 0, Color.clear, 8);
                th.style.overflow = Overflow.Hidden;
                Image im = new Image(); im.image = tex; im.scaleMode = ScaleMode.ScaleAndCrop;
                im.style.position = Position.Absolute;
                im.style.left = 0; im.style.top = 0; im.style.right = 0; im.style.bottom = 0;
                th.Add(im);
                rowEl.Add(th);
            }

            VisualElement col = new VisualElement();
            col.style.flexGrow = 1; col.style.flexShrink = 1; col.style.minWidth = 0;
            Label nm = new Label(nice);
            nm.style.fontSize = 12.5f; nm.style.color = C("#c9cbd2");
            nm.style.whiteSpace = WhiteSpace.NoWrap; nm.style.overflow = Overflow.Hidden; nm.style.textOverflow = TextOverflow.Ellipsis;
            VibeFonts.Set(nm, VibeFonts.SansRegular);
            col.Add(nm);
            if (mineTab)
            {
                Label by = new Label("by @you");
                by.style.fontSize = 11; by.style.color = C("#70768a"); by.style.marginTop = 2;
                VibeFonts.Set(by, VibeFonts.SansRegular);
                col.Add(by);
            }
            rowEl.Add(col);

            VibeGen3D.LibModel captured = m;
            Button add = new Button(() => { CloseOverlay(); VibeGen3D.AddFromLibrary(captured, Vector3.zero); }) { text = "Add" };
            add.style.height = 26; SetPadding(add, 0, 14, 0, 14); add.style.fontSize = 11.5f; add.style.color = C("#0c0d12"); add.style.flexShrink = 0;
            VibeFonts.Set(add, VibeFonts.SansBold);
            ResetMargins(add); add.style.marginLeft = 10; SetBorder(add, 0, Color.clear, 8);
            add.style.backgroundColor = Accent;
            add.RegisterCallback<MouseEnterEvent>(_ => add.style.backgroundColor = AccentHi);
            add.RegisterCallback<MouseLeaveEvent>(_ => add.style.backgroundColor = Accent);
            rowEl.Add(add);
            return rowEl;
        }

        private VisualElement PopoverBase()
        {
            VisualElement pop = new VisualElement();
            pop.style.position = Position.Absolute;
            pop.style.backgroundColor = C("#232428");
            SetBorder(pop, 1, C("#2f3036"), 12);
            SetPadding(pop, 6, 6, 6, 6);
            return pop;
        }

        // ---- Overlay-uri modale (biblioteca, sidebar chaturi, settings) ----
        private void ShowOverlay(VisualElement ov)
        {
            ClosePopover(); CloseOverlay();
            _overlay = ov; rootVisualElement.Add(ov);
        }
        private void CloseOverlay()
        {
            if (_keyField != null) { VibeBackendClient.UserToken = _keyField.value; _keyField = null; }
            if (_overlay != null) { _overlay.RemoveFromHierarchy(); _overlay = null; }
        }
        // Fundal intunecat pe tot panoul; click-ul pe el inchide overlay-ul.
        private VisualElement OverlayRoot(float dim)
        {
            VisualElement ov = new VisualElement();
            ov.style.position = Position.Absolute;
            ov.style.left = 0; ov.style.top = 0; ov.style.right = 0; ov.style.bottom = 0;
            ov.style.backgroundColor = new Color(0f, 0f, 0f, dim);
            ov.RegisterCallback<ClickEvent>(ev => { if (ev.target == ov) CloseOverlay(); });
            return ov;
        }

        private void ShowPopover(VisualElement pop) { ClosePopover(); _popover = pop; rootVisualElement.Add(pop); }
        private void ClosePopover()
        {
            // Salveaza tokenul indiferent cum se inchide popover-ul (click pe roata, click in afara, etc.)
            if (_keyField != null) { VibeBackendClient.UserToken = _keyField.value; _keyField = null; }
            if (_popover != null) { _popover.RemoveFromHierarchy(); _popover = null; }
        }
        private void RefreshModelButton() { RebuildBody(); }

        // ================= Logo (hexagon + sparkle) via Painter2D =================
        private VisualElement MakeLogo(float size, bool withSparkle = true)
        {
            VisualElement logo = new VisualElement();
            logo.style.width = size; logo.style.height = size; logo.style.flexShrink = 0;
            logo.style.backgroundColor = Accent;
            float rad = size * 0.27f;
            logo.style.borderTopLeftRadius = rad; logo.style.borderTopRightRadius = rad;
            logo.style.borderBottomLeftRadius = rad; logo.style.borderBottomRightRadius = rad;
            logo.generateVisualContent += ctx =>
            {
                float w = ctx.visualElement.contentRect.width;
                if (w <= 0) return;
                float s = w / 20f;
                var p = ctx.painter2D;
                p.lineWidth = 1.4f * s; p.strokeColor = Color.white; p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
                Vector2[] hex = {
                    new Vector2(10,4.6f)*s, new Vector2(15,7.4f)*s, new Vector2(15,12.6f)*s,
                    new Vector2(10,15.4f)*s, new Vector2(5,12.6f)*s, new Vector2(5,7.4f)*s
                };
                p.BeginPath(); p.MoveTo(hex[0]);
                for (int i = 1; i < hex.Length; i++) p.LineTo(hex[i]);
                p.ClosePath(); p.Stroke();
                Vector2 ctr = new Vector2(10, 10) * s;
                p.BeginPath(); p.MoveTo(ctr); p.LineTo(new Vector2(5, 7.4f) * s);   // sus-stanga
                p.MoveTo(ctr); p.LineTo(new Vector2(15, 7.4f) * s);                  // sus-dreapta
                p.MoveTo(ctr); p.LineTo(new Vector2(10, 15.4f) * s);                 // jos (centru -> varf de jos)
                p.Stroke();

                if (withSparkle)
                {
                    DrawSparkle(p, new Vector2(15.6f, 4.4f) * s, 2.7f * s, 0.95f * s);  // steluta mare, dreapta-sus
                    DrawSparkle(p, new Vector2(13.2f, 6.6f) * s, 1.1f * s, 0.4f * s);   // steluta mica
                }
            };
            logo.MarkDirtyRepaint();
            return logo;
        }

        // Steluta cu 4 colturi (sparkle), umpluta cu alb.
        private static void DrawSparkle(UnityEngine.UIElements.Painter2D p, Vector2 c, float R, float r)
        {
            p.fillColor = Color.white;
            p.BeginPath();
            for (int k = 0; k < 8; k++)
            {
                float ang = (-90f + k * 45f) * Mathf.Deg2Rad;
                float rad = (k % 2 == 0) ? R : r;
                Vector2 pt = c + new Vector2(Mathf.Cos(ang), Mathf.Sin(ang)) * rad;
                if (k == 0) p.MoveTo(pt); else p.LineTo(pt);
            }
            p.ClosePath();
            p.Fill();
        }

        // Iconita vectoriala (clara la orice marime, spre deosebire de emoji care iese blurry).
        private VisualElement MakeIcon(float size, Action<UnityEngine.UIElements.Painter2D, float> paint)
        {
            VisualElement ic = new VisualElement();
            ic.style.width = size; ic.style.height = size; ic.style.flexShrink = 0;
            ic.generateVisualContent += ctx =>
            {
                float w = ctx.visualElement.contentRect.width;
                if (w <= 0) return;
                paint(ctx.painter2D, w);
            };
            ic.MarkDirtyRepaint();
            return ic;
        }

        // Iconita "sliders/setari" — 3 linii cu manere, doar contururi (perfect clara).
        private static void DrawSliders(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 1.7f * s; p.strokeColor = C("#9a9a9a"); p.lineCap = LineCap.Round;
            Seg(p, 4f * s, 8f * s, 20f * s, 8f * s);
            Seg(p, 4f * s, 12f * s, 20f * s, 12f * s);
            Seg(p, 4f * s, 16f * s, 20f * s, 16f * s);
            Seg(p, 15f * s, 6.2f * s, 15f * s, 9.8f * s);
            Seg(p, 9f * s, 10.2f * s, 9f * s, 13.8f * s);
            Seg(p, 16f * s, 14.2f * s, 16f * s, 17.8f * s);
        }

        private static void Seg(UnityEngine.UIElements.Painter2D p, float x1, float y1, float x2, float y2)
        {
            p.BeginPath(); p.MoveTo(new Vector2(x1, y1)); p.LineTo(new Vector2(x2, y2)); p.Stroke();
        }

        // Iconita "cub 3D" (butonul de biblioteca) — acelasi desen ca in logo, dar gri.
        private static void DrawCube(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 20f;
            p.lineWidth = 1.5f * s; p.strokeColor = C("#8f95a8"); p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
            Vector2[] hex = {
                new Vector2(10,3.4f)*s, new Vector2(16,6.9f)*s, new Vector2(16,13.1f)*s,
                new Vector2(10,16.6f)*s, new Vector2(4,13.1f)*s, new Vector2(4,6.9f)*s
            };
            p.BeginPath(); p.MoveTo(hex[0]);
            for (int i = 1; i < hex.Length; i++) p.LineTo(hex[i]);
            p.ClosePath(); p.Stroke();
            Vector2 ctr = new Vector2(10, 10.2f) * s;
            p.BeginPath();
            p.MoveTo(ctr); p.LineTo(new Vector2(4, 6.9f) * s);
            p.MoveTo(ctr); p.LineTo(new Vector2(16, 6.9f) * s);
            p.MoveTo(ctr); p.LineTo(new Vector2(10, 16.6f) * s);
            p.Stroke();
        }

        // Jetonul de langa numarul de credite: doua cercuri concentrice, verde discret.
        private static void DrawCoin(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 20f;
            Vector2 c = new Vector2(10f, 10f) * s;
            p.lineWidth = 1.6f * s; p.strokeColor = C("#84cf9a");
            p.BeginPath();
            p.Arc(c, 7f * s, new Angle(0f, AngleUnit.Degree), new Angle(360f, AngleUnit.Degree));
            p.Stroke();
            p.BeginPath();
            p.Arc(c, 2.8f * s, new Angle(0f, AngleUnit.Degree), new Angle(360f, AngleUnit.Degree));
            p.Stroke();
        }

        // Iconita "piese" (rigid body): doua blocuri desprinse unul de altul, cu un mic
        // spatiu intre ele — se citeste ca "obiect taiat in bucati", spre deosebire de cubul
        // intreg de la "Decor". La 15px orice detaliu mai fin s-ar pierde.
        private static void DrawParts(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 20f;
            p.lineWidth = 1.5f * s; p.strokeColor = C("#8f95a8"); p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
            Box(p, 2.5f * s, 2.5f * s, 10f * s, 10f * s);    // bucata mare, sus-stanga
            Box(p, 11.5f * s, 11.5f * s, 6f * s, 6f * s);    // bucata mica, jos-dreapta
        }
        private static void Box(UnityEngine.UIElements.Painter2D p, float x, float y, float w, float h)
        {
            p.BeginPath();
            p.MoveTo(new Vector2(x, y));
            p.LineTo(new Vector2(x + w, y));
            p.LineTo(new Vector2(x + w, y + h));
            p.LineTo(new Vector2(x, y + h));
            p.ClosePath(); p.Stroke();
        }

        // Iconita "poza" (buton attach) — rama + munte + soare, doar contururi.
        private static void DrawPhoto(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 1.7f * s; p.strokeColor = C("#8f95a8"); p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
            p.BeginPath();
            p.MoveTo(new Vector2(4, 5) * s); p.LineTo(new Vector2(20, 5) * s);
            p.LineTo(new Vector2(20, 19) * s); p.LineTo(new Vector2(4, 19) * s);
            p.ClosePath(); p.Stroke();
            p.BeginPath();
            p.MoveTo(new Vector2(4, 16) * s); p.LineTo(new Vector2(9.5f, 11) * s);
            p.LineTo(new Vector2(13, 14) * s); p.LineTo(new Vector2(20, 8.5f) * s);
            p.Stroke();
            p.BeginPath();
            p.MoveTo(new Vector2(8.5f, 7.5f) * s); p.LineTo(new Vector2(10, 9) * s);
            p.LineTo(new Vector2(8.5f, 10.5f) * s); p.LineTo(new Vector2(7, 9) * s);
            p.ClosePath(); p.Stroke();
        }

        // Bifa (check) — pe cerc albastru, inchisa la culoare.
        private static void DrawCheck(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 3f * s; p.strokeColor = C("#0c0d12"); p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
            p.BeginPath(); p.MoveTo(new Vector2(5, 13) * s); p.LineTo(new Vector2(10, 18) * s); p.LineTo(new Vector2(20, 7) * s); p.Stroke();
        }

        // × (inchidere modal).
        private static void DrawClose(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 2.2f * s; p.strokeColor = C("#c9cbd2"); p.lineCap = LineCap.Round;
            Seg(p, 6 * s, 6 * s, 18 * s, 18 * s);
            Seg(p, 18 * s, 6 * s, 6 * s, 18 * s);
        }

        // Bifa alba (meniul de model — optiunea aleasa).
        private static void DrawCheckWhite(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 2.6f * s; p.strokeColor = C("#eceef4"); p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
            p.BeginPath(); p.MoveTo(new Vector2(5, 13) * s); p.LineTo(new Vector2(10, 18) * s); p.LineTo(new Vector2(20, 7) * s); p.Stroke();
        }

        // Bifa verde (cod adaugat / obiect in scena).
        private static void DrawCheckGreen(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 2.6f * s; p.strokeColor = C("#84cf9a"); p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
            p.BeginPath(); p.MoveTo(new Vector2(5, 13) * s); p.LineTo(new Vector2(10, 18) * s); p.LineTo(new Vector2(20, 7) * s); p.Stroke();
        }

        // Iconita "expand" (sageti in colturi) — pt butonul de marire din cardul de preview.
        private static void DrawExpand(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 1.9f * s; p.strokeColor = C("#c9cbd2"); p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
            p.BeginPath(); p.MoveTo(new Vector2(15, 3) * s); p.LineTo(new Vector2(21, 3) * s); p.LineTo(new Vector2(21, 9) * s); p.Stroke();
            p.BeginPath(); p.MoveTo(new Vector2(9, 21) * s); p.LineTo(new Vector2(3, 21) * s); p.LineTo(new Vector2(3, 15) * s); p.Stroke();
            Seg(p, 21f * s, 3f * s, 14f * s, 10f * s);
            Seg(p, 3f * s, 21f * s, 10f * s, 14f * s);
        }

        // Iconita "copy" — doua dreptunghiuri suprapuse (butonul de copiere a promptului).
        private static void DrawCopy(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 1.8f * s; p.strokeColor = C("#8b8d95"); p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
            p.BeginPath();
            p.MoveTo(new Vector2(9, 9) * s); p.LineTo(new Vector2(20, 9) * s);
            p.LineTo(new Vector2(20, 20) * s); p.LineTo(new Vector2(9, 20) * s);
            p.ClosePath(); p.Stroke();
            p.BeginPath();
            p.MoveTo(new Vector2(15.5f, 5.5f) * s); p.LineTo(new Vector2(4, 5.5f) * s);
            p.LineTo(new Vector2(4, 16) * s);
            p.Stroke();
        }

        // Iconita "My Assets" — bare verticale de inaltimi diferite.
        private static void DrawBars(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 1.8f * s; p.strokeColor = C("#8f95a8"); p.lineCap = LineCap.Round;
            Seg(p, 5 * s, 4 * s, 5 * s, 20 * s);
            Seg(p, 10 * s, 8 * s, 10 * s, 20 * s);
            Seg(p, 15 * s, 6 * s, 15 * s, 20 * s);
            Seg(p, 20 * s, 10 * s, 20 * s, 20 * s);
        }

        // Variante ALBE ale iconitelor de tab (cand tab-ul e activ, pe fundal albastru).
        private static void DrawBarsOn(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 1.8f * s; p.strokeColor = C("#f2f4fb"); p.lineCap = LineCap.Round;
            Seg(p, 5 * s, 4 * s, 5 * s, 20 * s);
            Seg(p, 10 * s, 8 * s, 10 * s, 20 * s);
            Seg(p, 15 * s, 6 * s, 15 * s, 20 * s);
            Seg(p, 20 * s, 10 * s, 20 * s, 20 * s);
        }
        private static void DrawCubeOn(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 20f;
            p.lineWidth = 1.5f * s; p.strokeColor = C("#f2f4fb"); p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
            Vector2[] hex = {
                new Vector2(10,3.4f)*s, new Vector2(16,6.9f)*s, new Vector2(16,13.1f)*s,
                new Vector2(10,16.6f)*s, new Vector2(4,13.1f)*s, new Vector2(4,6.9f)*s
            };
            p.BeginPath(); p.MoveTo(hex[0]);
            for (int i = 1; i < hex.Length; i++) p.LineTo(hex[i]);
            p.ClosePath(); p.Stroke();
            Vector2 ctr = new Vector2(10, 10.2f) * s;
            p.BeginPath();
            p.MoveTo(ctr); p.LineTo(new Vector2(4, 6.9f) * s);
            p.MoveTo(ctr); p.LineTo(new Vector2(16, 6.9f) * s);
            p.MoveTo(ctr); p.LineTo(new Vector2(10, 16.6f) * s);
            p.Stroke();
        }

        // Iconita "Settings" — rotita (cerc + spite).
        private static void DrawGear(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 1.8f * s; p.strokeColor = C("#8f95a8"); p.lineCap = LineCap.Round;
            p.BeginPath(); p.Arc(new Vector2(12, 12) * s, 3.2f * s, 0f, 360f); p.Stroke();
            Seg(p, 12 * s, 2.5f * s, 12 * s, 5f * s); Seg(p, 12 * s, 19f * s, 12 * s, 21.5f * s);
            Seg(p, 2.5f * s, 12 * s, 5f * s, 12 * s); Seg(p, 19f * s, 12 * s, 21.5f * s, 12 * s);
            Seg(p, 5.5f * s, 5.5f * s, 7.3f * s, 7.3f * s); Seg(p, 16.7f * s, 16.7f * s, 18.5f * s, 18.5f * s);
            Seg(p, 18.5f * s, 5.5f * s, 16.7f * s, 7.3f * s); Seg(p, 7.3f * s, 16.7f * s, 5.5f * s, 18.5f * s);
        }

        // Iconita "panou/collapse" — dreptunghi cu o linie verticala.
        private static void DrawPanel(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 1.8f * s; p.strokeColor = C("#70768a"); p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
            p.BeginPath();
            p.MoveTo(new Vector2(3, 5) * s); p.LineTo(new Vector2(21, 5) * s);
            p.LineTo(new Vector2(21, 19) * s); p.LineTo(new Vector2(3, 19) * s);
            p.ClosePath(); p.Stroke();
            Seg(p, 9 * s, 5 * s, 9 * s, 19 * s);
        }

        // Sageata in sus (butonul Send) — inchisa la culoare, pe fundal albastru.
        private static void DrawArrowUp(UnityEngine.UIElements.Painter2D p, float w)
        {
            float s = w / 24f;
            p.lineWidth = 2.2f * s; p.strokeColor = C("#0c0d12"); p.lineJoin = LineJoin.Round; p.lineCap = LineCap.Round;
            Seg(p, 12f * s, 19f * s, 12f * s, 5f * s);
            p.BeginPath();
            p.MoveTo(new Vector2(6, 11) * s); p.LineTo(new Vector2(12, 5) * s); p.LineTo(new Vector2(18, 11) * s);
            p.Stroke();
        }

        // Buton 📎: userul alege o poza de pe disc -> generam 3D din ea (calea A).
        private void AttachPhoto()
        {
            if (_thinking) return;
            string path = EditorUtility.OpenFilePanel("Choose a reference photo", "", "png,jpg,jpeg");
            if (string.IsNullOrEmpty(path)) return;
            byte[] bytes;
            try { bytes = File.ReadAllBytes(path); }
            catch (Exception e) { Debug.LogWarning("[VibeCopilot] Can't read image: " + e.Message); return; }
            if (bytes == null || bytes.Length == 0) return;
            if (bytes.Length > 8 * 1024 * 1024)
            {
                _messages.Add(new Msg { FromUser = false, IsAi = true, Text = "That photo is too big (max ~8MB). Try a smaller one." });
                RebuildBody(); return;
            }
            string ext = Path.GetExtension(path).ToLowerInvariant();
            string mime = (ext == ".png") ? "image/png" : "image/jpeg";
            string b64 = Convert.ToBase64String(bytes);

            _messages.Add(new Msg { FromUser = true, Text = "📎 " + Path.GetFileName(path) });
            _thinking = true; _awaitingPreview = true;
            Save(); RebuildBody();
            VibeGen3D.GenerateFromImage(b64, mime, "Model", Vector3.zero);
        }

        // ================= Logica (neschimbata fata de versiunea veche) =================
        private void QueuePrompt(string text)
        {
            EditorApplication.delayCall += () => { _input = text; if (_inputField != null) _inputField.value = text; Send(); };
        }

        private void Send()
        {
            if (string.IsNullOrWhiteSpace(_input) || _thinking) return;
            string userText = _input;

            // Corectia e valabila DOAR daca invitatia "tell me what to change" e inca ultimul
            // lucru din chat. Daca intre timp s-a mai intamplat orice, "Make a tweak" a fost
            // abandonat -> mesajul asta e o intrebare normala, nu o corectie.
            bool isTweak = _tweakFor != null
                && _tweakNote != null
                && _messages.Count > 0
                && ReferenceEquals(_messages[_messages.Count - 1], _tweakNote);
            if (!isTweak) { _tweakFor = null; _tweakNote = null; }

            _messages.Add(new Msg { FromUser = true, Text = userText });
            _input = "";
            _thinking = true;
            RebuildBody();

            // TWEAK: s-a apasat "Make a tweak", deci textul asta e o corectie la conceptul
            // curent, nu o replica de conversatie. Il ducem direct in fluxul de preview.
            // custom:true => slug unic, altfel serverul ar da inapoi exact poza veche din cache.
            if (isTweak)
            {
                VibeGen3D.PendingGen p = _tweakFor;
                _tweakFor = null; _tweakNote = null;
                _awaitingPreview = true;   // tinem spinnerul pana vine poza noua
                Save();
                string tweaked = string.IsNullOrEmpty(p.prompt) ? userText : (p.prompt + ", " + userText);
                VibeGen3D.CurrentUserText = userText;
                VibeGen3D.Generate(tweaked, p.name, new Vector3(p.px, p.py, p.pz), true);
                return;
            }

            VibeBackendClient.SendChat(BuildHistory(), VibeSceneContext.Build(), resp =>
            {
                _thinking = false;

                // try/finally: orice eroare la aplicarea raspunsului NU mai blocheaza UI-ul.
                // (Inainte, o exceptie aici sarea peste RebuildBody -> punctele "thinking"
                //  ramaneau pe ecran la infinit. Acum animatia se opreste mereu.)
                try
                {
                    if (resp.Blocked)
                    {
                        _messages.Add(new Msg { FromUser = false, Text = "[BLOCKED] " + resp.Reply });
                    }
                    else
                    {
                        Msg ai = new Msg { FromUser = false, IsAi = true, Text = resp.Reply, RawReply = resp.RawReply, Code = resp.Code, FileName = resp.FileName, Tokens = resp.TokensUsed, Directions = resp.Directions };
                        ai.Typing = !string.IsNullOrEmpty(resp.Reply);   // raspunsul se scrie literă cu literă

                        if (!string.IsNullOrEmpty(resp.ActionsJson))
                        {
                            VibeGen3D.CurrentUserText = userText;
                            int n = VibeSceneActions.ExecuteJson(resp.ActionsJson);
                            bool isGen3d = VibeSceneActions.LastRanGenerate3D;   // semnal SIGUR (chiar a rulat generate3d)
                            if (isGen3d)
                            {
                                // Flux 3D: NU aratam textul "Got it"/chip-ul "Applied to scene".
                                // Tinem spinnerul de "thinking" pana vine poza de concept
                                // (OnPreviewReady) sau pana se rezolva altfel (OnPreviewSkipped).
                                _awaitingPreview = true;
                                _thinking = true;
                                return;   // nu adaugam mesajul "ai"
                            }
                            if (n > 0) ai.SceneNote = "Applied to scene";
                        }
                        else if (!string.IsNullOrEmpty(resp.Code))
                        {
                            WriteCodeToFile(resp.Code, resp.FileName);
                            if (VibeAutoAttach.Enabled)
                            {
                                string target = resp.AttachTarget;
                                if (string.IsNullOrEmpty(target))
                                {
                                    GameObject sel = Selection.activeGameObject;
                                    target = sel != null ? sel.name : "new";
                                }
                                VibeAutoAttach.Queue(Path.GetFileNameWithoutExtension(resp.FileName), target);
                            }
                        }
                        _messages.Add(ai);
                    }
                }
                catch (Exception e)
                {
                    Debug.LogError("[VibeCopilot] Failed to apply response: " + e);
                    _messages.Add(new Msg { FromUser = false, Text = "Hmm, something went wrong applying that. Try again." });
                }
                finally
                {
                    Save();
                    RebuildBody();
                }
            });
        }

        private void FixError()
        {
            if (_thinking) return;
            if (!VibeConsole.TryGetLastCompileError(out ConsoleError err))
            {
                _messages.Add(new Msg { FromUser = false, Text = "No compile error found in the console. If you just broke a script, wait for Unity to finish compiling and press again." });
                RebuildBody(); Save(); return;
            }
            string projectRoot = Directory.GetParent(Application.dataPath).FullName;
            string abs = string.IsNullOrEmpty(err.File) ? null : Path.Combine(projectRoot, err.File);
            if (abs == null || !File.Exists(abs))
            {
                _messages.Add(new Msg { FromUser = false, Text = "Found an error but couldn't locate the file: " + err.File });
                RebuildBody(); Save(); return;
            }
            string code = File.ReadAllText(abs);
            _messages.Add(new Msg { FromUser = false, Text = "Found an error in " + Path.GetFileName(err.File) + ". Fixing it..." });
            _thinking = true; RebuildBody();

            string prompt = "Fix this Unity C# compile error.\nError: " + err.Message + " (line " + err.Line + ").\n\n--- FILE: " + Path.GetFileName(err.File) + " ---\n" + code;
            VibeBackendClient.SendFix(prompt, resp =>
            {
                _thinking = false;
                if (!string.IsNullOrEmpty(resp.Code))
                {
                    File.WriteAllText(abs, resp.Code);
                    AssetDatabase.Refresh();
                    _messages.Add(new Msg { FromUser = false, IsAi = true, Tokens = resp.TokensUsed, Text = string.IsNullOrEmpty(resp.Reply) ? "Fixed it! Recompiling..." : resp.Reply, Code = resp.Code, FileName = Path.GetFileName(err.File) });
                }
                else
                {
                    _messages.Add(new Msg { FromUser = false, Text = string.IsNullOrEmpty(resp.Reply) ? "Couldn't fix it automatically." : resp.Reply });
                }
                Save(); RebuildBody();
            });
        }

        private VibeBackendClient.Turn[] BuildHistory()
        {
            List<VibeBackendClient.Turn> turns = new List<VibeBackendClient.Turn>();
            foreach (Msg m in _messages)
            {
                if (m.UiNote) continue;   // text de interfata, nu replica de conversatie
                if (m.FromUser) turns.Add(new VibeBackendClient.Turn { role = "user", text = m.Text });
                else if (m.IsAi) turns.Add(new VibeBackendClient.Turn { role = "model", text = string.IsNullOrEmpty(m.RawReply) ? m.Text : m.RawReply });
            }
            const int keep = 16;
            if (turns.Count > keep) turns = turns.GetRange(turns.Count - keep, keep);
            return turns.ToArray();
        }

        private string WriteCodeToFile(string code, string fileName)
        {
            string dir = "Assets/VibeCopilot/Generated";
            Directory.CreateDirectory(dir);
            string name = string.IsNullOrEmpty(fileName) ? "GeneratedScript.cs" : fileName;
            string path = Path.Combine(dir, name).Replace("\\", "/");
            File.WriteAllText(path, code);
            AssetDatabase.Refresh();
            return path;
        }

        private void ClearChat() { _messages.Clear(); _thinking = false; _tweakFor = null; _tweakNote = null; Save(); RebuildBody(); }

        // ---- Sesiuni de chat (multi) ----
        private ChatSession Active()
        {
            if (_store == null || _store.sessions == null) return null;
            foreach (var s in _store.sessions) if (s.id == _store.activeId) return s;
            return _store.sessions.Count > 0 ? _store.sessions[0] : null;
        }
        private static string NewId() { return Guid.NewGuid().ToString("N"); }
        private static long NowSec() { return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); }
        private static string TitleFrom(List<Msg> msgs)
        {
            if (msgs != null) foreach (Msg m in msgs) if (m.FromUser && !string.IsNullOrEmpty(m.Text))
            {
                string t = m.Text.Trim().Replace("\n", " ");
                return t.Length > 42 ? t.Substring(0, 42) : t;
            }
            return "New chat";
        }

        private void Save()
        {
            if (_store == null) _store = new ChatStore();
            ChatSession a = Active();
            if (a != null) { a.Messages = _messages; a.updated = NowSec(); a.title = TitleFrom(_messages); }
            EditorPrefs.SetString(ChatsKey, JsonUtility.ToJson(_store));
        }
        private void Load()
        {
            _store = null;
            string json = EditorPrefs.GetString(ChatsKey, "");
            if (!string.IsNullOrEmpty(json)) { try { _store = JsonUtility.FromJson<ChatStore>(json); } catch { } }
            if (_store == null) _store = new ChatStore();
            if (_store.sessions == null) _store.sessions = new List<ChatSession>();
            // Prima data: migram chat-ul vechi (single) intr-o sesiune.
            if (_store.sessions.Count == 0)
            {
                ChatSession s = new ChatSession { id = NewId(), title = "New chat", updated = NowSec() };
                string old = EditorPrefs.GetString(ChatKey, "");
                if (!string.IsNullOrEmpty(old))
                {
                    SaveData d = null; try { d = JsonUtility.FromJson<SaveData>(old); } catch { }
                    if (d != null && d.Messages != null && d.Messages.Count > 0) { s.Messages = d.Messages; s.title = TitleFrom(d.Messages); }
                }
                _store.sessions.Add(s);
                _store.activeId = s.id;
            }
            if (Active() == null) _store.activeId = _store.sessions[0].id;
            _messages = Active().Messages ?? new List<Msg>();
            Active().Messages = _messages;
        }

        private void NewChat()
        {
            Save();
            ChatSession s = new ChatSession { id = NewId(), title = "New chat", updated = NowSec() };
            _store.sessions.Add(s);
            _store.activeId = s.id;
            _messages = s.Messages;
            _thinking = false; _awaitingPreview = false; _tweakFor = null; _tweakNote = null;
            EditorPrefs.SetString(ChatsKey, JsonUtility.ToJson(_store));
            ClosePopover(); CloseOverlay();
            RebuildBody();
        }
        private void SwitchChat(string id)
        {
            if (id == _store.activeId) { ClosePopover(); CloseOverlay(); return; }
            Save();
            _store.activeId = id;
            ChatSession a = Active();
            _messages = a != null ? a.Messages : new List<Msg>();
            ReconcileGenCards();
            _thinking = false; _awaitingPreview = false; _tweakFor = null; _tweakNote = null;
            ClosePopover(); CloseOverlay();
            RebuildBody();
        }
        private void DeleteChat(string id)
        {
            bool wasActive = (id == _store.activeId);
            _store.sessions.RemoveAll(x => x.id == id);
            if (_store.sessions.Count == 0)
                _store.sessions.Add(new ChatSession { id = NewId(), title = "New chat", updated = NowSec() });
            if (wasActive || Active() == null)
            {
                _store.activeId = _store.sessions[_store.sessions.Count - 1].id;
                _messages = Active().Messages;
                RebuildBody();
            }
            EditorPrefs.SetString(ChatsKey, JsonUtility.ToJson(_store));
            OpenChatsPopover();   // reimprospatam lista
        }

        // Popover cu lista de chaturi + "New chat" (stil mint).
        private void ToggleChatsPopover()
        {
            if (_overlay != null) { CloseOverlay(); return; }
            OpenChatsPopover();
        }
        // Sidebar de chaturi = DRAWER stanga (design): panou 300px + fundal intunecat (click = inchide).
        private void OpenChatsPopover()
        {
            VisualElement ov = OverlayRoot(0.45f);
            ov.style.flexDirection = FlexDirection.Row;

            VisualElement panel = new VisualElement();
            panel.style.width = 300; panel.style.maxWidth = Length.Percent(84);
            panel.style.height = Length.Percent(100);
            panel.style.backgroundColor = C("#111113");
            panel.style.borderRightWidth = 1; panel.style.borderRightColor = Hairline;
            SetPadding(panel, 20, 14, 16, 14);

            // rand logo + buton inchidere
            VisualElement logoRow = Row(); logoRow.style.alignItems = Align.Center; logoRow.style.justifyContent = Justify.SpaceBetween; logoRow.style.marginBottom = 16;
            VisualElement lr = Row(); lr.style.alignItems = Align.Center;
            lr.Add(MakeLogo(26f));
            Label wm = new Label("vibecopilot"); wm.style.marginLeft = 9; wm.style.fontSize = 14; wm.style.color = TextPri; VibeFonts.Set(wm, VibeFonts.MonoSemiBold);
            lr.Add(wm); logoRow.Add(lr);
            Button collapse = new Button(() => CloseOverlay()) { text = "" };
            collapse.style.width = 28; collapse.style.height = 28; ResetMargins(collapse);
            collapse.style.backgroundColor = Color.clear; SetBorder(collapse, 0, Color.clear, 7);
            collapse.style.alignItems = Align.Center; collapse.style.justifyContent = Justify.Center;
            collapse.Add(MakeIcon(16f, DrawPanel)); collapse.tooltip = "Close";
            collapse.RegisterCallback<MouseEnterEvent>(_ => collapse.style.backgroundColor = C("#1e1f23"));
            collapse.RegisterCallback<MouseLeaveEvent>(_ => collapse.style.backgroundColor = Color.clear);
            logoRow.Add(collapse);
            panel.Add(logoRow);

            // New chat (pill accent-dim, full width)
            Button nw = new Button(() => NewChat()) { text = "＋  New chat" };
            nw.style.height = 40; ResetMargins(nw); nw.style.marginBottom = 8;
            nw.style.backgroundColor = new Color(Accent.r, Accent.g, Accent.b, 0.12f); nw.style.color = AccentHi;
            VibeFonts.Set(nw, VibeFonts.SansBold); nw.style.fontSize = 13.5f;
            SetBorder(nw, 1, new Color(Accent.r, Accent.g, Accent.b, 0.4f), 10);
            panel.Add(nw);

            // nav
            panel.Add(DrawerNavRow("My Assets", DrawBars, () => { CloseOverlay(); ToggleLibraryPopover(); }));
            panel.Add(DrawerNavRow("Settings", DrawGear, () => { CloseOverlay(); ToggleUsagePopover(); }));

            // lista de chaturi (grupata Today / Earlier)
            ScrollView sv = new ScrollView(ScrollViewMode.Vertical);
            sv.style.flexGrow = 1; sv.style.minHeight = 0; sv.style.marginTop = 6;
            sv.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
            sv.verticalScrollerVisibility = ScrollerVisibility.Hidden;
            List<ChatSession> sorted = new List<ChatSession>(_store.sessions);
            sorted.Sort((a, b) => b.updated.CompareTo(a.updated));
            bool todayHdr = false, earlierHdr = false;
            foreach (ChatSession s in sorted)
            {
                bool today = IsToday(s.updated);
                if (today && !todayHdr) { sv.Add(DrawerGroupLabel("TODAY")); todayHdr = true; }
                if (!today && !earlierHdr) { sv.Add(DrawerGroupLabel("EARLIER")); earlierHdr = true; }
                sv.Add(DrawerChatRow(s));
            }
            panel.Add(sv);

            // Cardul de jos: "Top up" pentru cine ARE deja un plan (mai cumpara credite),
            // "Upgrade" pentru cine n-are niciunul. Cumpararea se face pe DASHBOARD, unde omul
            // e logat cu contul lui: pluginul are doar cheia de acces, iar aia n-ar trebui sa
            // circule prin Stripe doar ca sa stim pe cine creditam.
            bool hasPlan = VibeBackendClient.GenTokensMax > 0;
            string upUrl = hasPlan
                ? "https://vibecopilot.shop/dashboard.html#topup"
                : "https://vibecopilot.shop/#pricing";
            Button up = new Button(() => Application.OpenURL(upUrl)) { text = "" };
            up.style.flexDirection = FlexDirection.Column; up.style.alignItems = Align.FlexStart; ResetMargins(up); up.style.marginTop = 8;
            up.style.backgroundColor = new Color(Accent.r, Accent.g, Accent.b, 0.08f);
            SetBorder(up, 1, new Color(Accent.r, Accent.g, Accent.b, 0.3f), 16); SetPadding(up, 14, 14, 14, 14);
            Label ut = new Label(hasPlan ? "Top up" : "Upgrade"); ut.style.fontSize = 13; ut.style.color = AccentHi; VibeFonts.Set(ut, VibeFonts.SansBold);
            Label us = new Label(hasPlan ? "Add more 3D credits — they never expire" : "To continue creating, upgrade");
            us.style.fontSize = 11.5f; us.style.color = C("#70768a"); us.style.marginTop = 4; us.style.whiteSpace = WhiteSpace.Normal; VibeFonts.Set(us, VibeFonts.SansRegular);
            // Contorul de credite, chiar deasupra cardului de Upgrade: eticheta la stanga,
            // cifra + jetonul la dreapta. Numarul e cel REAL din cont (se reimprospateaza la
            // fiecare raspuns al serverului), nu o estimare locala.
            VisualElement credits = Row();
            credits.style.alignItems = Align.Center; credits.style.justifyContent = Justify.SpaceBetween;
            SetPadding(credits, 0, 4, 0, 4); credits.style.marginTop = 12;

            Label cLbl = new Label("Usage credits");
            cLbl.style.fontSize = 11.5f; cLbl.style.color = C("#70768a");
            VibeFonts.Set(cLbl, VibeFonts.SansRegular);
            credits.Add(cLbl);

            VisualElement cVal = Row(); cVal.style.alignItems = Align.Center; cVal.style.flexShrink = 0;
            Label cNum = new Label("…");
            cNum.style.fontSize = 12.5f; cNum.style.color = TextPri;
            VibeFonts.Set(cNum, VibeFonts.MonoMedium);
            cVal.Add(cNum);
            VisualElement coin = MakeIcon(12f, DrawCoin); coin.style.marginLeft = 6;
            cVal.Add(coin);
            credits.Add(cVal);
            credits.tooltip = "Credits are used for 3D previews, models and physics parts";
            panel.Add(credits);

            // Soldul din EditorPrefs poate fi vechi sau inexistent (prima rulare dupa update).
            // Cat timp nu stim o cifra REALA lasam "…" — un 0 ar arata ca o defectiune, desi
            // inseamna doar "inca nu am intrebat serverul".
            Action paintCredits = () =>
            {
                int l = VibeBackendClient.GenTokensLeft, t = VibeBackendClient.GenTokensMax;
                if (t <= 0) return;
                string sl = l.ToString("N0", System.Globalization.CultureInfo.InvariantCulture);
                cNum.text = sl;
                credits.tooltip = sl + " of " + t.ToString("N0", System.Globalization.CultureInfo.InvariantCulture) + " credits left";
            };
            paintCredits();
            // Citire de fundal, gratis: aduce soldul adevarat fara sa astepte un mesaj de chat.
            VibeGen3D.FetchUsage(() => { if (cNum.panel != null) paintCredits(); });

            up.Add(ut); up.Add(us);
            panel.Add(up);

            ov.Add(panel);
            ShowOverlay(ov);
        }

        private static bool IsToday(long unixSec)
        {
            try { return DateTimeOffset.FromUnixTimeSeconds(unixSec).ToLocalTime().Date == DateTime.Now.Date; }
            catch { return false; }
        }

        private VisualElement DrawerNavRow(string text, Action<UnityEngine.UIElements.Painter2D, float> icon, Action onClick)
        {
            Button b = new Button(onClick) { text = "" };
            b.style.flexDirection = FlexDirection.Row; b.style.alignItems = Align.Center; ResetMargins(b);
            b.style.height = 36; SetPadding(b, 0, 12, 0, 10); b.style.backgroundColor = Color.clear; SetBorder(b, 0, Color.clear, 10);
            b.RegisterCallback<MouseEnterEvent>(_ => b.style.backgroundColor = C("#1e1f23"));
            b.RegisterCallback<MouseLeaveEvent>(_ => b.style.backgroundColor = Color.clear);
            VisualElement ic = MakeIcon(16f, icon); ic.style.marginRight = 10;
            Label l = new Label(text); l.style.fontSize = 13.5f; l.style.color = TextSec; VibeFonts.Set(l, VibeFonts.SansMedium); l.pickingMode = PickingMode.Ignore;
            b.Add(ic); b.Add(l);
            return b;
        }

        private VisualElement DrawerGroupLabel(string text)
        {
            Label g = new Label(text);
            g.style.fontSize = 10; g.style.color = C("#4a4e5e"); g.style.letterSpacing = 1.4f;
            g.style.marginTop = 16; g.style.marginBottom = 6; g.style.paddingLeft = 8;
            VibeFonts.Set(g, VibeFonts.MonoMedium);
            return g;
        }

        private VisualElement DrawerChatRow(ChatSession s)
        {
            string sid = s.id;
            bool active = (sid == _store.activeId);
            VisualElement rowEl = Row(); rowEl.style.alignItems = Align.Center; rowEl.style.marginBottom = 2;
            rowEl.style.backgroundColor = active ? C("#2a2b30") : Color.clear;
            SetBorder(rowEl, 0, Color.clear, 12); SetPadding(rowEl, 8, 8, 8, 10);

            Label t = new Label(string.IsNullOrEmpty(s.title) ? "New chat" : s.title);
            t.style.flexGrow = 1; t.style.flexShrink = 1; t.style.minWidth = 0; t.style.fontSize = 13; t.style.color = active ? TextPri : C("#8f95a8");
            t.style.overflow = Overflow.Hidden; t.style.textOverflow = TextOverflow.Ellipsis; t.style.whiteSpace = WhiteSpace.NoWrap;
            VibeFonts.Set(t, VibeFonts.SansRegular);
            rowEl.Add(t);

            Button del = new Button(() => DeleteChat(sid)) { text = "✕" };
            del.style.width = 22; del.style.height = 22; ResetMargins(del);
            del.style.backgroundColor = Color.clear; del.style.color = C("#70768a"); del.style.fontSize = 11;
            SetBorder(del, 0, Color.clear, 4);
            rowEl.Add(del);

            if (!active)
            {
                rowEl.RegisterCallback<MouseEnterEvent>(_ => rowEl.style.backgroundColor = C("#1a1b1f"));
                rowEl.RegisterCallback<MouseLeaveEvent>(_ => rowEl.style.backgroundColor = Color.clear);
            }
            rowEl.RegisterCallback<ClickEvent>(ev => { if (!IsInside(ev.target as VisualElement, del)) SwitchChat(sid); });
            return rowEl;
        }

        // ---- style helpers ----
        private static void SetBorder(VisualElement ve, float w, Color col, float radius)
        {
            ve.style.borderTopWidth = w; ve.style.borderBottomWidth = w; ve.style.borderLeftWidth = w; ve.style.borderRightWidth = w;
            SetBorderColor(ve, col);
            ve.style.borderTopLeftRadius = radius; ve.style.borderTopRightRadius = radius; ve.style.borderBottomLeftRadius = radius; ve.style.borderBottomRightRadius = radius;
        }
        private static void SetBorderColor(VisualElement ve, Color col)
        {
            ve.style.borderTopColor = col; ve.style.borderBottomColor = col; ve.style.borderLeftColor = col; ve.style.borderRightColor = col;
        }
        private static void SetPadding(VisualElement ve, float t, float r, float b, float l)
        {
            ve.style.paddingTop = t; ve.style.paddingRight = r; ve.style.paddingBottom = b; ve.style.paddingLeft = l;
        }
        private static void ResetMargins(VisualElement ve)
        {
            ve.style.marginTop = 0; ve.style.marginBottom = 0; ve.style.marginLeft = 0; ve.style.marginRight = 0;
        }
    }
}
