// App shell — hash router, auth gate, toast, swipe-back, MQTT/API/Push data layer
const { useState, useEffect, useRef, useCallback, createContext, useContext } = React;

/* ============================================================
   HASH ROUTER
   Routes: /auth/login | /auth/register | /auth/forgot | /auth/sent | /auth/reset | /auth/done
           /dashboard  /devices  /analytics  /alerts  /settings
           /tank/:id   /device/:id
   ============================================================ */

const parseHash = () => {
  const h = window.location.hash.replace(/^#/, '') || '/auth/login';
  const parts = h.split('/').filter(Boolean);
  return { path: '/' + parts.join('/'), parts };
};

const useRouter = () => {
  const [route, setRoute] = useState(() => parseHash());

  useEffect(() => {
    const sync = () => setRoute(parseHash());
    window.addEventListener('popstate',    sync);
    window.addEventListener('hashchange',  sync);
    return () => {
      window.removeEventListener('popstate',   sync);
      window.removeEventListener('hashchange', sync);
    };
  }, []);

  const navigate = useCallback((path, { replace = false } = {}) => {
    const target = '#' + (path.startsWith('/') ? path : '/' + path);
    if (replace) window.history.replaceState({}, '', target);
    else         window.history.pushState({}, '', target);
    setRoute(parseHash());
  }, []);

  const back = useCallback(() => window.history.back(), []);
  return { route, navigate, back };
};

/* ============================================================
   TOAST SYSTEM
   ============================================================ */

const ToastCtx = createContext(null);
const useToast = () => useContext(ToastCtx);  // global so auth.jsx can call it post-mount

const ToastProvider = ({ children }) => {
  const [toasts, setToasts] = useState([]);
  const idRef = useRef(0);

  const push = useCallback((message, opts = {}) => {
    const id = ++idRef.current;
    setToasts(prev => [...prev, { id, message, kind: opts.kind || 'info', icon: opts.icon }]);
    setTimeout(() => setToasts(prev => prev.filter(x => x.id !== id)), opts.duration || 3500);
  }, []);

  // Expose globally for auth.jsx which renders outside the ToastCtx tree during sign-up
  useEffect(() => { window.__toast = push; return () => { window.__toast = null; }; }, [push]);

  return (
    <ToastCtx.Provider value={push}>
      {children}
      <div style={{
        position: 'fixed', top: 'calc(env(safe-area-inset-top, 0px) + 12px)',
        left: 0, right: 0,
        display: 'flex', flexDirection: 'column', alignItems: 'center',
        gap: 8, zIndex: 9000, pointerEvents: 'none',
      }}>
        {toasts.map(t => (
          <div key={t.id} style={{
            pointerEvents: 'auto',
            display: 'flex', alignItems: 'center', gap: 10,
            background: 'rgba(15,20,48,0.96)',
            border: '1px solid var(--border-strong)',
            borderLeft: `3px solid ${
              t.kind === 'success' ? 'var(--green)' :
              t.kind === 'error'   ? 'var(--red)'   :
              t.kind === 'warning' ? 'var(--amber)'  : 'var(--cyan)'}`,
            color: '#fff', borderRadius: 12, padding: '10px 14px',
            boxShadow: '0 12px 32px rgba(0,0,0,0.4)',
            backdropFilter: 'blur(8px)', fontSize: 13, fontWeight: 500,
            maxWidth: 'min(92vw, 420px)',
            animation: 'toast-in 220ms ease both',
          }}>
            <Ic name={t.icon || (
              t.kind === 'success' ? 'check' :
              t.kind === 'error'   ? 'alert' :
              t.kind === 'warning' ? 'alert' : 'info'
            )} size={16} color={
              t.kind === 'success' ? 'var(--green)' :
              t.kind === 'error'   ? 'var(--red)'   :
              t.kind === 'warning' ? 'var(--amber)'  : 'var(--cyan)'
            }/>
            <span>{t.message}</span>
          </div>
        ))}
      </div>
    </ToastCtx.Provider>
  );
};

/* ============================================================
   SWIPE-BACK  (drag right from left edge to pop history)
   ============================================================ */

const SwipeBack = ({ children, onBack, enabled = true }) => {
  const [dx, setDx] = useState(0);
  const ref         = useRef(null);
  const gesture     = useRef({ start: null, dx: 0 });

  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    const onTouchStart = (e) => {
      if (!enabled) return;
      const t = e.touches[0];
      if (t.clientX > 32) return;
      gesture.current = { start: { x: t.clientX, y: t.clientY, ts: Date.now() }, dx: 0 };
    };

    const onTouchMove = (e) => {
      if (!gesture.current.start) return;
      const t   = e.touches[0];
      const ddx = t.clientX - gesture.current.start.x;
      const ddy = Math.abs(t.clientY - gesture.current.start.y);
      if (ddy > 40 && ddx < 30) { gesture.current.start = null; setDx(0); return; }
      if (ddx > 0) {
        e.preventDefault(); // block native browser back-swipe so it doesn't flash history
        const newDx = Math.min(ddx, window.innerWidth);
        gesture.current.dx = newDx;
        setDx(newDx);
      }
    };

    const onTouchEnd = () => {
      if (!gesture.current.start) return;
      const { start, dx: curDx } = gesture.current;
      const elapsed = Date.now() - start.ts;
      const speed   = curDx / Math.max(1, elapsed);
      const passed  = curDx > window.innerWidth * 0.35 || speed > 0.5;
      gesture.current = { start: null, dx: 0 };
      if (passed) {
        setDx(window.innerWidth);
        setTimeout(() => { onBack?.(); setDx(0); }, 180);
      } else {
        setDx(0);
      }
    };

    el.addEventListener('touchstart',  onTouchStart, { passive: true  });
    el.addEventListener('touchmove',   onTouchMove,  { passive: false }); // must be non-passive to preventDefault
    el.addEventListener('touchend',    onTouchEnd);
    el.addEventListener('touchcancel', onTouchEnd);
    return () => {
      el.removeEventListener('touchstart',  onTouchStart);
      el.removeEventListener('touchmove',   onTouchMove);
      el.removeEventListener('touchend',    onTouchEnd);
      el.removeEventListener('touchcancel', onTouchEnd);
    };
  }, [enabled, onBack]);

  return (
    <div ref={ref} style={{
      transform:  dx ? `translateX(${dx}px)` : '',
      transition: dx ? 'none' : 'transform 200ms ease',
      boxShadow:  dx ? '-8px 0 32px rgba(0,0,0,0.5)' : 'none',
      minHeight: '100%', background: 'transparent', willChange: 'transform',
    }}>
      {children}
    </div>
  );
};

