// ============================================================
// Olive Garden — main shell
// Window manager, sidebar, menu bar, ASCII donut background, cursor
// Depends on: windows.jsx, terminal.jsx (loaded before this)
// Exports: <App /> mounted at #root
// ============================================================

const { useState, useEffect, useRef, useCallback, useMemo } = React;

// ---------- Window manifest ----------
// id, title, app key, initial position/size, content component name (resolved at render time)
const INITIAL_WINDOWS = [
  { id:'about',      app:'about',      title:'001 — about',           x:120, y:70,  w:720, h:440, content:'AboutContent' },
  { id:'work',       app:'work',       title:'002 — selected work',   x:540, y:340, w:580, h:380, content:'WorkContent' },
  { id:'experience', app:'experience', title:'003 — experience',      x:80,  y:480, w:500, h:460, content:'ExperienceContent' },
];

// All apps known to the sidebar — opens new window if not already open
const APPS = [
  { app:'about',      title:'001 — about',         glyph:'⌂', content:'AboutContent',      w:680, h:430 },
  { app:'work',       title:'002 — selected work', glyph:'◫', content:'WorkContent',       w:580, h:380 },
  { app:'experience', title:'003 — experience',   glyph:'⌗', content:'ExperienceContent', w:500, h:460 },
  { app:'writing',    title:'004 — writing',       glyph:'✎', content:'WritingContent',    w:440, h:420 },
  { app:'photos',     title:'005 — travel',        glyph:'◳', content:'PhotosContent',     w:560, h:480 },
  { app:'hobbies',    title:'006 — hobbies',       glyph:'❋', content:'HobbiesContent',    w:520, h:420 },
  { app:'terminal',   title:'~ terminal',          glyph:'›_', content:'TerminalContent',  w:540, h:360 },
  { app:'lessons',    title:'genai 101',           glyph:'⌬',  content:'LessonsContent',   w:500, h:460 },
  { app:'contact',    title:'@ contact',           glyph:'✉', content:'ContactContent',    w:420, h:340, bottom:true },
];

// ============================================================
// Sidebar icon set — pixel-grid SVGs (12×12 viewBox, strokeWidth 1.5)
// ============================================================
const APP_ICONS = {
  about: (
    <svg viewBox="0 0 12 12" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <circle cx="6" cy="3.5" r="2"/>
      <path d="M1 12C1 9 3.2 7.5 6 7.5C8.8 7.5 11 9 11 12"/>
    </svg>
  ),
  work: (
    <svg viewBox="0 0 12 12" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <rect x="1.5" y="4" width="9" height="6.5" rx="1"/>
      <path d="M4 4V2.5C4 1.9 5 1.9 6 1.9C7 1.9 8 1.9 8 2.5V4"/>
      <line x1="1.5" y1="7" x2="10.5" y2="7"/>
    </svg>
  ),
  experience: (
    <svg viewBox="0 0 12 12" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <line x1="3.5" y1="1" x2="3.5" y2="11"/>
      <circle cx="3.5" cy="3" r="1" fill="currentColor" stroke="none"/>
      <circle cx="3.5" cy="6.5" r="1" fill="currentColor" stroke="none"/>
      <circle cx="3.5" cy="10" r="1" fill="currentColor" stroke="none"/>
      <line x1="5.5" y1="3" x2="10.5" y2="3"/>
      <line x1="5.5" y1="6.5" x2="9" y2="6.5"/>
      <line x1="5.5" y1="10" x2="10" y2="10"/>
    </svg>
  ),
  writing: (
    <svg viewBox="0 0 12 12" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M8.5 1.5L10.5 3.5L3.5 10.5H1.5V8.5L8.5 1.5Z"/>
      <line x1="7.5" y1="2.5" x2="9.5" y2="4.5"/>
    </svg>
  ),
  photos: (
    <svg viewBox="0 0 12 12" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <polyline points="1,10 4.5,4.5 7,7.5 9,5.5 11,10"/>
      <line x1="1" y1="10.5" x2="11" y2="10.5"/>
    </svg>
  ),
  hobbies: (
    <svg viewBox="0 0 12 12" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M6 10.5C4 9 1.5 7 1 5C0.5 3 2 1.5 3.5 1.5C4.5 1.5 5.5 2 6 3C6.5 2 7.5 1.5 8.5 1.5C10 1.5 11.5 3 11 5C10.5 7 8 9 6 10.5Z"/>
    </svg>
  ),
  terminal: (
    <svg viewBox="0 0 12 12" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <polyline points="2,3 5.5,6 2,9"/>
      <line x1="6.5" y1="9" x2="10.5" y2="9"/>
    </svg>
  ),
  lessons: (
    <svg viewBox="0 0 12 12" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <rect x="3" y="3" width="6" height="6" rx="0.5"/>
      <line x1="5" y1="1.5" x2="5" y2="3"/>
      <line x1="7" y1="1.5" x2="7" y2="3"/>
      <line x1="5" y1="9" x2="5" y2="10.5"/>
      <line x1="7" y1="9" x2="7" y2="10.5"/>
      <line x1="1.5" y1="5" x2="3" y2="5"/>
      <line x1="1.5" y1="7" x2="3" y2="7"/>
      <line x1="9" y1="5" x2="10.5" y2="5"/>
      <line x1="9" y1="7" x2="10.5" y2="7"/>
    </svg>
  ),
  contact: (
    <svg viewBox="0 0 12 12" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <rect x="1" y="2.5" width="10" height="7" rx="1"/>
      <polyline points="1,2.5 6,7 11,2.5"/>
    </svg>
  ),
};

