import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
// Consumer typography pilot font faces (SimplerPro Alte, four exact weights).
import "./styles/fonts.css";
import { applyAppSurface } from "@/lib/app-surface-font-scope";
// Single canonical splash artwork. Same file the Capacitor asset-generation
// flow consumes for the native iOS/Android splash layers — editing it updates
// both the native splash and this WebView launch cover. No second copy exists.
import splashArtworkUrl from "../assets/splash.png";
import { isNativePlatform, isAndroid, isIOS } from "@/lib/capacitor-utils";
import { LaunchCover } from "@/plugins/launch-cover";
import { markStatusBarOverlayApplied } from "@/hooks/useStatusBarTint";


import { initGlobalErrorLogger, isExpectedAvatarStatMiss } from "@/lib/global-error-logger";

// Pre-init boot error capture — runs BEFORE initGlobalErrorLogger so any JS
// exception thrown during very early bootstrap (Capacitor bridge init, plugin
// registration, image/webp hooks, error-logger setup itself) is preserved on
// window.__bootErrors for later inspection. Logging-only; behavior unchanged.
(() => {
  try {
    const w = window as any;
    if (w.__bootErrorsInstalled) return;
    w.__bootErrorsInstalled = true;
    w.__bootErrors = [];
    w.__bootPhase = 'pre-init';
    const push = (entry: Record<string, unknown>) => {
      try {
        // Skip expected Capacitor Filesystem avatar cache misses. Canonical
        // signal remains [ImageCacheTrace] Filesystem.stat miss.
        if (isExpectedAvatarStatMiss([entry, (entry as any)?.message, (entry as any)?.stack])) return;
        w.__bootErrors.push({ t: Math.round(performance.now()), phase: w.__bootPhase, ...entry });
        // Mirror to console so native log shows it too
        // eslint-disable-next-line no-console
        console.error('[BootError]', JSON.stringify(entry));
      } catch { /* noop */ }
    };
    window.addEventListener('error', (e: ErrorEvent) => {
      push({
        kind: 'error',
        message: e.message,
        filename: e.filename,
        lineno: e.lineno,
        colno: e.colno,
        stack: e.error?.stack,
      });
    }, true);
    window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
      const r: any = e.reason;
      push({
        kind: 'unhandledrejection',
        message: r?.message ?? String(r),
        stack: r?.stack,
      });
    });
    // eslint-disable-next-line no-console
    console.log('[BootPhase] pre-init: boot-error capture installed');
  } catch { /* noop */ }
})();

const markBootPhase = (phase: string) => {
  try {
    (window as any).__bootPhase = phase;
    // eslint-disable-next-line no-console
    console.log(`[BootPhase] ${phase} t=${Math.round(performance.now())}`);
  } catch { /* noop */ }
};

markBootPhase('error-logger-init');
// Initialize global error logger BEFORE anything else (captures JS Eval errors)
initGlobalErrorLogger();


import { prefetchComplete, destinationReady, launchComplete, signalLaunchComplete } from "@/lib/prefetch-signals";
// TEMP-LAUNCH-TRACE: temporary instrumentation, remove after validation
import { markLaunchFlag, emitLaunchContractComplete } from "@/lib/debug/launchTrace";

import { getSnapshot as getSessionSnapshot, subscribe as subscribeSession } from "@/lib/auth/sessionStore";

/**
 * Duration of the launch cover's slide-down reveal. Must stay in sync with the
 * `#initial-loader` transition declared in index.html.
 */
const COVER_SLIDE_MS = 420;

/** Single upper bound on the whole launch handoff (pre-existing contract). */
const LAUNCH_FAILSAFE_MS = 6000;

/** Hard bound on the native `LaunchCover.present()` bridge call (iOS). */
const PRESENT_TIMEOUT_MS = 1200;

/** Hard bound on the native `LaunchCover.dismiss()` bridge call (iOS). */
const DISMISS_TIMEOUT_MS = 900;

/**
 * Bounded wait for the Consumer pilot webfonts to be decoded and ready.
 *
 * The pilot faces use `font-display: block`, so text is never painted in the
 * system font first — but the launch cover is the product's visual readiness
 * boundary, so the cover must not be released while glyphs are still invisible.
 * Hard-capped: a font that never resolves releases the cover anyway (the
 * pre-existing failure-release semantics are preserved) and the event is
 * reported through `[Launch] fonts` rather than stalling startup.
 */