/* ============================================================
   SUB-SCREEN  (full-screen overlay for detail / device views)
   ============================================================ */

const SubScreen = ({ children }) => {
  useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = prev; };
  }, []);

  return (
    <div style={{
      position: 'fixed', inset: 0, maxWidth: 460, margin: '0 auto',
      background:
        'radial-gradient(1000px 600px at 80% -10%, rgba(0,212,255,0.10), transparent 60%),' +
        'radial-gradient(800px 500px at -10% 110%, rgba(0,184,148,0.08), transparent 60%),' +
        'linear-gradient(180deg, var(--bg-1) 0%, var(--bg-2) 100%)',
      zIndex: 60,
      animation: 'subscreen-in 260ms ease both',
      overflow: 'auto',
    }}>
      {children}
    </div>
  );
};

/* ============================================================
   APP ROOT
   ============================================================ */

const USER_KEY             = 'velonics_user';
const DEVICE_APPEARANCE_KEY = 'velonics_device_appearance';

// ── Role-level helper (mirrors backend ROLE_LEVEL) ───────────────────────────
const _ROLE_LEVEL = { viewer: 0, operator: 1, admin: 2, owner: 3 };
function roleLevel(r) { return _ROLE_LEVEL[r] ?? -1; }

function canIssueCmd(tank, cmdKey) {
  const role = tank.myRole || 'owner';
  const lvl  = roleLevel(role);
  if (['motor', 'valve', 'mode', 'power', 'flush', 'reboot', 'poll', 'sleep', 'reset_energy'].includes(cmdKey))
    return lvl >= roleLevel('operator');
  if (['setpoints'].includes(cmdKey))
    return lvl >= roleLevel('admin');
  return true; // viewer can do read-only actions (no cmd issued for those)
}

const LIVE_TANK_FIELDS = ['level', 'volume', 'flow', 'temp', 'tds', 'voltage', 'current', 'power', 'pf', 'freq', 'energyToday', 'runtime', 'cycles', 'consumedToday', 'lastFill', 'motor', 'motorMode', 'valveIn', 'valveOut', 'signal', 'online', 'lastSeen', '_lastTelemetryMs', 'ssid', 'ip',
  'on', 'runtimeToday', 'schedules', 'timer',
  'phaseVoltageRY', 'phaseVoltageYB', 'phaseVoltageBR', 'currentR', 'currentY', 'currentB', 'phaseSequence', 'motorStatus',
  'schedulerMode', 'runtimeSet', 'runtimeCurrent', 'runtimeRemaining', 'offtimeSet', 'offtimeRemaining', 'currentCycle',
  'lastStart', 'lastStop', 'todayRunHours', 'todayCycles', 'totalRunHours', 'totalCycles',
  // OS500 Occupancy Sensor
  'occupied', 'countToday', 'duration', 'relayOn', 'relayMode', 'sensitivity',
  // Smart AC
  'mode', 'fanSpeed', 'sleepCurve', 'wakeTimer', 'presets', 'brand', 'model', 'irLearned',
];

function decorateTankState(tank) {
  return { ...tank, mqttReceived: !!tank.mqttReceived };
}

function mergeTankList(prevTanks, backendTanks) {
  const prevById = new Map(prevTanks.map(t => [String(t.id), t]));
  return backendTanks.map(rawTank => {
    const backendTank = decorateTankState(rawTank);
    const prevTank = prevById.get(String(backendTank.id));
    if (!prevTank) return backendTank;
    const merged = { ...backendTank, mqttReceived: !!prevTank.mqttReceived };
    // Always keep myRole + sharedBy from the backend response (authoritative)
    merged.myRole   = backendTank.myRole   ?? prevTank.myRole   ?? 'owner';
    merged.sharedBy = backendTank.sharedBy ?? prevTank.sharedBy ?? null;
    if (prevTank.mqttReceived) {
      LIVE_TANK_FIELDS.forEach(field => { merged[field] = prevTank[field]; });
    }
    return merged;
  });
}