// ============================================================
// App
// ============================================================
function App(){
  const [t, setTweak] = useTweaks(window.TWEAK_DEFAULTS);

  // Apply tweaks to <html> as data-attrs so CSS handles palette / chrome
  React.useEffect(() => {
    const r = document.documentElement;
    r.dataset.palette = t.palette;
    r.dataset.backdrop = t.backdrop;
    r.dataset.chrome = t.chrome;
  }, [t.palette, t.backdrop, t.chrome]);

  // Windows start at z:10 so desktop icons (z:5) stay behind them
  const WIN_Z_BASE = 10;
  const [windows, setWindows] = useState(() => INITIAL_WINDOWS.map((w,i) => ({...w, z: WIN_Z_BASE + i, minimized: false, maximized: false})));
  const [focused, setFocused] = useState('about');
  const zCounter = useRef(WIN_Z_BASE + INITIAL_WINDOWS.length);
  const [statusFlash, setStatusFlash] = useState(false);
  const [donutTakeover, setDonutTakeover] = useState(false);
  const [toast, setToast] = useState(null);
  const [hintHidden, setHintHidden] = useState(false);
  const [lightbox, setLightbox] = useState(null);

  // Welcome toast (3s)
  useEffect(() => {
    const t = setTimeout(() => setToast(null), 3000);
    setToast('welcome — type "help" in terminal');
    return () => clearTimeout(t);
  }, []);

  // Hide hint after 6s
  useEffect(() => {
    const t = setTimeout(() => setHintHidden(true), 6000);
    return () => clearTimeout(t);
  }, []);

  // Bring window to front
  const focus = useCallback((id) => {
    zCounter.current += 1;
    const newZ = zCounter.current;
    setWindows(ws => ws.map(w => w.id === id ? {...w, z:newZ} : w));
    setFocused(id);
  }, []);

  // Open or focus an app (also restores a minimized window)
  const openApp = useCallback((appKey) => {
    setWindows(ws => {
      const existing = ws.find(w => w.app === appKey);
      if (existing) {
        zCounter.current += 1;
        // Unminimize if it was rolled up
        return ws.map(w => w.id === existing.id ? {...w, z:zCounter.current, minimized: false} : w);
      }
      const def = APPS.find(a => a.app === appKey);
      if (!def) return ws;
      zCounter.current += 1;
      // Cascade new windows
      const offset = (ws.length % 6) * 28;
      return [...ws, {
        id: appKey + '-' + Date.now(),
        app: appKey,
        title: def.title,
        content: def.content,
        x: 180 + offset, y: 100 + offset,
        w: def.w, h: def.h,
        z: zCounter.current,
        minimized: false,
        maximized: false,
      }];
    });
    setFocused(appKey);
  }, []);

  // Close window
  const closeWindow = useCallback((id) => {
    setWindows(ws => ws.filter(w => w.id !== id));
  }, []);

  // Menu-bar bulk actions
  const minimizeAll = useCallback(() => {
    setWindows(ws => ws.map(w => ({...w, minimized: true})));
  }, []);
  const closeAll = useCallback(() => {
    setWindows([]);
  }, []);
  const restoreAll = useCallback(() => {
    setWindows(ws => ws.map(w => ({...w, minimized: false})));
  }, []);
  const tileWindows = useCallback(() => {
    setWindows(ws => {
      const deskW = window.innerWidth;
      const deskH = window.innerHeight - 30 - 58; // menubar + dock
      const n = ws.length;
      if (!n) return ws;
      const cols = Math.ceil(Math.sqrt(n));
      const rows = Math.ceil(n / cols);
      const pad = 10;
      const tileW = Math.floor((deskW - pad * (cols + 1)) / cols);
      const tileH = Math.floor((deskH - pad * (rows + 1)) / rows);
      return ws.map((w, i) => {
        const col = i % cols;
        const row = Math.floor(i / cols);
        return {...w, x: pad + col * (tileW + pad), y: pad + row * (tileH + pad), w: tileW, h: tileH, minimized: false, maximized: false};
      });
    });
  }, []);

  // Minimise — roll window up to just the titlebar.
  // If the window is currently maximised, restore original size/position first
  // so the collapsed strip appears at the window's natural location, not at 0,0.
  const toggleMinimize = useCallback((id) => {
    zCounter.current += 1;
    const newZ = zCounter.current;
    setWindows(ws => ws.map(w => {
      if (w.id !== id) return w;
      if (w.minimized) {
        // Un-minimise
        return {...w, minimized: false, z: newZ};
      }
      // If maximised, restore to pre-max dimensions before collapsing
      const restore = (w.maximized && w._prev) ? {...w._prev, maximized: false} : {};
      return {...w, ...restore, minimized: true, z: newZ};
    }));
    setFocused(id);
  }, []);

  // Maximise — fill the desktop area, save/restore previous size
  const toggleMaximize = useCallback((id) => {
    zCounter.current += 1;
    const newZ = zCounter.current;
    setWindows(ws => ws.map(w => {
      if (w.id !== id) return w;
      if (w.maximized) {
        // Restore previous position + size
        return {...w, maximized: false, minimized: false, z: newZ, ...(w._prev || {})};
      }
      return {
        ...w,
        maximized: true,
        minimized: false,
        z: newZ,
        _prev: {x: w.x, y: w.y, w: w.w, h: w.h},
        x: 0, y: 0,
        w: window.innerWidth,
        h: window.innerHeight - 30 - 58, // menubar + dock
      };
    }));
    setFocused(id);
  }, []);

  // Move/resize window
  const moveWindow = useCallback((id, x, y) => {
    setWindows(ws => ws.map(w => w.id === id ? {...w, x, y} : w));
  }, []);
  const resizeWindow = useCallback((id, w, h) => {
    setWindows(ws => ws.map(win => win.id === id ? {...win, w, h} : win));
  }, []);

  // Easter: konami code
  useEffect(() => {
    const code = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
    let idx = 0;
    const h = (e) => {
      const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
      if (k === code[idx]) {
        idx++;
        if (idx === code.length) {
          setDonutTakeover(true);
          idx = 0;
        }
      } else {
        idx = (k === code[0]) ? 1 : 0;
      }
      if (e.key === 'Escape') {
        setDonutTakeover(false);
        setLightbox(null);
      }
    };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, []);

  // Status dot click counter (5x → flash + secret toast)
  const dotClicks = useRef(0);
  const onDotClick = () => {
    dotClicks.current += 1;
    setStatusFlash(true);
    setTimeout(() => setStatusFlash(false), 2400);
    if (dotClicks.current === 5) {
      setToast('●●● 30+ tools shipped · ~30h saved/wk · hire me');
      setTimeout(() => setToast(null), 3200);
      dotClicks.current = 0;
    }
  };

  // Open lightbox from photos window
  const onPhotoClick = useCallback((photo) => setLightbox(photo), []);

  // Terminal command exposed to terminal component — uses openApp
  const termAPI = useMemo(() => ({
    openApp,
    triggerDonut: () => setDonutTakeover(true),
  }), [openApp]);

  return (
    <React.Fragment>
      {t.backdrop === 'donut' && <DonutCanvas palette={t.palette} />}
      <DesktopIcons openApp={openApp} openIds={windows.map(w=>w.app)} />
      <Dock openApp={openApp} openIds={windows.map(w=>w.app)} />
      <MenuBar
        focused={focused}
        flash={statusFlash}
        onDotClick={onDotClick}
        openApp={openApp}
        windows={windows}
        tweaks={t}
        setTweak={setTweak}
        onMinimizeAll={minimizeAll}
        onCloseAll={closeAll}
        onRestoreAll={restoreAll}
        onTileWindows={tileWindows}
        onFocusWindow={focus}
      />

      {/* Windows */}
      <div className="desktop" style={{position:'fixed', inset:'30px 0 58px 0', overflow:'hidden', zIndex:8, pointerEvents:'none'}}>
        {windows.map(w => (
          <Window
            key={w.id}
            win={w}
            chrome={t.chrome}
            focused={focused === w.app || focused === w.id}
            onFocus={() => focus(w.id)}
            onClose={() => closeWindow(w.id)}
            onMinimize={() => toggleMinimize(w.id)}
            onMaximize={() => toggleMaximize(w.id)}
            onMove={(x,y) => moveWindow(w.id, x, y)}
            onResize={(ww,hh) => resizeWindow(w.id, ww, hh)}
          >
            <WindowContent name={w.content} api={{...termAPI, onPhotoClick, openApp}} />
          </Window>
        ))}
      </div>

      <div className={'hint' + (hintHidden ? ' hidden' : '')}>
        click desktop icons · drag windows · try the terminal · ↑↑↓↓←→←→ B A
      </div>

      {toast && <div className="toast">{toast}</div>}

      {donutTakeover && <DonutTakeover palette={t.palette} onClose={() => setDonutTakeover(false)} />}
      {lightbox && <Lightbox photo={lightbox} onClose={() => setLightbox(null)} />}

      <TweaksPanel title="Tweaks">
        <TweakSection label="Palette" />
        <TweakSelect
          label="Mood"
          value={t.palette}
          options={['olive','sun-baked','forest','studio']}
          onChange={(v) => setTweak('palette', v)}
        />
        <TweakSection label="Backdrop" />
        <TweakRadio
          label="Scene"
          value={t.backdrop}
          options={['donut','tiles','quiet']}
          onChange={(v) => setTweak('backdrop', v)}
        />
        <TweakSection label="Windows" />
        <TweakRadio
          label="Chrome"
          value={t.chrome}
          options={['modern','retro','sketchy']}
          onChange={(v) => setTweak('chrome', v)}
        />
      </TweaksPanel>

      <DesktopPlant />
      <Cursor />
    </React.Fragment>
  );
}

// ============================================================
// Window
// ============================================================
function Window({win, chrome, focused, onFocus, onClose, onMinimize, onMaximize, onMove, onResize, children, editableBody}){
  const ref = useRef(null);
  const drag = useRef(null);
  const [dragging, setDragging] = useState(false);

  // ── Rough.js sketch overlay ──────────────────────────────
  // SVG is absolutely overlaid on the window; pointer-events:none so clicks pass through.
  // Re-drawn whenever the window resizes, chrome mode changes, or the window id changes.
  const sketchRef = useRef(null);
  useEffect(() => {
    const svg = sketchRef.current;
    if (!svg) return;
    // Clear previous paths
    while (svg.firstChild) svg.removeChild(svg.firstChild);
    if (chrome !== 'sketchy') return;
    if (typeof rough === 'undefined') return;
    if (win.minimized) return; // let CSS handle collapsed state

    const inkColor = getComputedStyle(document.documentElement).getPropertyValue('--ink').trim() || '#1d1c1a';
    // Stable-but-unique seed per window so each window looks consistently different
    const seed = win.id.split('').reduce((a, c) => a + c.charCodeAt(0), 0) % 9999;
    const rc = rough.svg(svg);

    const frameOpts = {
      roughness: 2.8, stroke: inkColor, strokeWidth: 1.8,
      fill: 'none', bowing: 1.1, seed,
      disableMultiStroke: false,
    };
    const lineOpts = {
      roughness: 2.2, stroke: inkColor, strokeWidth: 1.4,
      bowing: 1.4, seed: seed + 7,
    };

    // Outer frame — slightly inset so rough wobble stays within SVG bounds
    svg.appendChild(rc.rectangle(4, 4, win.w - 8, win.h - 8, frameOpts));
    // Titlebar divider at ~33px
    svg.appendChild(rc.line(4, 33, win.w - 4, 33, lineOpts));
  }, [win.w, win.h, win.id, win.minimized, chrome]);

  const onTitleMouseDown = (e) => {
    if (e.target.closest('.win-dot')) return;
    if (win.maximized) { onFocus(); return; }  // no drag while maximised
    e.preventDefault();
    onFocus();
    drag.current = { mx:e.clientX, my:e.clientY, ox:win.x, oy:win.y };
    setDragging(true);
  };

  useEffect(() => {
    if (!dragging) return;
    const onMove2 = (e) => {
      if (!drag.current) return;
      const dx = e.clientX - drag.current.mx;
      const dy = e.clientY - drag.current.my;
      onMove(drag.current.ox + dx, drag.current.oy + dy);
    };
    const onUp = () => { drag.current = null; setDragging(false); };
    window.addEventListener('mousemove', onMove2);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove2);
      window.removeEventListener('mouseup', onUp);
    };
  }, [dragging, onMove]);

  // Resize
  const resize = useRef(null);
  const [resizing, setResizing] = useState(false);
  const onResizeDown = (e) => {
    e.preventDefault();
    e.stopPropagation();
    onFocus();
    resize.current = { mx:e.clientX, my:e.clientY, ow:win.w, oh:win.h };
    setResizing(true);
  };
  useEffect(() => {
    if (!resizing) return;
    const onMove2 = (e) => {
      if (!resize.current) return;
      const dw = e.clientX - resize.current.mx;
      const dh = e.clientY - resize.current.my;
      onResize(Math.max(260, resize.current.ow + dw), Math.max(160, resize.current.oh + dh));
    };
    const onUp = () => { resize.current = null; setResizing(false); };
    window.addEventListener('mousemove', onMove2);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove2);
      window.removeEventListener('mouseup', onUp);
    };
  }, [resizing, onResize]);

  const stateClass = (win.minimized ? ' minimized' : '') + (win.maximized ? ' maximized' : '');

  return (
    <div
      ref={ref}
      className={'win' + (focused ? ' focused' : '') + (dragging ? ' dragging' : '') + stateClass}
      style={{ left:win.x, top:win.y, width:win.w, height:win.h, zIndex:win.z, pointerEvents:'auto' }}
      onMouseDown={onFocus}
    >
      {/* Rough.js sketch frame — pointer-events:none, above content */}
      <svg
        ref={sketchRef}
        style={{position:'absolute',inset:0,width:'100%',height:'100%',pointerEvents:'none',zIndex:10,overflow:'hidden'}}
        aria-hidden="true"
      />
      <div className="win-titlebar" onMouseDown={onTitleMouseDown}>
        <div className="win-dots">
          <button className="win-dot close" aria-label="close" onClick={(e)=>{e.stopPropagation();onClose()}}>
            <svg viewBox="0 0 10 10" width="9" height="9" aria-hidden="true"><path d="M2.2 2.2 L7.8 7.8 M7.8 2.2 L2.2 7.8" stroke="#2a0a00" strokeWidth="1.7" strokeLinecap="round"/></svg>
          </button>
          <button className="win-dot min" aria-label={win.minimized ? 'restore' : 'minimise'} onClick={(e)=>{e.stopPropagation();onMinimize()}}>
            {win.minimized
              ? <svg viewBox="0 0 10 10" width="9" height="9" aria-hidden="true"><path d="M2 7 L5 4 L8 7" stroke="#3a1a0a" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" fill="none"/></svg>
              : <svg viewBox="0 0 10 10" width="9" height="9" aria-hidden="true"><path d="M2 5 H8" stroke="#3a1a0a" strokeWidth="1.7" strokeLinecap="round"/></svg>
            }
          </button>
          <button className="win-dot max" aria-label={win.maximized ? 'restore' : 'maximise'} onClick={(e)=>{e.stopPropagation();onMaximize()}}>
            {win.maximized
              ? <svg viewBox="0 0 10 10" width="9" height="9" aria-hidden="true"><path d="M3.5 2 L8 2 L8 6.5 M2 3.5 L2 8 L6.5 8" stroke="#1f2d10" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" fill="none"/></svg>
              : <svg viewBox="0 0 10 10" width="9" height="9" aria-hidden="true"><rect x="2" y="2" width="6" height="6" fill="none" stroke="#1f2d10" strokeWidth="1.5"/></svg>
            }
          </button>
        </div>
        <div className="win-title">{win.title}</div>
        <div className="win-meta"><span className="pip"></span>{focused ? 'active' : ''}</div>
      </div>
      <div className="win-body">{children}</div>
      <div className="win-resize" onMouseDown={onResizeDown}></div>
    </div>
  );
}

