// AppShell — top nav (functional routing), gem tray, page outlet, transitions

// Tracks the mobile breakpoint (<680px) with a resize listener. Matches the
// isMobile checks scattered through the section components.
function useIsMobile(bp = 680) {
  const [m, setM] = React.useState(() => typeof window !== 'undefined' && window.innerWidth < bp);
  React.useEffect(() => {
    const onResize = () => setM(window.innerWidth < bp);
    window.addEventListener('resize', onResize);
    onResize();
    return () => window.removeEventListener('resize', onResize);
  }, [bp]);
  return m;
}

// ─── Rest timer (timestamp-based, background-proof) ──────────────────
// The countdown is derived from wall-clock timestamps, NOT from decrementing a
// counter on every setInterval tick. When the tab is backgrounded the browser
// freezes setInterval, but Date.now() keeps advancing — so on return we compute
// the true remaining time and, if the rest already elapsed, fire completion.
//
//   rem = ceil((endAt - Date.now()) / 1000)
//
// The interval only repaints the UI (and drives warning/done checks while the
// tab is visible); it never *is* the clock. A `visibilitychange → visible`
// listener recomputes immediately so a frozen crono self-corrects on return.
//
// opts.persist  → mirror the active rest into localStorage so it survives a
//                 reload / accidental app-close (coherent with session persist).
// opts.onDone   → called once when a rest completes (e.g. flash "next set").
const REST_KEY = 'atlas.resttimer.v1';

function useRestTimer(soundEnabled, opts = {}) {
  const { persist = false, onDone } = opts;
  // Active rest: { endAt (ms epoch), total (secs), key }. null = idle.
  const [timer, setTimer] = React.useState(null);
  const [, forceTick] = React.useState(0);
  const timerRef  = React.useRef(null);   // latest timer, for listeners/intervals
  const soundRef  = React.useRef(soundEnabled);
  const onDoneRef = React.useRef(onDone);
  const warnedRef = React.useRef(null);   // key we've already sounded the 10s warning for
  React.useEffect(() => { soundRef.current  = soundEnabled; }, [soundEnabled]);
  React.useEffect(() => { onDoneRef.current = onDone; }, [onDone]);
  React.useEffect(() => { timerRef.current  = timer; }, [timer]);

  const remOf = (t) => (t ? Math.max(0, Math.ceil((t.endAt - Date.now()) / 1000)) : 0);

  const clearActive = React.useCallback(() => {
    warnedRef.current = null;
    setTimer(null);
    if (persist) { try { localStorage.removeItem(REST_KEY); } catch (_) {} }
  }, [persist]);

  // Complete the current rest: sound + onDone + clear. Fired both by the normal
  // interval reaching 0 and by returning to a foreground where it already elapsed.
  const fireDone = React.useCallback((t) => {
    if (!t) return;
    if (typeof AtlasTimer !== 'undefined') AtlasTimer.restDone(soundRef.current);
    if (onDoneRef.current) onDoneRef.current();
    clearActive();
  }, [clearActive]);

  // Evaluate the current timer against the wall clock: warn at 10s, done at 0.
  const evaluate = React.useCallback(() => {
    const t = timerRef.current;
    if (!t) return;
    const rem = remOf(t);
    if (rem <= 0) { fireDone(t); return; }
    if (rem <= 10 && warnedRef.current !== t.key) {
      warnedRef.current = t.key;
      if (typeof AtlasTimer !== 'undefined') AtlasTimer.restWarning(soundRef.current);
    }
    forceTick(n => n + 1);   // repaint
  }, [fireDone]);

  // Restore a persisted rest on mount — but only if it's still meaningfully
  // running (>0.5s left). A stale/expired one is discarded silently (no sound
  // on a cold reload minutes later).
  React.useEffect(() => {
    if (!persist) return;
    let saved = null;
    try { saved = JSON.parse(localStorage.getItem(REST_KEY) || 'null'); } catch (_) {}
    if (saved && typeof saved.endAt === 'number' && (saved.endAt - Date.now()) > 500) {
      setTimer(saved);
    } else if (saved) {
      try { localStorage.removeItem(REST_KEY); } catch (_) {}
    }
  }, [persist]);

  // Repaint interval + visibilitychange recompute. Re-armed whenever the active
  // rest changes (keyed on timer?.key).
  React.useEffect(() => {
    if (!timer) return;
    const id = setInterval(evaluate, 500);
    const onVis = () => { if (document.visibilityState === 'visible') evaluate(); };
    document.addEventListener('visibilitychange', onVis);
    window.addEventListener('focus', onVis);
    evaluate();   // sync immediately on (re)arm
    return () => {
      clearInterval(id);
      document.removeEventListener('visibilitychange', onVis);
      window.removeEventListener('focus', onVis);
    };
  }, [timer?.key, evaluate]);   // eslint-disable-line react-hooks/exhaustive-deps

  const persistTimer = React.useCallback((t) => {
    if (!persist) return;
    try {
      if (t) localStorage.setItem(REST_KEY, JSON.stringify(t));
      else localStorage.removeItem(REST_KEY);
    } catch (_) {}
  }, [persist]);

  const start = React.useCallback((secs) => {
    const s = Math.max(1, Math.round(secs || 0));
    const t = { endAt: Date.now() + s * 1000, total: s, key: Date.now() };
    warnedRef.current = null;
    setTimer(t);
    persistTimer(t);
  }, [persistTimer]);

  const adjust = React.useCallback((delta) => {
    setTimer(prev => {
      if (!prev) return prev;
      const rem = Math.max(0, Math.ceil((prev.endAt - Date.now()) / 1000));
      const nextRem = Math.max(1, rem + delta);
      const t = { endAt: Date.now() + nextRem * 1000, total: Math.max(prev.total, nextRem), key: prev.key };
      // Re-open the warning window if we bumped back above 10s
      if (nextRem > 10) warnedRef.current = null;
      persistTimer(t);
      return t;
    });
  }, [persistTimer]);

  const skip = React.useCallback(() => { clearActive(); }, [clearActive]);

  const restTimer = timer ? { rem: remOf(timer), total: timer.total, key: timer.key } : null;
  return { restTimer, start, adjust, skip };
}