const FONTS_READY_NATIVE_MS = 2500;
const FONTS_READY_WEB_MS = 1500;

function fontsReady(maxMs: number): Promise<void> {
  const startedAt = performance.now();
  const report = (verdict: 'ready' | 'timeout' | 'unsupported') => {
    console.log(
      `[Launch] fonts ${verdict} after ${Math.round(performance.now() - startedAt)}ms`,
    );
  };
  const api = (document as Document & { fonts?: FontFaceSet }).fonts;
  if (!api || typeof api.ready?.then !== 'function') {
    report('unsupported');
    return Promise.resolve();
  }
  return new Promise<void>((resolve) => {
    let done = false;
    const finish = (verdict: 'ready' | 'timeout') => {
      if (done) return;
      done = true;
      clearTimeout(timer);
      report(verdict);
      resolve();
    };
    const timer = setTimeout(() => finish('timeout'), maxMs);
    api.ready.then(() => finish('ready'), () => finish('timeout'));
  });
}

/**
 * Bound a bridge promise. A native call that never resolves must never be able
 * to stall the launch: on timeout the returned promise rejects and the caller
 * treats it exactly like a failure. The underlying call is left to settle on
 * its own — the native plugin's generation token makes a late `present`
 * harmless once `dismiss` has been requested.
 */
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    const timer = setTimeout(
      () => reject(new Error(`LaunchCover.${label} timed out after ${ms}ms`)),
      ms,
    );
    promise.then(
      (value) => { clearTimeout(timer); resolve(value); },
      (error) => { clearTimeout(timer); reject(error); },
    );
  });
}

/**
 * Resolves once the canonical session store reaches a terminal verdict
 * (`authenticated` | `unauthenticated`). Read-only: no second auth listener,
 * no Supabase call, no state mutation, no timeout of its own (the launch
 * fail-safe bounds it). Unsubscribes immediately after resolving once.
 */
function sessionVerdictSettled(): Promise<void> {
  if (getSessionSnapshot().status !== "unknown") return Promise.resolve();
  return new Promise<void>((resolve) => {
    let done = false;
    const unsubscribe = subscribeSession(() => {
      if (done) return;
      if (getSessionSnapshot().status === "unknown") return;
      done = true;
      unsubscribe();
      resolve();
    });
    // Guard against a verdict committed between the snapshot read and the
    // subscription being installed.
    if (!done && getSessionSnapshot().status !== "unknown") {
      done = true;
      unsubscribe();
      resolve();
    }
  });
}

/** Resolves after two animation frames, i.e. once the ready frame is painted. */
function nextPaint(): Promise<void> {
  return new Promise<void>((resolve) => {
    requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
  });
}

/** The static launch cover parsed from index.html. */
function getLaunchCover(): HTMLElement | null {
  return document.getElementById('initial-loader');
}

/**
 * Promote the default browser loader into the native launch cover.
 *
 * The green field, full-bleed layout, z-index and slide transition live under
 * `html.native-launch #initial-loader` in index.html, so they are unreachable
 * until this runs — a browser can never paint the native cover. The artwork is
 * the Vite-resolved URL of the single canonical `assets/splash.png`, the same
 * file the Capacitor asset-generation flow uses for the native splash layers.
 */
function armNativeLaunchCover(): void {
  try {
    document.documentElement.style.setProperty(
      '--launch-cover-image',
      `url("${splashArtworkUrl}")`,
    );
    document.documentElement.classList.add('native-launch');
  } catch { /* noop — cosmetic only */ }
}

/** Drop the native launch-cover state. Safe to call repeatedly. */
function disarmNativeLaunchCover(): void {
  try {
    document.documentElement.classList.remove('native-launch');
    document.documentElement.style.removeProperty('--launch-cover-image');
  } catch { /* noop */ }
}


/**
 * Bounded wait for the WebView viewport to stop resizing.
 *
 * Applying the steady-state status-bar contract (notably iOS
 * `setOverlaysWebView({ overlay: false })`) resizes the WebView frame and
 * recomputes `env(safe-area-inset-*)`. Doing that behind the opaque cover and
 * waiting here is what removes the shell displacement / geometry jump: the
 * user never sees the intermediate layouts. Hard-capped — never open-ended.
 */