// Resolve content component by name string
function WindowContent({name, api}){
  const C = window[name];
  if (!C) return <div style={{padding:18}}>missing content: {name}</div>;
  return <C api={api} />;
}

// ============================================================
// Desktop icon manifest
// type: 'folder' | 'txt' | 'term'
// ============================================================
const DESKTOP_ITEMS = [
  { id:'home',       type:'folder', label:'home',          app:'about'      },
  { id:'work',       type:'folder', label:'work',          app:'work'       },
  { id:'experience', type:'folder', label:'experience',    app:'experience' },
  { id:'writing',    type:'txt',    label:'writing.txt',   app:'writing'    },
  { id:'photos',     type:'folder', label:'photos',        app:'photos'     },
  { id:'hobbies',    type:'folder', label:'hobbies',       app:'hobbies'    },
  { id:'terminal',   type:'term',   label:'terminal',      app:'terminal'   },
  { id:'lessons',    type:'txt',    label:'genai-101.txt', app:'lessons'    },
  { id:'contact',    type:'txt',    label:'contact.txt',   app:'contact'    },
];

// ── Folder icon (terracotta, palette-aware via CSS variables)
function FolderIcon(){
  return (
    <svg viewBox="0 0 36 28" width="46" height="36" fill="none" aria-hidden="true">
      <path d="M2,10 L2,5 Q2,3 4,3 L14,3 Q17.5,3 18.5,6 L18.5,10"
        fill="var(--accent)" stroke="var(--accent-deep)" strokeWidth="1.2" strokeLinejoin="round"/>
      <rect x="2" y="10" width="32" height="16" rx="2"
        fill="var(--accent)" stroke="var(--accent-deep)" strokeWidth="1.2"/>
      <line x1="4" y1="12" x2="32" y2="12" stroke="var(--accent-light)" strokeWidth="0.8" opacity="0.35"/>
    </svg>
  );
}