// Anchored dropdown menu rendered in a portal on <body> with position:fixed, so
// it's never clipped by an ancestor's overflow (e.g. a card's overflow:hidden or
// the global overflow-x:clip) and always sits above the tab bar. Positioned from
// the trigger's getBoundingClientRect; flips upward near the bottom of the screen.
function AtlasMenu({ open, anchorRef, onClose, width = 184, children }) {
  const [, force] = React.useState(0);
  React.useEffect(() => {
    if (!open) return;
    const rerender = () => force(n => n + 1);
    const close = () => onClose && onClose();
    window.addEventListener('resize', rerender);
    // Close on scroll so the fixed menu can't detach from its trigger
    window.addEventListener('scroll', close, true);
    return () => { window.removeEventListener('resize', rerender); window.removeEventListener('scroll', close, true); };
  }, [open, onClose]);

  if (!open || !anchorRef?.current || typeof ReactDOM === 'undefined' || !ReactDOM.createPortal) return null;
  const r = anchorRef.current.getBoundingClientRect();
  const vw = window.innerWidth, vh = window.innerHeight;
  const left = Math.max(8, Math.min(r.right - width, vw - width - 8));
  const openUp = (vh - r.bottom) < 200;
  const pos = openUp ? { bottom: Math.round(vh - r.top + 6) } : { top: Math.round(r.bottom + 6) };

  return ReactDOM.createPortal(
    React.createElement(React.Fragment, null,
      React.createElement('div', { onClick: onClose, style: { position: 'fixed', inset: 0, zIndex: 3000 } }),
      React.createElement('div', {
        style: {
          position: 'fixed', left: Math.round(left), ...pos, width, zIndex: 3001,
          background: '#0E1A2C', border: '1px solid rgba(255,255,255,0.10)', borderRadius: 12,
          boxShadow: '0 16px 44px rgba(0,0,0,0.55)', overflow: 'hidden',
          animation: 'fadeIn .12s ease',
        },
      }, children)
    ),
    document.body
  );
}