function viewportSettled(maxMs = 800): Promise<void> {
  return new Promise<void>((resolve) => {
    const startedAt = performance.now();
    let lastHeight = -1;
    let stableFrames = 0;
    let done = false;

    const finish = () => {
      if (done) return;
      done = true;
      window.removeEventListener('resize', onResize);
      resolve();
    };
    const onResize = () => { stableFrames = 0; };
    window.addEventListener('resize', onResize);

    const tick = () => {
      if (done) return;
      const height = window.innerHeight;
      if (height === lastHeight) stableFrames += 1;
      else { stableFrames = 0; lastHeight = height; }
      if (stableFrames >= 3 || performance.now() - startedAt >= maxMs) {
        finish();
        return;
      }
      requestAnimationFrame(tick);
    };
    requestAnimationFrame(tick);
  });
}

/**
 * Bounded wait for the safe-area CSS variable to be populated by the native
 * bridge. Preserves the pre-existing 50 × 20ms budget.
 */
async function safeAreaSettled(): Promise<void> {
  for (let attempt = 0; attempt < 50; attempt++) {
    const top = getComputedStyle(document.documentElement)
      .getPropertyValue('--safe-area-inset-top');
    if (top && top !== '0px' && !top.includes('undefined')) {
      console.log('[Launch] Safe area ready:', top);
      return;
    }
    await new Promise((resolve) => setTimeout(resolve, 20));
  }
}

/**
 * Slide the launch cover down off-screen, then remove it. Bounded: if
 * `transitionend` never fires (reduced motion, backgrounded WebView,
 * compositor stall) the cover is still removed on the timer.
 */
function revealApp(): Promise<void> {
  const cover = getLaunchCover();
  if (!cover) return Promise.resolve();

  return new Promise<void>((resolve) => {
    let done = false;
    const finish = () => {
      if (done) return;
      done = true;
      clearTimeout(timer);
      cover.removeEventListener('transitionend', onTransitionEnd);
      cover.remove();
      resolve();
    };
    const onTransitionEnd = (event: TransitionEvent) => {
      if (event.propertyName === 'transform') finish();
    };
    cover.addEventListener('transitionend', onTransitionEnd);
    const timer = setTimeout(finish, COVER_SLIDE_MS + 250);
    requestAnimationFrame(() => cover.classList.add('launch-cover-out'));
  });
}


import { setupRealtimeCacheInvalidation } from "@/lib/realtime-cache-invalidation";

// Start the global realtime cache invalidation listener exactly once for the
// app lifecycle. The function has an internal `isSubscribed` guard, so even if
// React StrictMode double-invokes effects this remains a single subscription.
//
// C3 — the websocket handshake is not required for Discovery first paint, so
// it is held until the LaunchCover is PHYSICALLY dismissed (`launchComplete`
// resolves on every exit path in this file: native, web, degraded, fail-safe).
launchComplete.then(() => {
  setupRealtimeCacheInvalidation();
  markBootPhase('realtime-listener-installed');
});