// ── Plain text document icon (cream page with folded corner)
function TxtFileIcon(){
  return (
    <svg viewBox="0 0 28 34" width="36" height="44" fill="none" aria-hidden="true">
      <path d="M3,1 L20,1 L27,8 L27,33 L3,33 Z"
        fill="var(--paper)" stroke="var(--ink)" strokeWidth="1.2" strokeLinejoin="round"/>
      <path d="M20,1 L20,8 L27,8"
        fill="var(--paper-2)" stroke="var(--ink)" strokeWidth="1.2" strokeLinejoin="round"/>
      <line x1="7" y1="14" x2="23" y2="14" stroke="var(--muted)" strokeWidth="1"/>
      <line x1="7" y1="18" x2="23" y2="18" stroke="var(--muted)" strokeWidth="1"/>
      <line x1="7" y1="22" x2="18" y2="22" stroke="var(--muted)" strokeWidth="1"/>
    </svg>
  );
}

// ── Terminal icon (mini terminal window frame)
function TerminalFileIcon(){
  return (
    <svg viewBox="0 0 36 28" width="46" height="36" fill="none" aria-hidden="true">
      <rect x="2" y="2" width="32" height="24" rx="3"
        fill="var(--bg-3)" stroke="var(--moss-light)" strokeWidth="1.2"/>
      <path d="M2,2 Q2,2 5,2 L33,2 Q34,2 34,3 L34,9 L2,9 L2,3 Q2,2 2,2"
        fill="var(--surface)" stroke="var(--moss-light)" strokeWidth="1.2"/>
      <circle cx="6.5" cy="5.5" r="1.5" fill="#c06050"/>
      <circle cx="11" cy="5.5" r="1.5" fill="var(--moss-light)" opacity="0.45"/>
      <circle cx="15.5" cy="5.5" r="1.5" fill="var(--moss-light)" opacity="0.45"/>
      <polyline points="7,14.5 12,18 7,21.5"
        stroke="var(--accent)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
      <line x1="15" y1="21.5" x2="22" y2="21.5"
        stroke="var(--moss-light)" strokeWidth="1.5" strokeLinecap="round"/>
    </svg>
  );
}

// ============================================================
// Desktop icons — fixed column on the left, above wallpaper
// ============================================================
function DesktopIcons({ openApp, openIds }){
  return (
    <div className="desktop-icons" onMouseDown={(e)=>e.stopPropagation()}>
      {DESKTOP_ITEMS.map(item => (
        <button
          key={item.id}
          className={'di' + (openIds.includes(item.app) ? ' open' : '')}
          onClick={() => openApp(item.app)}
          title={item.label}
        >
          <div className="di-icon">
            {item.type === 'folder' ? <FolderIcon />
              : item.type === 'term' ? <TerminalFileIcon />
              : <TxtFileIcon />}
          </div>
          <span className="di-label">{item.label}</span>
        </button>
      ))}
    </div>
  );
}