const App = () => {
  const [user,         setUser]         = useState(() => {
    try { return JSON.parse(localStorage.getItem(USER_KEY) || 'null'); } catch { return null; }
  });
  const [tanks,        setTanks]        = useState(() => [
    ...AC.initialTanks.map(decorateTankState),
    ...AC.initialPlugs,
    ...(AC.initialOccupancySensors || []),
    ...(AC.initialEM || []),
    ...(AC.initialRO || []),
    ...(AC.initialAC || []),
  ]);
  const [alerts,       setAlerts]       = useState([]);
  const [flashed,      setFlashed]      = useState(new Set());
  const [mqttOnline,   setMqttOnline]   = useState(false);
  const [loaded,       setLoaded]       = useState(false);
  // pendingCmds: { [tankId]: { motor: true, valve: true, motorMode: true, setpoints: true, ... } }
  const [pendingCmds,  setPendingCmds]  = useState({});
  const { route, navigate, back } = useRouter();
  // Always-current refs so async callbacks see live state without stale closures
  const tanksRef       = useRef(tanks);
  const pendingTimers  = useRef({});
  const _colorsSaveTimer = useRef(null);
  const _orderSaveTimer  = useRef(null);
  useEffect(() => { tanksRef.current = tanks; }, [tanks]);

  const [deviceAppearance, setDeviceAppearance] = useState(() => {
    try {
      const fresh = JSON.parse(localStorage.getItem(DEVICE_APPEARANCE_KEY) || 'null');
      if (fresh && Object.keys(fresh).length > 0) return fresh;
      // Migrate from old velonics_device_colors key (string values → {color,style} objects)
      const old = JSON.parse(localStorage.getItem('velonics_device_colors') || '{}');
      const migrated = {};
      for (const [k, v] of Object.entries(old)) {
        migrated[k] = typeof v === 'string' ? { color: v, style: 'border' } : v;
      }
      if (Object.keys(migrated).length > 0) {
        try { localStorage.setItem(DEVICE_APPEARANCE_KEY, JSON.stringify(migrated)); } catch {}
      }
      return migrated;
    } catch { return {}; }
  });
  const [cloudTileOrder, setCloudTileOrder] = useState(null);

  // Signal app ready (drops splash)
  useEffect(() => {
    if (window.__appReady) window.__appReady();
  }, []);

  // Handle messages from the service worker.
  // Two channels are used so the refresh reaches the tab even if the SW hasn't
  // fully claimed it yet (BroadcastChannel works regardless of SW control state).
  useEffect(() => {
    if (!('serviceWorker' in navigator)) return;

    const refreshAlerts = () => {
      if (window.API) API.get('/api/alerts').then(d => setAlerts(d)).catch(console.warn);
    };

    // Primary path: BroadcastChannel — reliable even across SW versions
    let bc = null;
    try {
      bc = new BroadcastChannel('velonics_refresh');
      bc.onmessage = (e) => { if (e.data?.type === 'ALERT_REFRESH') refreshAlerts(); };
    } catch {}

    // Fallback + SW_NAVIGATE: direct serviceWorker message event
    const swHandler = (e) => {
      if (e.data?.type === 'SW_NAVIGATE')   navigate(e.data.hash);
      if (e.data?.type === 'ALERT_REFRESH') refreshAlerts();
    };
    navigator.serviceWorker.addEventListener('message', swHandler);

    return () => {
      if (bc) bc.close();
      navigator.serviceWorker.removeEventListener('message', swHandler);
    };
  }, [navigate]);

  // Persist user to localStorage
  useEffect(() => {
    if (user) localStorage.setItem(USER_KEY, JSON.stringify(user));
    else      localStorage.removeItem(USER_KEY);
  }, [user]);

  // Auth guard
  useEffect(() => {
    const onAuth = route.path.startsWith('/auth');
    if (!user && !onAuth) navigate('/auth/login', { replace: true });
    if ( user &&  onAuth) navigate('/dashboard',   { replace: true });
  }, [user, route.path, navigate]);

  // Skeleton: show 600ms after login to let data load
  useEffect(() => {
    if (!user) { setLoaded(false); return; }
    const t = setTimeout(() => setLoaded(true), 200);
    return () => clearTimeout(t);
  }, [user]);

  const refreshTanks = useCallback(() => {
    if (!window.API) return Promise.resolve([]);
    return API.get('/api/tanks')
      .then(backendTanks => {
        setTanks(prev => mergeTankList(prev, backendTanks));
        if (window.MQ && MQ.isConnected()) {
          const ids = backendTanks.map(t => t.deviceId).filter(Boolean);
          if (ids.length) MQ.pollDevices(ids);
        }
        return backendTanks;
      });
  }, []);

  // Load tanks from backend; backend is the sole source of truth for config/state persistence
  useEffect(() => {
    if (!user || !window.API) return;
    refreshTanks().catch(err => console.warn('[Velonics Hub] /api/tanks failed:', err.message));
  }, [user, refreshTanks]);

  // Load alerts once on login; subsequent fetches are driven by MQTT notify
  useEffect(() => {
    if (!user || !window.API) return;
    API.get('/api/alerts').then(data => setAlerts(data)).catch(() => {});
  }, [user]);

  // Sync profile fields from server on login — timezone, name, phone roam across devices.
  // Uses user.id as dep so it runs once per login session, not on every user object update.
  useEffect(() => {
    if (!user?.id || !window.API) return;
    API.get('/api/user/profile')
      .then(profile => {
        if (!profile) return;
        setUser(prev => {
          if (!prev) return prev;
          const updated = {
            ...prev,
            timezone:  profile.timezone  || prev.timezone,
            name:      profile.name      || prev.name,
            phone:     profile.phone     || prev.phone || '',
            joinedAt:  profile.createdAt || prev.joinedAt,
          };
          const changed = updated.timezone !== prev.timezone ||
                          updated.name     !== prev.name     ||
                          updated.phone    !== prev.phone    ||
                          updated.joinedAt !== prev.joinedAt;
          if (!changed) return prev;
          try { localStorage.setItem(USER_KEY, JSON.stringify(updated)); } catch {}
          return updated;
        });
      })
      .catch(() => {});
  }, [user?.id]); // eslint-disable-line react-hooks/exhaustive-deps

  // Fetch cloud preferences (tile order + device colours) on login.
  // Cloud wins: merge into localStorage so it persists for next session.
  useEffect(() => {
    if (!user?.id || !window.API) return;
    API.get('/api/user/preferences')
      .then(prefs => {
        if (prefs.deviceColors && Object.keys(prefs.deviceColors).length > 0) {
          // Normalise: old values may be plain strings from before the style upgrade
          const normalised = {};
          for (const [k, v] of Object.entries(prefs.deviceColors)) {
            normalised[k] = typeof v === 'string' ? { color: v, style: 'border' } : v;
          }
          setDeviceAppearance(prev => {
            const merged = { ...prev, ...normalised };
            try { localStorage.setItem(DEVICE_APPEARANCE_KEY, JSON.stringify(merged)); } catch {}
            return merged;
          });
        }
        if (prefs.tileOrder) {
          setCloudTileOrder(prefs.tileOrder);
          try { localStorage.setItem('velonics_group_tile_order', JSON.stringify(prefs.tileOrder)); } catch {}
        }
        if (prefs.groups && Array.isArray(prefs.groups)) {
          try { localStorage.setItem('velonics_groups', JSON.stringify(prefs.groups)); } catch {}
          window.GROUPS = prefs.groups;
        }
        if (prefs.deviceGroups && typeof prefs.deviceGroups === 'object') {
          try { localStorage.setItem('velonics_device_groups', JSON.stringify(prefs.deviceGroups)); } catch {}
        }
        if (prefs.plugUiPrefs && typeof prefs.plugUiPrefs === 'object') {
          try {
            const current = JSON.parse(localStorage.getItem('velonics_plug_data') || '{}');
            for (const [plugId, uiPrefs] of Object.entries(prefs.plugUiPrefs)) {
              current[plugId] = { ...current[plugId], ...uiPrefs };
            }
            localStorage.setItem('velonics_plug_data', JSON.stringify(current));
          } catch {}
        }
      })
      .catch(() => {});
  }, [user?.id]); // eslint-disable-line react-hooks/exhaustive-deps

  const handleUpdateUser = useCallback((updates) => {
    setUser(prev => {
      if (!prev) return prev;
      const updated = { ...prev, ...updates };
      try { localStorage.setItem(USER_KEY, JSON.stringify(updated)); } catch {}
      return updated;
    });
  }, []);

  const handleAppearanceChange = useCallback((appearance) => {
    setDeviceAppearance(appearance);
    try { localStorage.setItem(DEVICE_APPEARANCE_KEY, JSON.stringify(appearance)); } catch {}
    clearTimeout(_colorsSaveTimer.current);
    _colorsSaveTimer.current = setTimeout(() => {
      if (window.API) API.patch('/api/user/preferences', { deviceColors: appearance }).catch(() => {});
    }, 500);
  }, []);

  const handleTileOrderChange = useCallback((order) => {
    clearTimeout(_orderSaveTimer.current);
    _orderSaveTimer.current = setTimeout(() => {
      if (window.API) API.patch('/api/user/preferences', { tileOrder: order }).catch(() => {});
    }, 500);
  }, []);

  // Web Push — re-register subscription on every login
  useEffect(() => {
    if (user && window.Push) Push.setup();
  }, [user]);

  // MQTT — connect once user is authenticated, stream telemetry
  useEffect(() => {
    if (!user || !window.MQ) return;

    MQ.onConnect(() => {
      setMqttOnline(true);
      // Stamp _lastTelemetryMs for tanks already marked online so the
      // heartbeat timer will fire after 15 s if no real data arrives.
      // Without this, stale "online" from the backend lingers indefinitely.
      const now = Date.now();
      setTanks(prev => prev.map(t =>
        (t.online && t.deviceId && !t._lastTelemetryMs)
          ? { ...t, _lastTelemetryMs: now } : t
      ));
      // Poll all known devices for an immediate telemetry burst on connect
      const ids = tanksRef.current.map(t => t.deviceId).filter(Boolean);
      if (ids.length) MQ.pollDevices(ids);
    });
    MQ.onDisconnect(() => setMqttOnline(false));

    // Retained status (LWT): update online flag without waiting for next telemetry
    MQ.onStatus((deviceId, status) => {
      const online = status === 'online';
      setTanks(prev => prev.map(t =>
        t.deviceId !== deviceId ? t
          : { ...t, online, lastSeen: online ? 'now' : t.lastSeen }
      ));
    });

    MQ.onTelemetry((deviceId, data) => {
      // Find the matching tank ID via ref (no side-effect inside the updater)
      const match = tanksRef.current.find(t => t.deviceId === deviceId);
      setTanks(prev => prev.map(t => {
        if (t.deviceId !== deviceId) return t;
        return {
          ...t,
          level:         data.level    != null ? +Number(data.level).toFixed(2)   : t.level,
          volume:        data.volume   != null ? Math.round(data.volume)           : t.volume,
          flow:          data.flow     != null ? +Number(data.flow).toFixed(1)     : t.flow,
          temp:          data.temp     != null ? +Number(data.temp).toFixed(1)     : t.temp,
          tds:           data.tds      != null ? Math.round(data.tds)              : t.tds,
          voltage:       data.voltage  != null ? Math.round(data.voltage)          : t.voltage,
          current:       data.current  != null ? +Number(data.current).toFixed(2)  : t.current,
          energyToday:   data.energy   != null ? +Number(data.energy).toFixed(2)   : t.energyToday,
          power:         data.power   != null ? Math.round(data.power)             : t.power,
          pf:            data.pf      != null ? +Number(data.pf).toFixed(2)        : t.pf,
          freq:          data.freq    != null ? +Number(data.freq).toFixed(1)      : t.freq,
          runtime:       data.runtime  != null ? Math.round(data.runtime)          : t.runtime,
          cycles:        data.cycles   != null ? data.cycles                       : t.cycles,
          consumedToday: data.consumed != null ? Math.round(data.consumed)         : t.consumedToday,
          lastFill:      data.lastFill != null ? +Number(data.lastFill).toFixed(1) : t.lastFill,
          motor:         data.motor    != null ? data.motor                        : t.motor,
          valveIn:       data.valveIn  != null ? data.valveIn                      : t.valveIn,
          valveOut:      data.valveOut != null ? data.valveOut                     : t.valveOut,
          signal:        data.signal   != null ? data.signal                       : t.signal,
          ssid:          data.ssid     != null ? data.ssid                         : t.ssid,
          ip:            data.ip       != null ? data.ip                           : t.ip,
          // Agri Pump fields
          phaseVoltageRY:   data.vRY  != null ? Math.round(data.vRY)             : t.phaseVoltageRY,
          phaseVoltageYB:   data.vYB  != null ? Math.round(data.vYB)             : t.phaseVoltageYB,
          phaseVoltageBR:   data.vBR  != null ? Math.round(data.vBR)             : t.phaseVoltageBR,
          currentR:         data.iR   != null ? +Number(data.iR).toFixed(1)      : t.currentR,
          currentY:         data.iY   != null ? +Number(data.iY).toFixed(1)      : t.currentY,
          currentB:         data.iB   != null ? +Number(data.iB).toFixed(1)      : t.currentB,
          phaseSequence:    data.seq  != null ? data.seq                          : t.phaseSequence,
          motorStatus:      data.status != null ? data.status                     : t.motorStatus,
          schedulerMode:    data.sched  != null ? data.sched                      : t.schedulerMode,
          runtimeSet:       data.rtSet  != null ? data.rtSet                      : t.runtimeSet,
          runtimeCurrent:   data.rtCurr != null ? data.rtCurr                     : t.runtimeCurrent,
          runtimeRemaining: data.rtRem  != null ? data.rtRem                      : t.runtimeRemaining,
          offtimeSet:       data.otSet  != null ? data.otSet                      : t.offtimeSet,
          offtimeRemaining: data.otRem  != null ? data.otRem                      : t.offtimeRemaining,
          currentCycle:     data.cycle  != null ? data.cycle                      : t.currentCycle,
          lastStart:        data.lastStart != null ? data.lastStart               : t.lastStart,
          lastStop:         data.lastStop  != null ? data.lastStop                : t.lastStop,
          todayRunHours:    data.todayRH   != null ? +Number(data.todayRH).toFixed(1) : t.todayRunHours,
          todayCycles:      data.todayCyc  != null ? data.todayCyc               : t.todayCycles,
          totalRunHours:    data.totalRH   != null ? +Number(data.totalRH).toFixed(1) : t.totalRunHours,
          totalCycles:      data.totalCyc  != null ? data.totalCyc               : t.totalCycles,
          // Smart Plug fields
          on:           data.on          != null ? !!data.on                          : t.on,
          runtimeToday: data.runtimeToday != null ? +Number(data.runtimeToday).toFixed(1) : t.runtimeToday,
          // OS500 Occupancy Sensor fields
          occupied:    data.occupied    != null ? !!data.occupied                         : t.occupied,
          countToday:  data.countToday  != null ? data.countToday                         : t.countToday,
          duration:    data.duration    != null ? data.duration                            : t.duration,
          relayOn:     data.relayOn     != null ? !!data.relayOn                          : t.relayOn,
          relayMode:   data.relayMode   != null ? data.relayMode                          : t.relayMode,
          sensitivity: data.sensitivity != null ? data.sensitivity                         : t.sensitivity,
          // Smart RO fields
          inletTDS:      data.inletTDS       != null ? +Number(data.inletTDS).toFixed(1)      : t.inletTDS,
          outletTDS:     data.outletTDS      != null ? +Number(data.outletTDS).toFixed(1)      : t.outletTDS,
          inletFlow:     data.inletFlow      != null ? +Number(data.inletFlow).toFixed(2)      : t.inletFlow,
          outletFlow:    data.outletFlow     != null ? +Number(data.outletFlow).toFixed(2)     : t.outletFlow,
          saltRejection: data.saltRejection  != null ? +Number(data.saltRejection).toFixed(1)  : t.saltRejection,
          recoveryRate:  data.recoveryRate   != null ? +Number(data.recoveryRate).toFixed(1)   : t.recoveryRate,
          purifiedToday: data.purifiedToday  != null ? +Number(data.purifiedToday).toFixed(1)  : t.purifiedToday,
          inletVolTotal: data.inletVolTotal  != null ? +Number(data.inletVolTotal).toFixed(0)  : t.inletVolTotal,
          mqttReceived:     true,
          _lastTelemetryMs: Date.now(),
          online:   true,
          lastSeen: 'now',
        };
      }));
      if (match) setFlashed(new Set([match.id]));
    });

    // ACK handler — clears pending state and updates tank fields with confirmed values.
    // Only called after the ESP32 physically executes the command and publishes the ACK.
    MQ.onNotify(() => {
      if (window.API) API.get('/api/alerts').then(data => setAlerts(data)).catch(console.warn);
    });

    // Device sharing: re-fetch tanks when a share invite arrives or a role changes
    if (MQ.onShareNotify) {
      MQ.onShareNotify((data) => {
        if (window.API) {
          API.get('/api/tanks')
            .then(backendTanks => {
              setTanks(prev => mergeTankList(prev, backendTanks));
            })
            .catch(() => {});
        }
        const deviceName = data?.deviceName || 'A device';
        const sharedBy   = data?.sharedBy   || 'someone';
        if (data?.roleChanged) {
          window.__toast?.(`Your access to ${deviceName} was changed to ${data.role} by ${sharedBy}`, { kind: 'info', icon: 'shield' });
        } else {
          window.__toast?.(`${deviceName} was shared with you by ${sharedBy}`, { kind: 'info', icon: 'share' });
        }
      });
    }

    MQ.onAck((deviceId, data) => {
      const tank = tanksRef.current.find(t => t.deviceId === deviceId);
      if (!tank) return;
      const { cmd, ok } = data;
      const field = cmd === 'motor' ? 'motor'
                  : cmd === 'valve' ? 'valve'
                  : cmd === 'mode'  ? 'motorMode'
                  : cmd; // setpoints, poll, sleep, reboot

      // Cancel timeout and clear pending flag
      const key = `${tank.id}:${field}`;
      if (pendingTimers.current[key]) {
        clearTimeout(pendingTimers.current[key]);
        delete pendingTimers.current[key];
      }
      setPendingCmds(prev => {
        const copy = { ...prev[tank.id] };
        delete copy[field];
        return { ...prev, [tank.id]: copy };
      });

      if (!ok) {
        window.__toast?.(`Command rejected by device: ${cmd}`, { kind: 'error' });
        return;
      }

      // Update confirmed state
      if (cmd === 'power')     updateTank(tank.id, { on: data.value });
      if (cmd === 'motor')     updateTank(tank.id, { motor: data.value });
      if (cmd === 'valve')     updateTank(tank.id, { valveIn: data.inlet, valveOut: data.outlet });
      if (cmd === 'mode') {
        const mode = data.value; // 'manual' or 'auto'
        updateTank(tank.id, { motorMode: mode });
        saveTankMode(tank.id, mode).catch(() => {});
      }
      if (cmd === 'setpoints') window.__toast?.('Setpoints saved to device', { kind: 'success' });
      if (cmd === 'reboot')    window.__toast?.('Device is rebooting…', { kind: 'info' });
    });

    MQ.connect(user.id);
  }, [user]);

  // Visibility — sleep devices when backgrounded; on foreground fetch backend
  // status immediately (backend MQTT never drops) then poll for live telemetry
  useEffect(() => {
    if (!user) return;
    const onVisibility = () => {
      const ids = tanksRef.current.map(t => t.deviceId).filter(Boolean);
      if (!ids.length) return;
      if (document.hidden) {
        if (window.MQ && MQ.isConnected()) MQ.sleepDevices(ids);
      } else {
        // Fetch backend tanks immediately — backend MQTT never drops so its
        // online field reflects true device state within ~200ms.
        // Also reset _lastTelemetryMs for online devices so the heartbeat
        // timer (marks offline after 15s of no telemetry) doesn't fire
        // while MQTT is still reconnecting from the background suspension.
        API.get('/api/tanks')
          .then(backendTanks => {
            const now = Date.now();
            const onlineIds = new Set(
              backendTanks.filter(t => t.online).map(t => String(t.id))
            );
            setTanks(prev => {
              const merged = mergeTankList(prev, backendTanks);
              return merged.map(t =>
                onlineIds.has(String(t.id))
                  ? { ...t, online: true, _lastTelemetryMs: now }
                  : t
              );
            });
          })
          .catch(() => {});
        // Refresh alerts on foreground so any alerts from the background period appear immediately
        API.get('/api/alerts').then(data => setAlerts(data)).catch(() => {});
        // Also poll via MQTT if already connected; onConnect handler covers reconnect case
        if (window.MQ && MQ.isConnected()) MQ.pollDevices(ids);
      }
    };
    document.addEventListener('visibilitychange', onVisibility);
    return () => document.removeEventListener('visibilitychange', onVisibility);
  }, [user]);

  // Heartbeat timeout: mark a device offline if no telemetry for 15 s.
  // The ESP32 publishes every 5 s, so three missed packets → offline.
  // This catches hard power-off faster than the broker LWT (~90 s delay).
  useEffect(() => {
    if (!user) return;
    const id = setInterval(() => {
      const now = Date.now();
      setTanks(prev => prev.map(t => {
        if (!t._lastTelemetryMs || !t.online) return t;
        return now - t._lastTelemetryMs > 15000 ? { ...t, online: false } : t;
      }));
    }, 10000);
    return () => clearInterval(id);
  }, [user]);

  // Clear flash glow after 1s
  useEffect(() => {
    if (flashed.size === 0) return;
    const t = setTimeout(() => setFlashed(new Set()), 1000);
    return () => clearTimeout(t);
  }, [flashed]);

  const updateTank = useCallback((id, patch) =>
    setTanks(prev => prev.map(t => t.id === id ? { ...t, ...patch } : t)), []);

  const refreshAlerts = useCallback(() => {
    if (!user || !window.API) return;
    API.get('/api/alerts').then(data => setAlerts(data)).catch(() => {});
  }, [user]);

  // Persists motorMode to the backend. Called from onAck after device confirms mode change.
  const saveTankMode = useCallback((id, motorMode) => {
    const nextMode = motorMode === 'manual' ? 'manual' : 'auto';
    if (!window.API) return Promise.resolve();
    return API.put(`/api/tanks/${id}/mode`, { motorMode: nextMode });
  }, []);

  // Issue a command with ACK tracking.
  // Sets pendingCmds[tankId][field] = true, fires publishFn(), starts 5s timeout.
  // Cleared by onAck or by timeout. UI shows PendingDots while pending.
  const issueCmd = useCallback((tankId, field, publishFn) => {
    // Role-gate: check caller's role before dispatching
    const tank = tanksRef.current.find(t => t.id === tankId || String(t.id) === String(tankId));
    if (tank && !canIssueCmd(tank, field)) {
      window.__toast?.(`Your ${tank.myRole || 'viewer'} role cannot perform this action`, { kind: 'error' });
      return;
    }

    const key = `${tankId}:${field}`;
    if (pendingTimers.current[key]) clearTimeout(pendingTimers.current[key]);

    setPendingCmds(prev => ({
      ...prev,
      [tankId]: { ...prev[tankId], [field]: true },
    }));

    const label = {
      motor: 'Motor command', valve: 'Valve command',
      motorMode: 'Mode command', setpoints: 'Setpoints',
      reboot: 'Reboot', poll: 'Poll', sleep: 'Sleep',
    }[field] || field;

    pendingTimers.current[key] = setTimeout(() => {
      delete pendingTimers.current[key];
      setPendingCmds(prev => {
        const copy = { ...prev[tankId] };
        delete copy[field];
        return { ...prev, [tankId]: copy };
      });
      window.__toast?.(`No response from device — ${label} may not have applied`, { kind: 'warning' });
    }, 5000);

    publishFn();
  }, []);

  const handleLogin = useCallback((u) => {
    setUser(u);
  }, []);

  const handleLogout = useCallback(() => {
    setUser(null);
    setTanks([]);
    setAlerts([]);
    setMqttOnline(false);
    if (window.API) localStorage.removeItem(window.API.TOKEN_KEY);
    if (window.MQ && MQ.disconnect) MQ.disconnect();
    navigate('/auth/login', { replace: true });
  }, [navigate]);

  const unackCount    = alerts.filter(a => !a.ack).length;
  const criticalUnack = alerts.filter(a => !a.ack && a.sev === 'critical').length;

  // ── Route rendering ───────────────────────────────────────────────────────
  const renderContent = () => {
    const p = route.path;

    // ── Auth routes ──
    if (p.startsWith('/auth')) {
      if (p === '/auth/register') return <AuthRegister onSignUp={handleLogin} navigate={navigate}/>;
      if (p === '/auth/forgot')   return <AuthForgot navigate={navigate}/>;
      if (p === '/auth/sent')     return <AuthForgotSent navigate={navigate}/>;
      if (p === '/auth/reset')    return <AuthReset navigate={navigate}/>;
      if (p === '/auth/done')     return <AuthResetDone navigate={navigate}/>;
      return <AuthLogin onLogIn={handleLogin} navigate={navigate}/>;
    }

    if (!user) return null; // auth guard will redirect

    // Determine which bottom tab is active
    const tabFromPath =
      p.startsWith('/dashboard') ? 'dashboard' :
      p.startsWith('/devices')   ? 'devices'   :
      p.startsWith('/analytics') ? 'analytics' :
      p.startsWith('/alerts')    ? 'alerts'    :
      p.startsWith('/settings')  ? 'settings'  :
      p.startsWith('/tank')      ? 'dashboard' :
      p.startsWith('/device')    ? 'devices'   :
      'dashboard';

    const dashboardContent = (
      <Dashboard
        tanks={tanks} setTanks={setTanks}
        openDetail={id => navigate(`/tank/${id}`)}
        setActiveTab={t => navigate(`/${t}`, { replace: true })}
        flashed={flashed} alertCount={criticalUnack} user={user}
        issueCmd={issueCmd} pendingCmds={pendingCmds}
        deviceAppearance={deviceAppearance} onAppearanceChange={handleAppearanceChange}
        onTileOrderChange={handleTileOrderChange} cloudTileOrder={cloudTileOrder}/>
    );

    // ── Tank detail overlay ──
    if (p.startsWith('/tank/')) {
      const rawId = route.parts[1];
      const tank  = tanks.find(t => String(t.id) === rawId || t.id === +rawId);
      if (!tank) { navigate('/dashboard', { replace: true }); return null; }
      const DetailComponent = tank.type === 'Smart Plug'        ? PlugDetail
                           : tank.type === 'Smart RO'         ? RODetail
                           : tank.type === 'Agri Pump'        ? AgriDetail
                           : tank.type === 'Energy Meter'     ? EmDetail
                           : tank.type === 'Occupancy Sensor' ? OsDetail
                           : tank.type === 'Smart AC'         ? AcDetail
                           : Detail;
      const backToDash = () => navigate('/dashboard', { replace: true });
      return (
        <>
          <MainShell tab="dashboard" navigate={navigate} alertBadge={unackCount}
              user={user} mqttOnline={mqttOnline} tanks={tanks}>
              {dashboardContent}
            </MainShell>
          <SubScreen>
            <SwipeBack onBack={backToDash}>
              <DetailComponent tank={tank} onBack={backToDash} updateTank={updateTank}
                      issueCmd={issueCmd} pendingCmds={pendingCmds}/>
            </SwipeBack>
          </SubScreen>
        </>
      );
    }

    // ── Device info — full page in MainShell so header+footer are always visible ──
    if (p.startsWith('/device/')) {
      const rawId = route.parts[1];
      const tank  = tanks.find(t => String(t.id) === rawId || t.id === +rawId);
      if (!tank) { navigate('/devices', { replace: true }); return null; }
      const backToDevices = () => navigate('/devices', { replace: true });
      return (
        <MainShell tab="devices" navigate={navigate} alertBadge={unackCount}
          user={user} mqttOnline={mqttOnline} tanks={tanks}>
          <DeviceInfo
            tank={tank} onBack={backToDevices} onUpdate={updateTank}
            deviceAppearance={deviceAppearance} onAppearanceChange={handleAppearanceChange}
            onRemove={() => {
              setTanks(prev => prev.filter(t => t.id !== tank.id));
              backToDevices();
              if (window.API) API.del('/api/tanks/' + tank.id).catch(() => {
                setTanks(prev => [...prev, tank]);
                window.__toast?.('Failed to remove device — please try again', { kind: 'error' });
              });
            }}
            onReset={() => backToDevices()}/>
        </MainShell>
      );
    }

    // ── Settings sub-screen routes — full pages in MainShell so header+footer are always visible ──
    const settingsContent = (
      <Settings tanks={tanks} user={user} onLogout={handleLogout}
        refreshAlerts={refreshAlerts} navigate={navigate} onUpdateUser={handleUpdateUser}/>
    );

    if (p === '/settings/groups') {
      return (
        <MainShell tab="settings" navigate={navigate} alertBadge={unackCount}
          user={user} mqttOnline={mqttOnline} tanks={tanks}>
          <ManageGroups onBack={() => navigate('/settings', { replace: true })}/>
        </MainShell>
      );
    }

    if (p === '/settings/sharing') {
      return (
        <MainShell tab="settings" navigate={navigate} alertBadge={unackCount}
          user={user} mqttOnline={mqttOnline} tanks={tanks}>
          <ManageSharing tanks={tanks} onBack={() => navigate('/settings', { replace: true })}/>
        </MainShell>
      );
    }

    if (p === '/settings/display') {
      return (
        <MainShell tab="settings" navigate={navigate} alertBadge={unackCount}
          user={user} mqttOnline={mqttOnline} tanks={tanks}>
          <DisplayPreferences user={user} onBack={() => navigate('/settings', { replace: true })}/>
        </MainShell>
      );
    }

    if (p === '/settings/devices') {
      return (
        <MainShell tab="settings" navigate={navigate} alertBadge={unackCount}
          user={user} mqttOnline={mqttOnline} tanks={tanks}>
          <Devices tanks={tanks} setTanks={setTanks} navigate={navigate}
            refreshTanks={refreshTanks} basePath="/settings/device"
            onBack={() => navigate('/settings', { replace: true })}/>
        </MainShell>
      );
    }

    if (p.startsWith('/settings/device/')) {
      const rawId = route.parts[2];
      const tank  = tanks.find(t => String(t.id) === rawId || t.id === +rawId);
      if (!tank) { navigate('/settings/devices', { replace: true }); return null; }
      const backToSettingsDevices = () => navigate('/settings/devices', { replace: true });
      return (
        <MainShell tab="settings" navigate={navigate} alertBadge={unackCount}
          user={user} mqttOnline={mqttOnline} tanks={tanks}>
          <DeviceInfo
            tank={tank} onBack={backToSettingsDevices} onUpdate={updateTank}
            deviceAppearance={deviceAppearance} onAppearanceChange={handleAppearanceChange}
            onRemove={() => {
              setTanks(prev => prev.filter(t => t.id !== tank.id));
              backToSettingsDevices();
              if (window.API) API.del('/api/tanks/' + tank.id).catch(() => {
                setTanks(prev => [...prev, tank]);
                window.__toast?.('Failed to remove device — please try again', { kind: 'error' });
              });
            }}
            onReset={() => backToSettingsDevices()}/>
        </MainShell>
      );
    }

    // ── Main tabbed content ──
    const backToDash = () => navigate('/dashboard', { replace: true });
    return (
        <MainShell tab={tabFromPath} navigate={navigate} alertBadge={unackCount}
          user={user} mqttOnline={mqttOnline} tanks={tanks}>
          {!loaded ? <SkeletonView/> : (
          <>
            {tabFromPath === 'dashboard' && dashboardContent}
            {tabFromPath === 'devices'   && <Devices tanks={tanks} setTanks={setTanks} navigate={navigate} refreshTanks={refreshTanks}/>}
            {tabFromPath === 'analytics' && <SwipeBack onBack={backToDash}><Analytics tanks={tanks}/></SwipeBack>}
            {tabFromPath === 'alerts'    && <SwipeBack onBack={backToDash}><Alerts alerts={alerts} setAlerts={setAlerts}/></SwipeBack>}
            {tabFromPath === 'settings'  && <SwipeBack onBack={backToDash}>{settingsContent}</SwipeBack>}
          </>
        )}
      </MainShell>
    );
  };

  return (
    <ToastProvider>
      <div style={{ minHeight: '100vh', maxWidth: 460, margin: '0 auto', position: 'relative', paddingTop: 'env(safe-area-inset-top, 0px)' }}>
        {renderContent()}
      </div>
    </ToastProvider>
  );
};