// Minimal stroke icons for the mobile tab bar
function NavIcon({ name, color }) {
  const p = { width: 22, height: 22, viewBox: '0 0 24 24', fill: 'none', stroke: color, strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' };
  if (name === 'home')    return <svg {...p}><path d="M3 10.5 12 3l9 7.5" /><path d="M5 9.5V21h14V9.5" /></svg>;
  if (name === 'coach')   return <svg {...p}><path d="M4 5h16v10H9l-4 4v-4H4z" /></svg>;
  if (name === 'builder') return <svg {...p}><path d="M4 6h16M4 12h16M4 18h10" /></svg>;
  if (name === 'play')    return <svg {...p}><path d="M7 4.5v15l12-7.5z" fill={color} /></svg>;
  if (name === 'rutinas') return <svg {...p}><path d="M6 3h12a1 1 0 0 1 1 1v17l-7-4-7 4V4a1 1 0 0 1 1-1z" /></svg>;
  if (name === 'aula')    return <svg {...p}><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3H20v15H6.5A2.5 2.5 0 0 0 4 20.5z" /><path d="M4 20.5A2.5 2.5 0 0 1 6.5 18H20" /></svg>;
  if (name === 'user')    return <svg {...p}><circle cx="12" cy="8" r="4" /><path d="M4 21c0-4 4-6 8-6s8 2 8 6" /></svg>;
  return null;
}

// Fixed bottom tab bar for mobile — native-app style navigation
function AppMobileTabBar() {
  const { route, navigate } = useRoute();
  const tabs = [
    { path: '/',        label: 'Inicio',   icon: 'home' },
    { path: '/coach',   label: 'Coach',    icon: 'coach' },
    { path: '/builder', label: 'Builder',  icon: 'builder' },
    { path: '/rutinas', label: 'Rutinas',  icon: 'rutinas' },
    { path: '/aula',    label: 'Aula',     icon: 'aula' },
  ];
  const ACTIVE = '#3B82F6';
  const IDLE   = 'rgba(250,250,247,0.5)';
  return (
    <nav style={{
      position: 'fixed', left: 0, right: 0, bottom: 0, zIndex: 300,
      background: '#0F1A2E', borderTop: '1px solid rgba(255,255,255,0.08)',
      paddingBottom: 'env(safe-area-inset-bottom)',
      display: 'flex', alignItems: 'stretch',
    }}>
      {tabs.map(t => {
        const active = route === t.path;
        const color = active ? ACTIVE : IDLE;
        return (
          <a key={t.path} href={'#' + t.path}
            onClick={(e) => { e.preventDefault(); atlasHandleTabTap(route, t.path, navigate); }}
            aria-current={active ? 'page' : undefined}
            style={{
              flex: 1, height: 56, textDecoration: 'none',
              display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 3,
              color, transition: 'color .15s',
            }}>
            <NavIcon name={t.icon} color={color} />
            <span style={{ fontFamily: '"Inter",system-ui', fontSize: 10, fontWeight: active ? 700 : 500, letterSpacing: -0.1 }}>{t.label}</span>
          </a>
        );
      })}
    </nav>
  );
}

// Slim mobile top header — logo + gems + profile avatar (Perfil lives here now
// that the bottom tab bar is Inicio·Coach·Builder·Rutinas·Aula)
function AppMobileHeader() {
  const { route, navigate } = useRoute();
  const { state } = useStore();
  const perfilActive = route === '/perfil';
  return (
    <nav className="atlas-mobile-header" style={{
      position: 'sticky', top: 0, zIndex: 50,
      background: 'rgba(250,250,247,0.9)',
      backdropFilter: 'saturate(180%) blur(20px)',
      WebkitBackdropFilter: 'saturate(180%) blur(20px)',
      borderBottom: '1px solid rgba(15,26,46,0.06)',
      // Sit below the iPhone status bar / notch (viewport-fit=cover)
      paddingTop: 'env(safe-area-inset-top)',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, padding: '11px 16px' }}>
        <a href="#/" onClick={(e) => { e.preventDefault(); navigate('/'); }}
          style={{ display: 'flex', alignItems: 'center', gap: 9, textDecoration: 'none', color: '#0F1A2E', minWidth: 0 }}>
          <AtlasA size={24} color="#0F1A2E" stroke={9} />
          <span style={{ fontFamily: '"Inter",system-ui', fontWeight: 800, fontSize: 16, letterSpacing: -0.4, whiteSpace: 'nowrap' }}>
            Atlas <span style={{ fontWeight: 500, opacity: 0.55 }}>Method</span>
          </span>
        </a>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
          <GemTray />
          <button aria-label="Perfil" onClick={() => navigate('/perfil')}
            style={{ borderRadius: 999, flexShrink: 0, cursor: 'pointer', padding: 0,
              border: 'none', background: 'none', lineHeight: 0,
              outline: perfilActive ? '2px solid #3B82F6' : 'none', outlineOffset: 1,
              display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <AvatarBadge
              avatarId={(state.cosmetics || {}).avatar || 'default'}
              frameId={(state.cosmetics || {}).frame || 'none'}
              name={state.user?.name} size={34} />
          </button>
        </div>
      </div>
    </nav>
  );
}

function AppNav() {
  const { route, navigate } = useRoute();
  const { state } = useStore();
  const isMobile = useIsMobile();

  const items = [
    { path: '/', label: 'Inicio' },
    { path: '/aula', label: 'Aula' },
    { path: '/builder', label: 'Builder' },
    { path: '/rutinas', label: 'Mis rutinas' },
    { path: '/coach', label: 'Atlas Coach' },
    { path: '/progreso', label: 'Progreso' },
    { path: '/perfil', label: 'Perfil' },
  ];

  // Mobile: slim top header + fixed bottom tab bar (native-app navigation)
  if (isMobile) {
    return (
      <>
        <AppMobileHeader />
        <AppMobileTabBar />
      </>
    );
  }

  return (
    <nav style={{
      position: 'sticky', top: 0, zIndex: 50,
      background: 'rgba(250,250,247,0.85)',
      backdropFilter: 'saturate(180%) blur(20px)',
      WebkitBackdropFilter: 'saturate(180%) blur(20px)',
      borderBottom: '1px solid rgba(15,26,46,0.06)',
    }}>
      <div style={{
        maxWidth: 1280, margin: '0 auto', padding: '14px 32px',
        display: 'flex', alignItems: 'center', gap: 32,
      }}>
        {/* Logo */}
        <a href="#/" onClick={(e)=>{e.preventDefault(); navigate('/');}}
          style={{ display: 'flex', alignItems: 'center', gap: 10, textDecoration: 'none', color: '#0F1A2E' }}>
          <AtlasA size={26} color="#0F1A2E" stroke={9} />
          <span style={{ fontFamily: '"Inter",system-ui', fontWeight: 800, fontSize: 17, letterSpacing: -0.4 }}>
            Atlas <span style={{ fontWeight: 500, opacity: 0.55 }}>Method</span>
          </span>
        </a>

        {/* Routes */}
        <div style={{ display: 'flex', gap: 4, marginLeft: 8 }}>
          {items.map(it => {
            const active = route === it.path;
            return (
              <a key={it.path} href={'#'+it.path}
                onClick={(e)=>{e.preventDefault(); atlasHandleTabTap(route, it.path, navigate);}}
                aria-current={active ? 'page' : undefined}
                style={{
                  padding: '7px 14px', borderRadius: 999, textDecoration: 'none',
                  fontFamily: '"Inter",system-ui', fontSize: 13, fontWeight: active ? 700 : 500,
                  letterSpacing: -0.1,
                  color: active ? '#FAFAF7' : '#3A4257',
                  background: active ? '#0F1A2E' : 'transparent',
                  transition: 'background 0.18s, color 0.18s',
                }}>
                {it.label}
              </a>
            );
          })}
        </div>

        <div style={{ flex: 1 }} />

        {/* Gem tray */}
        <GemTray />

        {/* User chip */}
        <a href="#/perfil" onClick={(e)=>{e.preventDefault(); navigate('/perfil');}}
          style={{
            display: 'flex', alignItems: 'center', gap: 8, padding: '5px 12px 5px 5px',
            borderRadius: 999, border: '1px solid rgba(15,26,46,0.1)',
            textDecoration: 'none', color: '#0F1A2E',
          }}>
          <span style={{
            width: 26, height: 26, borderRadius: 999,
            background: 'linear-gradient(135deg, #1A2845, #0F1A2E)',
            color: '#FAFAF7',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontFamily: '"Inter",system-ui', fontSize: 11, fontWeight: 800,
          }}>{(state.user.name || 'A')[0].toUpperCase()}</span>
          <span style={{ fontFamily: '"Inter",system-ui', fontSize: 13, fontWeight: 600 }}>{state.user.name}</span>
        </a>
      </div>
    </nav>
  );
}

// ─── Gem tray ────────────────────────────────────────
function GemTray() {
  const { state } = useStore();
  const { navigate } = useRoute();
  const [pulse, setPulse] = React.useState(false);
  const lastBalance = React.useRef(state.gems.balance);

  React.useEffect(() => {
    if (state.gems.balance > lastBalance.current) {
      setPulse(true);
      const t = setTimeout(() => setPulse(false), 800);
      lastBalance.current = state.gems.balance;
      return () => clearTimeout(t);
    }
    lastBalance.current = state.gems.balance;
  }, [state.gems.balance]);

  const balanceTxt = state.gems.balance.toLocaleString('es-ES');

  // The gem counter is a shortcut into the store's redeemable items.
  return (
    <button
      onClick={() => navigate('/tienda')}
      aria-label={`Gemas: ${balanceTxt}. Abrir la tienda`}
      title="Abrir la tienda"
      style={{
        display: 'flex', alignItems: 'center', gap: 6,
        padding: '5px 12px', borderRadius: 999, cursor: 'pointer',
        background: 'rgba(15,26,46,0.04)', border: '1px solid rgba(15,26,46,0.08)',
        transform: pulse ? 'scale(1.06)' : 'scale(1)',
        transition: 'transform 0.4s cubic-bezier(.34,1.56,.64,1), background 0.15s',
      }}
      onMouseEnter={e => { e.currentTarget.style.background = 'rgba(15,26,46,0.08)'; }}
      onMouseLeave={e => { e.currentTarget.style.background = 'rgba(15,26,46,0.04)'; }}
    >
      <Gem size={14} />
      <span aria-hidden="true" style={{
        fontFamily: 'ui-monospace,Menlo,monospace', fontSize: 12, fontWeight: 700,
        color: '#0F1A2E', letterSpacing: -0.2,
      }}>
        {balanceTxt}
      </span>
    </button>
  );
}

function Gem({ size = 16 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none">
      <defs>
        <linearGradient id="gemG" x1="0" y1="0" x2="1" y2="1">
          <stop offset="0" stopColor="#7AC4FF" />
          <stop offset="1" stopColor="#2A6FDB" />
        </linearGradient>
      </defs>
      <path d="M 6 4 L 18 4 L 22 9 L 12 22 L 2 9 Z" fill="url(#gemG)" stroke="#0F1A2E" strokeWidth="1.4" strokeLinejoin="round" />
      <path d="M 6 4 L 9 9 L 2 9 M 18 4 L 15 9 L 22 9 M 9 9 L 12 22 L 15 9 M 9 9 L 15 9" stroke="#FAFAF7" strokeWidth="1" strokeLinejoin="round" fill="none" opacity="0.7" />
    </svg>
  );
}

// ─── Page transition wrapper ────────────────────────
function PageTransition({ routeKey, children }) {
  const [displayed, setDisplayed] = React.useState({ key: routeKey, content: children });
  const [phase, setPhase] = React.useState('in');

  React.useEffect(() => {
    if (routeKey !== displayed.key) {
      setPhase('out');
      const t = setTimeout(() => {
        setDisplayed({ key: routeKey, content: children });
        setPhase('in');
      }, 180);
      return () => clearTimeout(t);
    } else {
      setDisplayed({ key: routeKey, content: children });
    }
  }, [routeKey, children]);

  return (
    <div style={{
      opacity: phase === 'in' ? 1 : 0,
      transform: phase === 'in' ? 'translateY(0)' : 'translateY(8px)',
      transition: 'opacity 0.22s ease, transform 0.22s ease',
    }}>
      {displayed.content}
    </div>
  );
}

// ── Shared navigation helpers ─────────────────────────────────────────────────
function atlasReducedMotion() {
  try { return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); }
  catch { return false; }
}
function atlasScrollToTop() {
  try { window.scrollTo({ top: 0, behavior: atlasReducedMotion() ? 'auto' : 'smooth' }); }
  catch { window.scrollTo(0, 0); }
}

// Tapping the active nav tab: scroll up if the page is scrolled; once at the top,
// ask the current section to return to its root (e.g. close an open Aula article).
function atlasHandleTabTap(route, path, navigate) {
  if (route !== path) { navigate(path); return; }
  if ((window.scrollY || window.pageYOffset || 0) > 4) { atlasScrollToTop(); return; }
  try { window.dispatchEvent(new CustomEvent('atlas:navreset', { detail: { route: path } })); } catch {}
}

// Horizontal swipe between internal tabs (no library). Returns touch handlers to
// spread on the content container. Honors: >60px horizontal and clearly more
// horizontal than vertical; ignores the iOS edge-back zone (<24px from a side),
// inputs/sliders, [data-swipe-ignore], and any ancestor that scrolls horizontally
// (carousels, the category strip, the muscle map, etc.).
function useSwipeNav({ index, count, onChange }) {
  const start = React.useRef(null);
  function hasHScrollAncestor(el, root) {
    let n = el;
    while (n && n !== root && n !== document.body) {
      try {
        const s = window.getComputedStyle(n);
        if ((s.overflowX === 'auto' || s.overflowX === 'scroll') && n.scrollWidth > n.clientWidth + 2) return true;
      } catch {}
      n = n.parentElement;
    }
    return false;
  }
  const onTouchStart = (e) => {
    if (!e.touches || e.touches.length !== 1) { start.current = null; return; }
    const t = e.touches[0];
    const W = window.innerWidth || document.documentElement.clientWidth;
    if (t.clientX < 24 || t.clientX > W - 24) { start.current = null; return; }  // iOS back-gesture zone
    const tgt = e.target;
    if (tgt && tgt.closest && tgt.closest('input,textarea,select,[role="slider"],[data-swipe-ignore]')) { start.current = null; return; }
    if (hasHScrollAncestor(tgt, e.currentTarget)) { start.current = null; return; }
    start.current = { x: t.clientX, y: t.clientY };
  };
  const onTouchEnd = (e) => {
    const s = start.current; start.current = null;
    if (!s || !e.changedTouches || !e.changedTouches.length) return;
    const c = e.changedTouches[0];
    const dx = c.clientX - s.x, dy = c.clientY - s.y;
    if (Math.abs(dx) < 60) return;                       // not far enough
    if (Math.abs(dx) <= Math.abs(dy) * 1.3) return;      // not clearly horizontal
    const dir = dx < 0 ? 1 : -1;                         // swipe left → next tab
    const next = index + dir;
    if (next < 0 || next >= count) return;
    onChange(next, dir);
  };
  return { onTouchStart, onTouchEnd };
}

// Floating "back to top" button. Fades in past ~600px; hidden in the player and
// the Coach chat (there the scroll goes downward). Sits above the mobile tab bar.
function ScrollTopButton() {
  const { route } = useRoute();
  const isMobile = useIsMobile();
  const [visible, setVisible] = React.useState(false);
  const hidden = route === '/player' || route === '/coach';

  React.useEffect(() => {
    if (hidden) { setVisible(false); return; }
    let ticking = false;
    const onScroll = () => {
      if (ticking) return;
      ticking = true;
      window.requestAnimationFrame(() => {
        setVisible((window.scrollY || window.pageYOffset || 0) > 600);
        ticking = false;
      });
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => window.removeEventListener('scroll', onScroll);
  }, [hidden]);

  if (hidden) return null;

  return (
    <button
      aria-label="Volver arriba"
      onClick={atlasScrollToTop}
      style={{
        position: 'fixed', zIndex: 320,
        right: isMobile ? 16 : 24,
        bottom: isMobile ? 'calc(56px + env(safe-area-inset-bottom) + 16px)' : 24,
        width: 48, height: 48, borderRadius: 999,
        background: '#0F1A2E', color: '#FAFAF7',
        border: '1px solid rgba(255,255,255,0.12)', cursor: 'pointer',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        boxShadow: '0 8px 24px -8px rgba(15,26,46,0.5)',
        opacity: visible ? 1 : 0,
        transform: visible ? 'translateY(0)' : 'translateY(8px)',
        pointerEvents: visible ? 'auto' : 'none',
        transition: 'opacity .22s var(--ease-out), transform .22s var(--ease-out)',
      }}>
      <svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
        <path d="M12 19V6M6 11l6-6 6 6" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
      </svg>
    </button>
  );
}

Object.assign(window, { AppNav, PageTransition, Gem, GemTray, AtlasMenu, useIsMobile, useRestTimer, ScrollTopButton, useSwipeNav, atlasReducedMotion, atlasScrollToTop });