// ============================================================
// Dock — horizontal bottom bar (replaces left sidebar)
// ============================================================
function Dock({ openApp, openIds }){
  return (
    <div className="dock" onMouseDown={(e)=>e.stopPropagation()}>
      <div className="dock-inner">
        {APPS.filter(a => !a.bottom).map(a => (
          <button
            key={a.app}
            className={'dock-item' + (openIds.includes(a.app) ? ' open' : '')}
            onClick={() => openApp(a.app)}
            title={a.title}
          >
            {APP_ICONS[a.app] || <span aria-hidden>{a.glyph}</span>}
          </button>
        ))}
        <div className="dock-sep" aria-hidden="true"/>
        {APPS.filter(a => a.bottom).map(a => (
          <button
            key={a.app}
            className={'dock-item' + (openIds.includes(a.app) ? ' open' : '')}
            onClick={() => openApp(a.app)}
            title={a.title}
          >
            {APP_ICONS[a.app] || <span aria-hidden>{a.glyph}</span>}
          </button>
        ))}
      </div>
    </div>
  );
}

// ============================================================
// Menu Bar
// ============================================================
function MenuBar({focused, flash, onDotClick, openApp, windows, tweaks, setTweak, onMinimizeAll, onCloseAll, onRestoreAll, onTileWindows, onFocusWindow}){
  const [now, setNow] = useState(() => formatNow());
  const [openMenu, setOpenMenu] = useState(null);

  useEffect(() => {
    const i = setInterval(() => setNow(formatNow()), 30000);
    return () => clearInterval(i);
  }, []);

  // Close dropdown on click outside the menu bar
  useEffect(() => {
    if (!openMenu) return;
    const h = (e) => { if (!e.target.closest('.menubar')) setOpenMenu(null); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [openMenu]);

  const toggle = (name) => setOpenMenu(m => m === name ? null : name);
  const act = (fn) => { setOpenMenu(null); fn(); };

  const crumb = useMemo(() => {
    const f = APPS.find(a => a.app === focused);
    return f ? f.title.replace(/^\d+\s*—\s*/, '').replace(/^[~@]\s*/, '') : '—';
  }, [focused]);

  return (
    <div className="menubar">
      <div className="logo"><span className="dot"></span><span>jj.dev</span></div>

      {/* ── File ── */}
      <div className="menu-wrap">
        <button className={'item' + (openMenu === 'file' ? ' open' : '')} onClick={() => toggle('file')}>file</button>
        {openMenu === 'file' && (
          <div className="menu-dropdown">
            <button className="mi" onClick={() => act(() => APPS.forEach(a => openApp(a.app)))}>open all windows</button>
            <div className="sep"/>
            <button className="mi" onClick={() => act(onRestoreAll)}>restore all</button>
            <button className="mi" onClick={() => act(onMinimizeAll)}>minimise all</button>
            <button className="mi" onClick={() => act(onCloseAll)}>close all windows</button>
            <div className="sep"/>
            <a className="mi" href="cv.pdf" download>download cv →</a>
          </div>
        )}
      </div>

      {/* ── View ── */}
      <div className="menu-wrap">
        <button className={'item' + (openMenu === 'view' ? ' open' : '')} onClick={() => toggle('view')}>view</button>
        {openMenu === 'view' && (
          <div className="menu-dropdown">
            <div className="mi-label">palette</div>
            {['olive','sun-baked','forest','studio'].map(v => (
              <button key={v} className={'mi' + (tweaks.palette === v ? ' check' : '')} onClick={() => act(() => setTweak('palette', v))}>{v}</button>
            ))}
            <div className="sep"/>
            <div className="mi-label">backdrop</div>
            {['donut','tiles','quiet'].map(v => (
              <button key={v} className={'mi' + (tweaks.backdrop === v ? ' check' : '')} onClick={() => act(() => setTweak('backdrop', v))}>{v}</button>
            ))}
            <div className="sep"/>
            <div className="mi-label">chrome</div>
            {['modern','retro','sketchy'].map(v => (
              <button key={v} className={'mi' + (tweaks.chrome === v ? ' check' : '')} onClick={() => act(() => setTweak('chrome', v))}>{v}</button>
            ))}
          </div>
        )}
      </div>

      {/* ── Window ── */}
      <div className="menu-wrap">
        <button className={'item' + (openMenu === 'window' ? ' open' : '')} onClick={() => toggle('window')}>window</button>
        {openMenu === 'window' && (
          <div className="menu-dropdown">
            <button className="mi" onClick={() => act(onTileWindows)}>tile all</button>
            <button className="mi" onClick={() => act(onRestoreAll)}>restore all</button>
            <button className="mi" onClick={() => act(onMinimizeAll)}>minimise all</button>
            {windows.length > 0 && <div className="sep"/>}
            {windows.map(w => (
              <button
                key={w.id}
                className={'mi' + (w.app === focused || w.id === focused ? ' check' : '')}
                onClick={() => act(() => onFocusWindow(w.id))}
              >{w.title}</button>
            ))}
          </div>
        )}
      </div>

      <span className="item crumb">› {crumb}</span>

      <div className="right">
        <button className="item term-quick" onClick={() => openApp('terminal')} title="open terminal">&gt;_</button>
        <button className={'status' + (flash ? ' flash' : '')} onClick={onDotClick}>
          <span className="bdot"></span>
          <span>curious · open to talk</span>
        </button>
        <span className="clock">{now}</span>
      </div>
    </div>
  );
}

function formatNow(){
  const d = new Date();
  const day = d.toLocaleDateString('en-GB', { weekday:'short', day:'2-digit', month:'short' });
  const time = d.toLocaleTimeString('en-GB', { hour:'2-digit', minute:'2-digit' });
  return `${day} · ${time} CET`;
}

// ============================================================
// Cursor — precise dot, magnetic visual ring, ink ripple on click
// Position is always the EXACT mouse position (no lag, no pull).
// The "magnetic" effect is purely visual: the ring grows/changes
// when near interactive elements, but the hotspot never shifts.
// ============================================================
function Cursor(){
  const cursorRef = useRef(null);
  const [ripples, setRipples] = useState([]);

  useEffect(() => {
    const INTERACTIVE = 'button, a, .filelist .row, .navi, .photos .ph, .menubar .item, .status, .hobby';
    const RADIUS = 80; // px: distance at which ring appears

    function findMagnet(mx, my){
      const els = document.querySelectorAll(INTERACTIVE);
      for (const el of els){
        const r = el.getBoundingClientRect();
        if (!r.width) continue;
        const d = Math.hypot(mx - (r.left + r.width/2), my - (r.top + r.height/2));
        if (d < RADIUS) return true;
      }
      return false;
    }

    const onMove = (e) => {
      const el = cursorRef.current;
      if (!el) return;
      // -15 centres the 30px container on the exact mouse position
      el.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`;
      const isHov  = !!e.target.closest(INTERACTIVE);
      const isNear = !isHov && findMagnet(e.clientX, e.clientY);
      el.className = 'cursor' + (isHov ? ' hov' : isNear ? ' near' : '');
    };

    const onDown = (e) => {
      setRipples(rs => [...rs, { id: Date.now() + Math.random(), x: e.clientX, y: e.clientY }]);
    };

    window.addEventListener('mousemove', onMove);
    window.addEventListener('mousedown', onDown);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mousedown', onDown);
    };
  }, []);

  return (
    <>
      <div ref={cursorRef} className="cursor" />
      {ripples.map(r => (
        <div
          key={r.id}
          className="cursor-ripple"
          style={{ left: r.x, top: r.y }}
          onAnimationEnd={() => setRipples(rs => rs.filter(x => x.id !== r.id))}
        />
      ))}
    </>
  );
}

// ============================================================
// Donut canvas — ASCII torus in the bg, drag-rotatable
// ============================================================

// Per-palette color ramps: bg = canvas clear color, dim = shadow side char, bright = highlight char
const DONUT_PALETTES = {
  olive:        { bg:[58,69,37],    dim:[58,69,37],   bright:[192,98,58]  },
  'sun-baked':  { bg:[140,65,40],   dim:[140,65,40],  bright:[90,106,58]  },
  forest:       { bg:[31,45,16],    dim:[31,45,16],   bright:[94,156,0]   },
  studio:       { bg:[230,220,186], dim:[150,135,95], bright:[192,98,58]  },
};

function DonutCanvas({ palette }){
  const canvasRef = useRef(null);
  const stateRef = useRef({ A: 0, B: 0, isDragging: false, lastX:0, lastY:0 });

  useEffect(() => {
    const cv = canvasRef.current;
    if (!cv) return;
    const ctx = cv.getContext('2d');
    let raf;
    const state = stateRef.current;
    const pal = DONUT_PALETTES[palette] || DONUT_PALETTES.olive;

    function resize(){
      cv.width = window.innerWidth;
      cv.height = window.innerHeight;
    }
    resize();
    window.addEventListener('resize', resize);

    const CHARS = ' .,-~:;=+*#$@';
    function render(){
      const W = cv.width, H = cv.height;
      const cellW = 7, cellH = 12;
      const cols = Math.floor(W / cellW);
      const rows = Math.floor(H / cellH);
      const zBuf = new Float32Array(cols * rows);
      const cBuf = new Int8Array(cols * rows);
      zBuf.fill(0); cBuf.fill(-1);

      // Visible desktop area: full width, between menubar(30) and dock(58)
      const K1 = Math.min(W, H - 30 - 58) * 0.32 / cellW;
      const K2 = 5;
      const R1 = 1, R2 = 2;

      for (let theta = 0; theta < 6.28; theta += 0.07) {
        for (let phi = 0; phi < 6.28; phi += 0.02) {
          const ct = Math.cos(theta), st = Math.sin(theta);
          const cp = Math.cos(phi),   sp = Math.sin(phi);
          const cA = Math.cos(state.A), sA = Math.sin(state.A);
          const cB = Math.cos(state.B), sB = Math.sin(state.B);

          const circX = R2 + R1 * ct;
          const circY = R1 * st;
          const x = circX * (cB*cp + sA*sB*sp) - circY*cA*sB;
          const y = circX * (sB*cp - sA*cB*sp) + circY*cA*cB;
          const z = K2 + cA*circX*sp + circY*sA;
          const ooz = 1/z;

          // Centre on the visible desktop area (between menubar and dock)
          const menubarRows = Math.round(30 / cellH);
          const dockRows    = Math.round(58 / cellH);
          const xp = Math.floor(cols / 2 + K1*ooz*x);
          const yp = Math.floor((rows + menubarRows - dockRows) / 2 - K1*ooz*y * (cellW/cellH));

          const L = cp*ct*sB - cA*ct*sp - sA*st + cB*(cA*st - ct*sA*sp);

          if (yp >= 0 && yp < rows && xp >= 0 && xp < cols && L > 0) {
            const idx = xp + cols*yp;
            if (ooz > zBuf[idx]) {
              zBuf[idx] = ooz;
              cBuf[idx] = Math.floor(L * 8);
            }
          }
        }
      }

      // Clear to palette background
      ctx.fillStyle = `rgba(${pal.bg[0]},${pal.bg[1]},${pal.bg[2]},0.96)`;
      ctx.fillRect(0, 0, W, H);

      ctx.font = `400 10.5px 'Martian Mono', monospace`;
      ctx.textBaseline = 'top';

      for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
          const idx = c + cols*r;
          const charIdx = cBuf[idx];
          if (charIdx < 0) continue;
          const ch = CHARS[Math.min(charIdx, CHARS.length-1)];
          const t = Math.min(1, charIdx/11);
          // Interpolate dim → bright across the palette ramp
          const rr = Math.floor(pal.dim[0] + (pal.bright[0] - pal.dim[0]) * t);
          const gg = Math.floor(pal.dim[1] + (pal.bright[1] - pal.dim[1]) * t);
          const bb = Math.floor(pal.dim[2] + (pal.bright[2] - pal.dim[2]) * t);
          ctx.fillStyle = `rgba(${rr},${gg},${bb},${0.55 + 0.4*t})`;
          ctx.fillText(ch, c*cellW, r*cellH);
        }
      }
    }

    function loop(){
      if (!state.isDragging) {
        state.A += 0.004;
        state.B += 0.008;
      }
      render();
      raf = requestAnimationFrame(loop);
    }
    loop();

    const onDown = (e) => {
      if (e.target !== cv) return;
      state.isDragging = true;
      state.lastX = e.clientX;
      state.lastY = e.clientY;
    };
    const onMove = (e) => {
      if (!state.isDragging) return;
      const dx = e.clientX - state.lastX;
      const dy = e.clientY - state.lastY;
      state.B += dx * 0.005;
      state.A -= dy * 0.005;
      state.lastX = e.clientX;
      state.lastY = e.clientY;
    };
    const onUp = () => { state.isDragging = false; };

    cv.addEventListener('mousedown', onDown);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);

    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('resize', resize);
      cv.removeEventListener('mousedown', onDown);
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [palette]); // re-run whenever palette changes

  return <canvas ref={canvasRef} className="donut"></canvas>;
}

// ============================================================
// Donut Takeover (konami easter)
// ============================================================
function DonutTakeover({ palette, onClose }){
  const ref = useRef(null);
  useEffect(() => {
    const c = ref.current;
    if (!c) return;
    const ctx = c.getContext('2d');
    let A=0, B=0, raf;
    const CHARS = ' .,-~:;=+*#$@';
    const pal = DONUT_PALETTES[palette] || DONUT_PALETTES.olive;
    // Dramatic near-black tinted to the palette bg (multiply by 0.15)
    const dk = pal.bg.map(v => Math.max(4, Math.floor(v * 0.15)));

    function resize(){ c.width = window.innerWidth; c.height = window.innerHeight; }
    resize();
    window.addEventListener('resize', resize);

    function draw(){
      const W = c.width, H = c.height;
      const cellW = 9, cellH = 14;
      const cols = Math.floor(W/cellW), rows = Math.floor(H/cellH);
      const zBuf = new Float32Array(cols*rows); const cBuf = new Int8Array(cols*rows);
      zBuf.fill(0); cBuf.fill(-1);
      const K1 = Math.min(W,H)*0.36/cellW, K2 = 5, R1=1, R2=2;
      for (let t=0;t<6.28;t+=0.06){
        for (let p=0;p<6.28;p+=0.018){
          const ct=Math.cos(t), st=Math.sin(t), cp=Math.cos(p), sp=Math.sin(p);
          const cA=Math.cos(A), sA=Math.sin(A), cB=Math.cos(B), sB=Math.sin(B);
          const cx=R2+R1*ct, cy=R1*st;
          const x=cx*(cB*cp+sA*sB*sp)-cy*cA*sB;
          const y=cx*(sB*cp-sA*cB*sp)+cy*cA*cB;
          const z=K2+cA*cx*sp+cy*sA;
          const ooz=1/z;
          const xp=Math.floor(cols/2+K1*ooz*x);
          const yp=Math.floor(rows/2-K1*ooz*y*(cellW/cellH));
          const L=cp*ct*sB-cA*ct*sp-sA*st+cB*(cA*st-ct*sA*sp);
          if (yp>=0&&yp<rows&&xp>=0&&xp<cols&&L>0){
            const idx=xp+cols*yp;
            if (ooz>zBuf[idx]){ zBuf[idx]=ooz; cBuf[idx]=Math.floor(L*8); }
          }
        }
      }
      ctx.fillStyle=`rgba(${dk[0]},${dk[1]},${dk[2]},1)`;
      ctx.fillRect(0,0,W,H);
      ctx.font="400 12px 'Martian Mono', monospace";
      ctx.textBaseline='top';
      for (let r=0;r<rows;r++){
        for (let cc=0;cc<cols;cc++){
          const idx=cc+cols*r;
          const ci=cBuf[idx]; if (ci<0) continue;
          const ch=CHARS[Math.min(ci, CHARS.length-1)];
          const tt=Math.min(1,ci/11);
          const rr=Math.floor(pal.dim[0]+(pal.bright[0]-pal.dim[0])*tt);
          const gg=Math.floor(pal.dim[1]+(pal.bright[1]-pal.dim[1])*tt);
          const bb=Math.floor(pal.dim[2]+(pal.bright[2]-pal.dim[2])*tt);
          ctx.fillStyle=`rgba(${rr},${gg},${bb},${0.75+0.25*tt})`;
          ctx.fillText(ch, cc*cellW, r*cellH);
        }
      }
      A+=0.012; B+=0.022;
      raf=requestAnimationFrame(draw);
    }
    draw();
    return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', resize); };
  }, [palette]); // re-run when palette changes

  return (
    <div className="donut-modal" onClick={onClose}>
      <canvas ref={ref} style={{position:'absolute',inset:0,width:'100%',height:'100%'}}></canvas>
      <div className="esc">● donut mode — press esc / click to exit</div>
    </div>
  );
}

// ============================================================
// DesktopPlant — Easter egg, bottom-right corner
// Click 30 times to watch it grow and bloom 🌸
// ============================================================
function Flower({ cx, cy, r, petalColor }) {
  // Each petal: portrait ellipse centered just above (cx, cy), rotated around (cx, cy)
  return (
    <g>
      {[0, 72, 144, 216, 288].map((deg, i) => (
        <ellipse
          key={i}
          cx={cx}
          cy={cy - r * 0.62}
          rx={r * 0.36}
          ry={r * 0.60}
          fill={petalColor}
          opacity="0.92"
          transform={`rotate(${deg}, ${cx}, ${cy})`}
        />
      ))}
      <circle cx={cx} cy={cy} r={r * 0.30} fill="#f5d46a" />
    </g>
  );
}

function DesktopPlant() {
  const [grow, setGrow] = useState(0);
  const [showHint, setShowHint] = useState(false);
  const [shake, setShake]   = useState(false);
  const hintTimer  = useRef(null);
  const shakeTimer = useRef(null);

  const onClick = () => {
    if (grow >= 30) return;
    const next = grow + 1;
    setGrow(next);

    // brief shake feedback
    setShake(true);
    clearTimeout(shakeTimer.current);
    shakeTimer.current = setTimeout(() => setShake(false), 320);

    // show progress counter then fade
    setShowHint(true);
    clearTimeout(hintTimer.current);
    hintTimer.current = setTimeout(() => setShowHint(false), 1600);
  };

  const bloomed = grow >= 30;

  return (
    <div
      className={
        'desktop-plant' +
        (bloomed ? ' bloomed' : '') +
        (shake    ? ' shake'   : '')
      }
      onClick={onClick}
      title={bloomed ? '🌸 fully bloomed!' : `nurture me (${grow}/30)`}
    >
      {/* Progress hint or bloom badge */}
      {showHint && !bloomed && (
        <div className="plant-hint">{grow} / 30</div>
      )}
      {bloomed && (
        <div className="plant-hint bloom-badge">🌸</div>
      )}

      <svg viewBox="0 0 80 130" width="80" height="130" fill="none" aria-hidden="true">

        {/* ── Always visible: pot ─────────────────────────────── */}
        <path d="M22 108 L58 108 L54 124 L26 124 Z"
          fill="var(--accent)" stroke="var(--accent-deep)" strokeWidth="1.3" strokeLinejoin="round"/>
        {/* pot rim */}
        <path d="M17 108 Q40 104 63 108"
          stroke="var(--accent-deep)" strokeWidth="1.8" strokeLinecap="round" fill="none"/>
        {/* soil */}
        <ellipse cx="40" cy="108" rx="21" ry="3.5"
          fill="var(--bg-2)" opacity="0.85"/>

        {/* ── Tier 1 (≥1): short stem ─────────────────────────── */}
        {grow >= 1 && (
          <path d="M40 108 Q39 100 40 90"
            stroke="var(--moss-light)" strokeWidth="2.4" strokeLinecap="round" fill="none"/>
        )}

        {/* ── Tier 1b (≥3): tiny first leaf at base ───────────── */}
        {grow >= 3 && (
          <g>
            <path d="M40 102 Q48 96 54 90"
              stroke="var(--moss-light)" strokeWidth="1.2" strokeLinecap="round" fill="none"/>
            <ellipse cx="53" cy="90" rx="5.5" ry="2.2"
              fill="var(--bg-2)" stroke="var(--moss-light)" strokeWidth="0.9"
              transform="rotate(-42 53 90)"/>
          </g>
        )}

        {/* ── Tier 2 (≥5): stem extends ───────────────────────── */}
        {grow >= 5 && (
          <path d="M40 90 Q41 76 40 62"
            stroke="var(--moss-light)" strokeWidth="2.2" strokeLinecap="round" fill="none"/>
        )}

        {/* ── Tier 2b (≥7): right low leaf ────────────────────── */}
        {grow >= 7 && (
          <g>
            <path d="M40 82 Q50 74 58 70"
              stroke="var(--moss-light)" strokeWidth="1.2" strokeLinecap="round" fill="none"/>
            <ellipse cx="57" cy="70" rx="6" ry="2.4"
              fill="var(--bg-2)" stroke="var(--moss-light)" strokeWidth="0.9"
              transform="rotate(22 57 70)"/>
          </g>
        )}

        {/* ── Tier 3 (≥10): left main branch + leaf ───────────── */}
        {grow >= 10 && (
          <g>
            <path d="M40 74 Q26 64 16 54"
              stroke="var(--moss-light)" strokeWidth="1.9" strokeLinecap="round" fill="none"/>
            <ellipse cx="17" cy="55" rx="8" ry="3"
              fill="var(--bg-2)" stroke="var(--moss-light)" strokeWidth="0.9"
              transform="rotate(-36 17 55)"/>
          </g>
        )}

        {/* ── Tier 3b (≥12): left low leaf ────────────────────── */}
        {grow >= 12 && (
          <g>
            <path d="M40 78 Q30 70 22 68"
              stroke="var(--moss-light)" strokeWidth="1.1" strokeLinecap="round" fill="none"/>
            <ellipse cx="23" cy="68" rx="6" ry="2.4"
              fill="var(--bg-2)" stroke="var(--moss-light)" strokeWidth="0.9"
              transform="rotate(-22 23 68)"/>
          </g>
        )}

        {/* ── Tier 4 (≥15): right main branch + leaf ──────────── */}
        {grow >= 15 && (
          <g>
            <path d="M40 66 Q54 56 64 46"
              stroke="var(--moss-light)" strokeWidth="1.9" strokeLinecap="round" fill="none"/>
            <ellipse cx="63" cy="47" rx="8" ry="3"
              fill="var(--bg-2)" stroke="var(--moss-light)" strokeWidth="0.9"
              transform="rotate(30 63 47)"/>
          </g>
        )}

        {/* ── Tier 4b (≥17): stem extends higher ──────────────── */}
        {grow >= 17 && (
          <path d="M40 62 Q39 50 40 40"
            stroke="var(--moss-light)" strokeWidth="2" strokeLinecap="round" fill="none"/>
        )}

        {/* ── Tier 5 (≥20): upper-left branch + leaf ──────────── */}
        {grow >= 20 && (
          <g>
            <path d="M40 50 Q29 40 20 32"
              stroke="var(--moss-light)" strokeWidth="1.6" strokeLinecap="round" fill="none"/>
            <ellipse cx="21" cy="33" rx="6.5" ry="2.4"
              fill="var(--bg-2)" stroke="var(--moss-light)" strokeWidth="0.9"
              transform="rotate(-32 21 33)"/>
          </g>
        )}

        {/* ── Tier 5b (≥22): upper-right branch + leaf ────────── */}
        {grow >= 22 && (
          <g>
            <path d="M40 46 Q51 36 60 28"
              stroke="var(--moss-light)" strokeWidth="1.6" strokeLinecap="round" fill="none"/>
            <ellipse cx="59" cy="29" rx="6.5" ry="2.4"
              fill="var(--bg-2)" stroke="var(--moss-light)" strokeWidth="0.9"
              transform="rotate(34 59 29)"/>
          </g>
        )}

        {/* ── Tier 6 (≥25): top stem tip + buds appear ────────── */}
        {grow >= 25 && (
          <g>
            <path d="M40 40 Q40 30 40 20"
              stroke="var(--moss-light)" strokeWidth="1.8" strokeLinecap="round" fill="none"/>
            {/* buds — small warm circles */}
            <circle cx="40" cy="19" r="3.2" fill="#c8a040" opacity="0.9"/>
            <circle cx="17" cy="53" r="2.6" fill="#c8a040" opacity="0.85"/>
            <circle cx="63" cy="45" r="2.6" fill="#c8a040" opacity="0.85"/>
          </g>
        )}

        {/* ── Tier 6b (≥28): more buds ─────────────────────────── */}
        {grow >= 28 && (
          <g>
            <circle cx="21" cy="32" r="2.2" fill="#c8a040" opacity="0.85"/>
            <circle cx="59" cy="28" r="2.2" fill="#c8a040" opacity="0.85"/>
          </g>
        )}

        {/* ── BLOOM (≥30): flowers everywhere ─────────────────── */}
        {grow >= 30 && (
          <g className="plant-flowers">
            <Flower cx={40}  cy={19} r={9}   petalColor="#e89ab8" />
            <Flower cx={17}  cy={53} r={7.5} petalColor="#b8a8e8" />
            <Flower cx={63}  cy={45} r={7.5} petalColor="#e8c488" />
            <Flower cx={21}  cy={32} r={6}   petalColor="#98d4a8" />
            <Flower cx={59}  cy={28} r={6}   petalColor="#e898a8" />
          </g>
        )}

      </svg>
    </div>
  );
}

// ============================================================
// Lightbox for photo gallery
// ============================================================
function Lightbox({photo, onClose}){
  return (
    <div className="lightbox" onClick={onClose}>
      <div className="lb-img">{photo.svg}</div>
      <div className="lb-cap">{photo.tag} — {photo.yr} · {photo.caption}</div>
      <div className="lb-esc">esc to close</div>
    </div>
  );
}

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