/* ============================================================
   MAIN SHELL — top bar + content area + bottom nav
   ============================================================ */

const MainShell = ({ tab, navigate, alertBadge, user, mqttOnline, tanks, children }) => (
  <>
    <TopBar alertBadge={alertBadge} navigate={navigate} user={user} mqttOnline={mqttOnline} tanks={tanks}/>
    {children}
    <BottomNav activeTab={tab} setActiveTab={t => navigate(`/${t}`, { replace: true })} alertBadge={alertBadge}/>
  </>
);

/* ============================================================
   TOP BAR
   ============================================================ */

const TopBar = ({ alertBadge, navigate, user, mqttOnline, tanks = [] }) => {
  const anyOnline = tanks.some(t => t.online);
  return (
    <div style={{
      position: 'sticky', top: 0, zIndex: 30,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: '10px 16px',
      background: 'rgba(10,14,39,0.78)',
      backdropFilter: 'blur(14px)', WebkitBackdropFilter: 'blur(14px)',
      borderBottom: '1px solid var(--border)', height: 56,
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        <div style={{
          width: 32, height: 32, borderRadius: 9,
          background: '#ffffff',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          boxShadow: '0 4px 12px rgba(0,0,0,0.12)',
        }}>
          <svg viewBox="0 0 48 48" width={22} height={22} fill="none">
            <circle cx="24" cy="24" r="19" stroke="#1D9E75" strokeWidth="0.75" fill="none" opacity="0.45"/>
            <line x1="24" y1="15" x2="24" y2="5" stroke="#1D9E75" strokeWidth="1"/>
            <line x1="31.79" y1="19.5" x2="40.45" y2="14.5" stroke="#1D9E75" strokeWidth="1"/>
            <line x1="31.79" y1="28.5" x2="40.45" y2="33.5" stroke="#1D9E75" strokeWidth="1"/>
            <line x1="24" y1="33" x2="24" y2="43" stroke="#1D9E75" strokeWidth="1"/>
            <line x1="16.21" y1="28.5" x2="7.55" y2="33.5" stroke="#1D9E75" strokeWidth="1"/>
            <line x1="16.21" y1="19.5" x2="7.55" y2="14.5" stroke="#1D9E75" strokeWidth="1"/>
            <circle cx="24" cy="24" r="9" fill="#1D9E75"/>
            <path d="M 26 20.54 A 4 4 0 1 1 22 20.54" stroke="white" strokeWidth="1.4" fill="none" strokeLinecap="round"/>
            <line x1="24" y1="18" x2="24" y2="22.5" stroke="white" strokeWidth="1.4" strokeLinecap="round"/>
            <circle cx="24" cy="5" r="3" fill="#185FA5"/>
            <circle cx="40.45" cy="14.5" r="3" fill="#D85A30"/>
            <circle cx="40.45" cy="33.5" r="3" fill="#7F77DD"/>
            <circle cx="24" cy="43" r="3" fill="#EF9F27"/>
            <circle cx="7.55" cy="33.5" r="3" fill="#D4537E"/>
            <circle cx="7.55" cy="14.5" r="3" fill="#639922"/>
          </svg>
        </div>
        <div>
          <div style={{ fontSize: 16, fontWeight: 700, letterSpacing: -0.2, lineHeight: 1 }}>Velonics Hub</div>
          <div style={{ fontSize: 10, color: mqttOnline ? 'var(--green)' : 'var(--text-3)', marginTop: 2, letterSpacing: 0.6, display: 'flex', alignItems: 'center', gap: 4 }}>
            <span style={{
              width: 5, height: 5, borderRadius: '50%',
              background: mqttOnline ? 'var(--green)' : 'var(--text-3)',
              boxShadow: mqttOnline ? '0 0 6px var(--green)' : 'none',
              animation: mqttOnline ? 'pulse-dot 2s infinite' : 'none',
            }}/>
            {mqttOnline ? (anyOnline ? 'ALL ONLINE' : 'CONNECTED') : 'CONNECTING…'}
          </div>
        </div>
      </div>

      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        {/* MQTT status pill */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 9px', background: 'rgba(255,255,255,0.04)', border: '1px solid var(--border)', borderRadius: 999 }}>
          <Ic name={mqttOnline ? 'wifi' : 'wifiOff'} size={12} color={mqttOnline ? 'var(--green)' : 'var(--amber)'}/>
          <StatusDot kind={mqttOnline ? (anyOnline ? 'green' : 'amber') : 'red'} size={6} pulse={mqttOnline}/>
        </div>
        {/* Alerts bell */}
        <button onClick={() => navigate('/alerts', { replace: true })} style={{
          position: 'relative',
          background: 'rgba(255,255,255,0.04)', border: '1px solid var(--border)',
          borderRadius: 10, width: 34, height: 34,
          display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text)',
        }}>
          <Ic name="bell" size={16} color="var(--text-2)"/>
          {alertBadge > 0 && (
            <span style={{
              position: 'absolute', top: -4, right: -4,
              background: 'var(--red)', color: '#fff',
              fontSize: 9, fontWeight: 700, minWidth: 16, height: 16, borderRadius: 999,
              display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 4px',
              border: '2px solid #0a0e27', animation: 'bounce-y 1.6s ease-in-out infinite',
            }}>{alertBadge}</span>
          )}
        </button>
        {/* User avatar → settings */}
        <button onClick={() => navigate('/settings', { replace: true })} style={{
          width: 34, height: 34, borderRadius: '50%',
          background: 'var(--avatar-gradient, linear-gradient(135deg, var(--cyan), var(--teal)))',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          fontSize: 12, fontWeight: 600, color: '#0a0e27',
          border: 'none', cursor: 'pointer',
        }}>
          {(user && user.initials) || 'AC'}
        </button>
      </div>
    </div>
  );
};

/* ============================================================
   BOTTOM NAV
   ============================================================ */

const BottomNav = ({ activeTab, setActiveTab, alertBadge }) => {
  const tabs = [
    { id: 'dashboard', label: 'Home',      icon: 'home'  },
    { id: 'analytics', label: 'Analytics', icon: 'chart' },
    { id: 'alerts',    label: 'Alerts',    icon: 'bell', badge: alertBadge },
    { id: 'settings',  label: 'Settings',  icon: 'gear'  },
  ];
  return (
    <div style={{
      position: 'fixed', bottom: 0, left: 0, right: 0, maxWidth: 460, margin: '0 auto',
      background: 'rgba(10,14,39,0.92)',
      backdropFilter: 'blur(18px)', WebkitBackdropFilter: 'blur(18px)',
      borderTop: '1px solid var(--border)',
      paddingBottom: 'env(safe-area-inset-bottom)', zIndex: 40,
    }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', height: 60 }}>
        {tabs.map(t => {
          const active = activeTab === t.id;
          return (
            <button key={t.id} onClick={() => setActiveTab(t.id)} style={{
              background: 'transparent', border: 'none',
              display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
              gap: 4, padding: 4, cursor: 'pointer',
              color: active ? 'var(--cyan)' : 'var(--text-3)',
              position: 'relative', transition: 'color 200ms ease',
            }}>
              {active && <div style={{ position: 'absolute', top: 0, left: '30%', right: '30%', height: 2, background: 'linear-gradient(90deg, transparent, var(--cyan), transparent)', borderRadius: 2 }}/>}
              <div style={{ position: 'relative' }}>
                <Ic name={t.icon} size={20} strokeWidth={active ? 2 : 1.6}/>
                {t.badge > 0 && <span style={{ position: 'absolute', top: -3, right: -5, width: 7, height: 7, borderRadius: '50%', background: 'var(--red)', border: '1px solid #0a0e27' }}/>}
              </div>
              <span style={{ fontSize: 10, fontWeight: active ? 600 : 500, letterSpacing: 0.2 }}>{t.label}</span>
            </button>
          );
        })}
      </div>
    </div>
  );
};

/* ============================================================
   SKELETON
   ============================================================ */

const SkeletonView = () => (
  <div style={{ padding: 20 }}>
    <div className="skeleton" style={{ height: 24, width: '60%', marginBottom: 12 }}/>
    <div className="skeleton" style={{ height: 14, width: '40%', marginBottom: 22 }}/>
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 8, marginBottom: 22 }}>
      {[1,2,3,4].map(i => <div key={i} className="skeleton" style={{ height: 60 }}/>)}
    </div>
    {[1,2,3].map(i => <div key={i} className="skeleton" style={{ height: 220, marginBottom: 14 }}/>)}
  </div>
);

/* ============================================================
   MOUNT
   ============================================================ */

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);