async function initializeApp() {
  markBootPhase('initializeApp-start');

  // ANDROID launch-cover ownership (canonical, explicit): Android continues to
  // use the existing HTML launch cover (`html.native-launch #initial-loader`)
  // by design. iOS uses the native LaunchCover rendering port instead, because
  // only a native view can stay above the status-bar background view that
  // `setOverlaysWebView(false)` inserts. No conditional/ambiguous ownership.
  if (isAndroid()) {
    armNativeLaunchCover();
  }


  // Configure native platform basics (non-blocking)
  if (isNativePlatform()) {
    const [
      nativeImageUtils,
      iosWebpDebug,
      splashScreenModule,
      statusBarModule,
    ] = await Promise.all([
      import("@/lib/native-image-utils"),
      import("@/lib/ios-webp-debug"),
      import("@capacitor/splash-screen"),
      import("@capacitor/status-bar"),
    ]);

    const { purgeImageCacheIfNeeded, installNativeImageRewriteGuards } = nativeImageUtils;
    const { installIOSWebpDebugHook } = iosWebpDebug;
    const { SplashScreen } = splashScreenModule;
    const { StatusBar, Style } = statusBarModule;

    document.body.classList.add('native-app');
    if (isAndroid()) {
      document.body.classList.add('android');
    }
    if (isIOS()) {
      document.body.classList.add('ios');
    }

    markBootPhase('native-modules-loaded');
    // Install native iOS image URL rewrite guards early
    installNativeImageRewriteGuards();
    installIOSWebpDebugHook();
    markBootPhase('image-hooks-installed');
    // Show splash screen on both platforms
    try {
      await SplashScreen.show({
        showDuration: 0,  // Don't auto-hide
        autoHide: false,
      });
      console.log('[Init] SplashScreen shown manually');
    } catch (error) {
      console.warn('[Init] Failed to show SplashScreen:', error);
    }
    markBootPhase('splash-shown');

    
    // Configure Status Bar for the LAUNCH PHASE (platform-specific).
    // The native splash and the web launch cover are both full-bleed brand
    // green (#01A375); the status bar must overlay them so no white strip is
    // painted above. The steady-state contract is applied later in this file,
    // behind the still-opaque cover, before the slide-down reveal.

    try {
      if (isIOS()) {
        // iOS: overlay only. setBackgroundColor is Android-only and must not
        // be called here.
        await StatusBar.setOverlaysWebView({ overlay: true });
        await StatusBar.setStyle({ style: Style.Dark }); // Dark = light icons
        console.log('[Init] iOS status bar configured for splash (overlay: true)');
      } else if (isAndroid()) {
        await StatusBar.setOverlaysWebView({ overlay: true });
        await StatusBar.setBackgroundColor({ color: '#00000000' }); // transparent
        await StatusBar.setStyle({ style: Style.Dark }); // Dark = light icons
        console.log('[Init] Android status bar configured for splash (transparent overlay)');
      }
    } catch (error) {
      // Non-fatal: a visual status-bar call must never block startup.
      console.warn('[Init] Splash status bar config failed:', error);
    }
    markBootPhase('statusbar-configured');
  }



  // Purge stale cached images (WEBP cleanup) - non-blocking, native only
  if (isNativePlatform()) {
    const { purgeImageCacheIfNeeded } = await import("@/lib/native-image-utils");
    purgeImageCacheIfNeeded().catch(() => {});
  }
  
  markBootPhase('react-render');
  // Canonical typography surface, applied BEFORE the first React paint so the
  // PSC / Club OS system-font pin is already in effect on an admin deep link.
  applyAppSurface(window.location.pathname);
  // Render the app immediately
  createRoot(document.getElementById("root")!).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
  markBootPhase('react-rendered');

  queueMicrotask(() => {
    console.log('[GlobalError] POST-BOOT marker');
  });

  // ---------------------------------------------------------------------
  // Web path (Desktop Web / Mobile Web): previous browser behavior.
  // The dark spinner loader is removed at the first React paint — no native
  // launch cover, no minimum display time, no prefetch gate, no fail-safe,
  // no viewport settlement, no StatusBar call, no slide animation. Nothing
  // native can block the browser.
  //
  // The one addition is a hard-bounded `document.fonts.ready` wait so the
  // spinner is not swapped for a screen whose glyphs are still invisible
  // (`font-display: block`). On timeout the loader is removed regardless.
  // ---------------------------------------------------------------------
  if (!isNativePlatform()) {
    await fontsReady(FONTS_READY_WEB_MS);
    getLaunchCover()?.remove();
    markBootPhase('web-loader-removed');
    // TEMP-LAUNCH-TRACE — web has no launch gates; the contract is emitted for
    // parity from a detached observer that nothing awaits.
    markLaunchFlag('minimumDisplaySatisfied');
    markLaunchFlag('launchCoverDismissed');
    void (async () => {
      const settled = sessionVerdictSettled();
      settled.then(() => markLaunchFlag('sessionReady'));
      destinationReady.then(() => markLaunchFlag('destinationReady'));
      await Promise.race([
        Promise.all([settled, prefetchComplete, destinationReady]),
        new Promise((r) => setTimeout(r, LAUNCH_FAILSAFE_MS)),
      ]);
      emitLaunchContractComplete();
    })();

    signalLaunchComplete();
    return;
  }





  // ---------------------------------------------------------------------
  // Native launch handoff (iOS / Android only — the web path returned above).

  //
  //   1. Confirm the static launch cover has painted.
  //   2. Dismiss the native splash with fadeOutDuration: 0 (hard cut onto an
  //      identical, already-painted green field — no seam, no white frame).
  //   3. Behind the still-opaque cover: wait for the canonical session verdict
  //      and the honest terminal prefetch verdict.
  //   4. Behind the still-opaque cover: apply the steady-state StatusBar
  //      contract and let the resulting viewport/safe-area geometry settle.
  //   5. Confirm the first application frame is painted, slide the cover down,
  //      remove it, and release StatusBar ownership.
  //
  // Every wait is bounded, and the whole path is additionally capped by the
  // pre-existing 6s fail-safe.
  // ---------------------------------------------------------------------
  const minDisplayTime = new Promise((resolve) => setTimeout(resolve, 1000));

  // Track if prefetch completed to suppress spurious timeout warning
  let prefetchCompleted = false;
  prefetchComplete.then(() => { prefetchCompleted = true; });

  let failSafeWon = true;
  const failSafe = new Promise<void>((resolve) => setTimeout(() => {
    if (!prefetchCompleted) {
      console.warn('[Launch] Fail-safe reached (6s), proceeding without full prefetch');
    }
    resolve();
  }, LAUNCH_FAILSAFE_MS));

  // 1 + 2 + 3 — present the launch cover and prove it is attached, laid out
  // and ready to paint BEFORE the native splash is dismissed.
  //
  // The bridge call is hard-bounded: a missing plugin, a rejection, a
  // `presented: false` result or a call that never comes back all collapse to
  // the same DEGRADED verdict. iOS never falls back to the HTML/WebView cover —
  // that cover is Android-owned and animating it here would reintroduce the
  // white status strip, the cover resize and the artwork recrop that the
  // native port exists to remove.
  let nativeCoverPresented = false;
  let iosCoverDegraded = false;
  if (isIOS()) {
    try {
      const result = await withTimeout(
        LaunchCover.present(),
        PRESENT_TIMEOUT_MS,
        'present',
      );
      nativeCoverPresented = result?.presented === true;
      console.log('[Launch] Native LaunchCover present ->', JSON.stringify(result));
    } catch (error) {
      console.warn('[Launch] Native LaunchCover.present unavailable:', error);
    }
    iosCoverDegraded = !nativeCoverPresented;
    if (iosCoverDegraded) {
      console.warn('[Launch] iOS launch cover DEGRADED — immediate, non-animated handoff');
    }
  }
  await nextPaint();

  // Canonical, single splash dismissal. Shared by every native path (normal,
  // degraded, Android) so the Capacitor splash can never stay retained.
  if (isNativePlatform()) {
    try {
      const { SplashScreen } = await import("@capacitor/splash-screen");
      await SplashScreen.hide({ fadeOutDuration: 0 });
      console.log('[Launch] Native splash dismissed (zero fade) behind launch cover');
    } catch (error) {
      console.warn('[Launch] SplashScreen.hide failed:', error);
    }
  }
  markBootPhase('native-splash-dismissed');

  if (iosCoverDegraded) {
    // ---------------------------------------------------------------------
    // iOS DEGRADED handoff — no cover exists, so nothing may be hidden behind
    // one. No HTML cover is armed (`armNativeLaunchCover` is Android-only), no
    // readiness gate is held, no animation runs. Apply the canonical final
    // contract in guarded non-fatal calls and reveal immediately.
    // ---------------------------------------------------------------------
    try {
      const { StatusBar, Style } = await import('@capacitor/status-bar');
      await StatusBar.setStyle({ style: Style.Light }); // Light = dark icons
      // Same canonical steady state as the normal path (see below): overlay
      // mode ON, so no post-reveal resize can be triggered by tinted surfaces.
      await StatusBar.setOverlaysWebView({ overlay: true });
      markStatusBarOverlayApplied();
    } catch (error) {
      console.warn('[Launch] Degraded steady-state status bar config failed:', error);
    }

    await safeAreaSettled();
    await viewportSettled();

    // Cancel any native present that may still be in flight: dismiss()
    // invalidates the pending generation so a late main-thread present can
    // never attach a cover after this point.
    try {
      await withTimeout(LaunchCover.dismiss(), DISMISS_TIMEOUT_MS, 'dismiss');
    } catch (error) {
      console.warn('[Launch] Degraded LaunchCover.dismiss cleanup skipped:', error);
    }
    disarmNativeLaunchCover(); // defensive: iOS never arms it
    // The static HTML loader is never armed on iOS, so it still carries its
    // default in-flow black/spinner styling. It must be removed on EVERY iOS
    // exit path or it occupies a full viewport ahead of #root and pushes the
    // whole app below the fold.
    getLaunchCover()?.remove();
    markBootPhase('launch-cover-degraded-reveal');
    markLaunchFlag('launchCoverDismissed'); // TEMP-LAUNCH-TRACE
    signalLaunchComplete();
    console.log('[Launch] Degraded reveal complete (no animation)');
    emitLaunchContractComplete(); // TEMP-LAUNCH-TRACE

  } else {

  // 3 — readiness, entirely behind the opaque cover.
  //
  // `destinationReady` is the strengthened contract: it resolves only when the
  // FINAL destination surface is rendering (ProtectedRoute's terminal children
  // render, or LoginPage's phone surface). Without it, `prefetchComplete` can
  // win while ProtectedRoute is still in its `profile_pending` "טוען…" branch,
  // which is what exposed loading text during the slide.
  console.log('[Launch] Waiting for session verdict, destination, prefetch, fonts and minimum display time...');
  const readyPath = (async () => {
    // TEMP-LAUNCH-TRACE: passive observers. Each awaits the SAME promise the
    // Promise.all below awaits, so ordering and timing are unchanged.
    const sessionSettled = sessionVerdictSettled();
    sessionSettled.then(() => markLaunchFlag('sessionReady'));
    destinationReady.then(() => markLaunchFlag('destinationReady'));
    const minDisplay = minDisplayTime;
    Promise.resolve(minDisplay).then(() => markLaunchFlag('minimumDisplaySatisfied'));

    await Promise.all([
      sessionSettled,
      prefetchComplete,
      destinationReady,
      fontsReady(FONTS_READY_NATIVE_MS),
      minDisplay,
    ]);
    failSafeWon = false;
  })();


  await Promise.race([readyPath, failSafe]);

  if (failSafeWon) {
    console.warn('[Launch] Fail-safe reveal — app may briefly show a loading state');
  }

  // 6 — steady-state status bar + geometry settlement, still fully behind the
  // opaque cover. iOS restores the canonical `overlay: false` contract here.
  if (isNativePlatform()) {
    try {
      const { StatusBar, Style } = await import('@capacitor/status-bar');
      if (isIOS()) {
        // Canonical iOS steady state: dark icons with the WebView extending
        // UNDER the status bar (`overlay: true`), so every surface paints its
        // own page tint into the top safe area (`useStatusBarTint`).
        //
        // This must be the value applied HERE, behind the still-opaque cover.
        // Previously the launch applied `overlay: false` and the first tinted
        // surface flipped it back to `true` right after the reveal — that
        // WebView resize (top inset 0px → 62px) was the one-time boot jump.
        await StatusBar.setStyle({ style: Style.Light }); // Light = dark icons
        await StatusBar.setOverlaysWebView({ overlay: true });
        markStatusBarOverlayApplied();
      } else if (isAndroid()) {

        await StatusBar.setOverlaysWebView({ overlay: true });
        await StatusBar.setBackgroundColor({ color: '#ffffffff' });
        await StatusBar.setStyle({ style: Style.Dark });
      }
    } catch (error) {
      // Non-fatal: never block the reveal on a cosmetic call.
      console.warn('[Launch] Steady-state status bar config failed:', error);
    }
    await safeAreaSettled();
  }
  await viewportSettled();

  // iOS never arms the HTML cover, so the static `#initial-loader` still
  // carries its default in-flow black/spinner styling. It MUST be detached
  // BEFORE the native cover starts sliding away — otherwise the slide
  // progressively uncovers that black spinner surface instead of the app.
  // Android owns the same node AS its animating cover, so it is never removed
  // here — `revealApp()` keeps that ownership.
  if (isIOS()) getLaunchCover()?.remove();


  // Two final painted frames of the settled geometry, with the loader already
  // gone, before anything moves.
  await nextPaint();
  await nextPaint();

  // 7 — dismiss the cover owned by this platform. iOS: bounded native dismiss
  // (a stalled bridge call must never block `launchComplete`). Android: the
  // existing HTML slide-down. `revealApp()` is unreachable on iOS.
  if (nativeCoverPresented) {
    try {
      await withTimeout(LaunchCover.dismiss(), DISMISS_TIMEOUT_MS, 'dismiss');
    } catch (error) {
      console.warn('[Launch] Native LaunchCover.dismiss failed:', error);
    }
  } else if (!isIOS()) {
    await revealApp();
  }
  disarmNativeLaunchCover();
  // Idempotent safety net: Android removes the loader inside `revealApp()`,
  // and on iOS the node is already detached (`?.remove()` is a no-op then).
  getLaunchCover()?.remove();
  markBootPhase('launch-cover-removed');
  markLaunchFlag('launchCoverDismissed'); // TEMP-LAUNCH-TRACE


  // 8 — launchComplete.
  signalLaunchComplete();
  console.log('[Launch] Reveal complete, StatusBar ownership released');
  emitLaunchContractComplete(); // TEMP-LAUNCH-TRACE


  }




  if (isNativePlatform()) {


    
    // Background verification tasks (non-blocking)
    setTimeout(async () => {
      // Verify layout
      const topBar = document.querySelector('[data-layout="top-bar"]');
      const bottomNav = document.querySelector('[data-layout="bottom-nav"]');
      const fixedElements = Array.from(document.querySelectorAll('*'))
        .filter(el => {
          const s = getComputedStyle(el as HTMLElement);
          if (!s || s.position !== 'fixed') return false;
          const top = s.top || '';
          const isTopZero = top === '0px' || top.startsWith('0');
          const hasBackground = s.backgroundColor !== 'rgba(0, 0, 0, 0)';
          const z = parseInt(s.zIndex || '0', 10);
          return isTopZero && hasBackground && z >= 0;
        });

      if (topBar && bottomNav) {
        const topRect = (topBar as HTMLElement).getBoundingClientRect();
        const bottomRect = (bottomNav as HTMLElement).getBoundingClientRect();
        
        console.log('[Init] Layout verification:', {
          topBar: { top: topRect.top, height: topRect.height },
          bottomNav: { bottom: window.innerHeight - bottomRect.bottom, height: bottomRect.height },
          windowHeight: window.innerHeight
        });
      }
      
      // Scan for unwanted fixed elements with visible backgrounds (skip intentional layout elements)
      const fixedTopElements = fixedElements.filter(el => {
        const htmlEl = el as HTMLElement;
        // Skip our intentional layout elements
        if (htmlEl.dataset.layout) return false;
        const s = getComputedStyle(htmlEl);
        const top = s.top || '';
        const isTopZero = top === '0px' || top.startsWith('0');
        return isTopZero;
      });

      if (fixedTopElements.length > 0) {
        console.warn('[Layout Scan] Found unexpected fixed@top elements:');
        fixedTopElements.forEach(el => {
          const s = getComputedStyle(el as HTMLElement);
          console.log(' - Element:', {
            tag: el.tagName,
            class: (el as HTMLElement).className,
            id: (el as HTMLElement).id,
            top: s.top,
            z: s.zIndex,
          });
        });
      }
    }, 100);
  }
}

// Start initialization. `signalLaunchComplete` is idempotent, and this
// fallback guarantees steady-state StatusBar owners are never blocked if the
// orchestrator throws before its own release point. Any failure — before or
// after native splash dismissal, during readiness, geometry settlement or the
// slide — hard-removes the cover and clears the native-launch state so no
// green field can ever be retained.
initializeApp()
  .catch((error) => {
    console.error('[Launch] initializeApp failed:', error);
    getLaunchCover()?.remove();
    disarmNativeLaunchCover();
  })
  .finally(() => {
    signalLaunchComplete();
  });


