// Penrose v4 MVP — Object of the Day.
//
// A low-chrome calming mosaic surface integrated into v4. The surface is
// deterministic by UTC day, supports the last 5 daily objects, and stores a
// small favorites shelf locally.

const {
  useState: useObjState,
  useEffect: useObjEffect,
  useRef: useObjRef,
  useMemo: useObjMemo,
  useCallback: useObjCallback,
} = React;

const OBJ_STORAGE_KEY = 'penrose-v4-object';
const OBJ_STORAGE_VERSION = 1;
const OBJ_MAX_FAVORITES = 3;
const OBJ_RIPPLE_STEP_MS = 55;
const OBJ_RIPPLE_PEAK_MS = 280;
const OBJ_RIPPLE_MAX_DEPTH = 22;
const OBJ_DRAG_RIPPLE_DEPTH = 8;
const OBJ_DRAG_RIPPLE_INTERVAL_MS = 70;
const OBJ_DRAG_RIPPLE_MIN_DIST = 22;
const OBJ_LONG_PRESS_MS = 480;
const OBJ_LONG_PRESS_CANCEL_DIST = 30;
const OBJ_TILING_RADIUS = 58;
const OBJ_FIT_MARGIN = 0.86;
const OBJ_DEFAULT_DISORDER = 0.34;
const OBJ_AMBIENT_AMPLITUDE = 0.05;
const OBJ_AMBIENT_PERIOD_MS = 5800;
const OBJ_MULTITAP_BOOST = 0.28;
const OBJ_MULTITAP_DECAY_PER_S = 0.55;
const OBJ_EDGE_THRESHOLD = 0.55;
const OBJ_IDLE_MS = 8000;
const OBJ_IDLE_PULSE_INTERVAL_MS = 5200;
const OBJ_BUILD_STEP_MS = 9;
const OBJ_BUILD_FADE_MS = 140;

function objHaptic(ms = 8) {
  if (navigator.vibrate) {
    try { navigator.vibrate(ms); } catch (e) {}
  }
}

function objDefaultState() {
  return { version: OBJ_STORAGE_VERSION, last: null, favorites: [] };
}

function normalizeObjectState(raw) {
  const base = objDefaultState();
  if (!raw || typeof raw !== 'object') return base;
  return {
    version: OBJ_STORAGE_VERSION,
    last: raw.last && typeof raw.last === 'object' ? raw.last : null,
    favorites: Array.isArray(raw.favorites) ? raw.favorites.slice(0, OBJ_MAX_FAVORITES) : [],
  };
}

function loadObjectState() {
  try {
    const raw = localStorage.getItem(OBJ_STORAGE_KEY);
    if (!raw) return objDefaultState();
    return normalizeObjectState(JSON.parse(raw));
  } catch (e) {
    return objDefaultState();
  }
}

function saveObjectState(state) {
  const next = normalizeObjectState(state);
  try { localStorage.setItem(OBJ_STORAGE_KEY, JSON.stringify(next)); } catch (e) {}
  return next;
}

function storeLastObject(preset) {
  const current = loadObjectState();
  return saveObjectState({ ...current, last: preset });
}

function addObjectFavorite(preset) {
  const current = loadObjectState();
  const id = objectPresetId(preset);
  const favorite = { ...preset, favoriteId: id, savedAt: new Date().toISOString() };
  const favorites = [favorite, ...current.favorites.filter(f => objectPresetId(f) !== id)]
    .slice(0, OBJ_MAX_FAVORITES);
  return saveObjectState({ ...current, last: preset, favorites });
}

function objectPresetId(preset) {
  if (!preset) return '';
  return [
    preset.date || 'freeform',
    preset.seedLabel || preset.patternId || 'seed',
    (preset.pattern || 0).toFixed(6),
    Number(preset.disorder || 0).toFixed(2),
    preset.paletteId || 'palette',
  ].join(':');
}

function objectRandom(seed) {
  let h = seed >>> 0;
  return function next() {
    h += 0x6D2B79F5;
    let t = h;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function objectDefaultDisorder(seed) {
  const rand = objectRandom(((seed || 1) ^ 0xD1507EAD) >>> 0);
  return 0.22 + rand() * 0.34;
}

function objectOffsetsFromSeed(seed, pattern, disorder = OBJ_DEFAULT_DISORDER) {
  const rand = objectRandom(((seed || 1) ^ 0x9E3779B9) >>> 0);
  const d = Math.max(0, Math.min(1, disorder));
  return Array.from({ length: 5 }, () => (pattern + d * (rand() - 0.5) + 1) % 1);
}

function objectPresetOffsets(preset) {
  if (preset && Array.isArray(preset.offsets) && preset.offsets.length === 5) return preset.offsets;
  return objectOffsetsFromSeed(preset?.seed || 1, preset?.pattern || 0.5, preset?.disorder ?? OBJ_DEFAULT_DISORDER);
}

function objectOffsetsKey(preset) {
  return objectPresetOffsets(preset).map(v => v.toFixed(5)).join(',');
}

function objectPresetWithDisorder(preset, disorder) {
  const nextDisorder = Math.max(0, Math.min(1, disorder));
  return {
    ...preset,
    disorder: nextDisorder,
    offsets: objectOffsetsFromSeed(preset.seed || 1, preset.pattern || 0.5, nextDisorder),
  };
}

function objectPresetFromDaily(daily) {
  const palette = window.PenrosePalettes.getDailyPalette(daily.date);
  const disorder = objectDefaultDisorder(daily.seed);
  return {
    source: 'daily',
    date: daily.date,
    ymd: daily.ymd,
    seed: daily.seed,
    pattern: daily.pattern,
    patternId: daily.patternId,
    seedLabel: daily.patternId,
    paletteId: palette.id,
    disorder,
    offsets: objectOffsetsFromSeed(daily.seed, daily.pattern, disorder),
  };
}

function objectFreeformPreset() {
  const seed = Math.floor(Math.random() * 0xffffffff) >>> 0;
  const pattern = 0.05 + ((seed % 1000000) / 1000000) * 0.9;
  const active = window.PenrosePalettes.active();
  const disorder = 0.42;
  return {
    source: 'freeform',
    date: null,
    ymd: null,
    seed,
    pattern,
    patternId: seed.toString(16).toUpperCase().padStart(8, '0').slice(0, 4),
    seedLabel: seed.toString(16).toUpperCase().padStart(8, '0').slice(0, 4),
    paletteId: active.id,
    disorder,
    offsets: objectOffsetsFromSeed(seed, pattern, disorder),
  };
}

function objectDateDaysAgo(daysBack) {
  const now = new Date();
  return new Date(Date.UTC(
    now.getUTCFullYear(),
    now.getUTCMonth(),
    now.getUTCDate() - daysBack,
    0, 0, 0, 0
  ));
}

function objectHistoryPresets() {
  return Array.from({ length: 5 }, (_, i) => objectPresetFromDaily(
    window.PenroseDaily.getDaily(objectDateDaysAgo(i))
  ));
}

function objectBfsDepths(tiles, seedId, maxDepth) {
  const depths = new Map([[seedId, 0]]);
  const queue = [seedId];
  while (queue.length) {
    const id = queue.shift();
    const d = depths.get(id);
    if (d >= maxDepth) continue;
    for (const n of tiles[id].neighbors) {
      if (!depths.has(n)) {
        depths.set(n, d + 1);
        queue.push(n);
      }
    }
  }
  return depths;
}

function objectHitTile(tiles, worldX, worldY) {
  for (let i = 0; i < tiles.length; i++) {
    if (window.PenroseGeo.pointInPoly(worldX, worldY, tiles[i].verts)) return i;
  }
  return -1;
}

function ObjectPreview({ preset, size = 74, selected = false }) {
  const paletteEntry = window.PenrosePalettes.getById(preset.paletteId) || window.PenrosePalettes.active();
  const palette = paletteEntry.palette;
  const offsetsKey = objectOffsetsKey(preset);
  const tiles = useObjMemo(() => window.PenroseGeo.generateTiling({
    radius: 9,
    pattern: preset.pattern,
    offsets: objectPresetOffsets(preset),
  }), [preset.pattern, offsetsKey]);
  const r = useObjMemo(() => {
    let max = 1;
    for (const tile of tiles) {
      for (const v of tile.verts) max = Math.max(max, Math.hypot(v.x, v.y));
    }
    return max;
  }, [tiles]);
  const scale = (size * 0.43) / r;
  const cx = size / 2, cy = size / 2;
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ display: 'block' }}>
      <rect x="0" y="0" width={size} height={size} rx="14" fill={palette.bgPanel || palette.bg} />
      {tiles.slice(0, 120).map(t => {
        const d = t.verts.map((v, i) => {
          const x = cx + v.x * scale;
          const y = cy - v.y * scale;
          return (i === 0 ? 'M' : 'L') + x.toFixed(1) + ' ' + y.toFixed(1);
        }).join(' ') + ' Z';
        return <path key={t.id} d={d}
          fill={t.type === 'thick' ? palette.thick : palette.thin}
          stroke={t.type === 'thick' ? palette.thickEdge : palette.thinEdge}
          strokeWidth={selected ? 0.5 : 0.35}
          opacity={selected ? 0.95 : 0.62} />;
      })}
      <rect x="0.5" y="0.5" width={size - 1} height={size - 1} rx="14"
        fill="none" stroke={selected ? palette.glow : palette.outlineStrong} strokeWidth={selected ? 2 : 1} />
    </svg>
  );
}

function ObjectCanvas({ preset, palette, bridge, ripples, time, multiTapEnergy, buildStartedAt }) {
  const canvasRef = useObjRef(null);
  const sizeRef = useObjRef({ w: 0, h: 0, scale: 1, dpr: 1 });
  const offsetsKey = objectOffsetsKey(preset);

  const { tiles, boundRadius, buildDepths } = useObjMemo(() => {
    const t = window.PenroseGeo.generateTiling({
      radius: OBJ_TILING_RADIUS,
      pattern: preset.pattern,
      offsets: objectPresetOffsets(preset),
    });
    let r = 0;
    for (const tile of t) {
      for (const v of tile.verts) r = Math.max(r, Math.hypot(v.x, v.y));
    }
    const centre = window.PenroseGeo.findCenterTile(t);
    return {
      tiles: t,
      boundRadius: r || 1,
      buildDepths: centre >= 0 ? objectBfsDepths(t, centre, 999) : new Map(),
    };
  }, [preset.pattern, offsetsKey]);

  bridge._tiles = tiles;
  bridge._boundRadius = boundRadius;

  useObjEffect(() => {
    const c = canvasRef.current;
    if (!c) return;
    const resize = () => {
      const r = c.getBoundingClientRect();
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      c.width = r.width * dpr;
      c.height = r.height * dpr;
      c.style.width = r.width + 'px';
      c.style.height = r.height + 'px';
      const fit = (Math.min(r.width, r.height) * OBJ_FIT_MARGIN) / (boundRadius * 2);
      sizeRef.current = { w: r.width, h: r.height, scale: fit, dpr };
    };
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(c);
    return () => ro.disconnect();
  }, [boundRadius]);

  useObjEffect(() => {
    const c = canvasRef.current;
    if (!c || !tiles.length) return;
    const { w, h, scale, dpr } = sizeRef.current;
    if (!w || !h) return;
    const ctx = c.getContext('2d');
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.fillStyle = palette.bg;
    ctx.fillRect(0, 0, w, h);

    const cx = w / 2, cy = h / 2;
    const toScreen = (x, y) => [cx + x * scale, cy - y * scale];
    const ambientPhase = (time / OBJ_AMBIENT_PERIOD_MS) * Math.PI * 2;

    const energyMap = new Map();
    for (const ripple of ripples) {
      const dt = time - ripple.startedAt;
      const strength = ripple.strength != null ? ripple.strength : 1;
      for (const [tileId, depth] of ripple.depths) {
        const localT = dt - depth * OBJ_RIPPLE_STEP_MS;
        if (localT < 0 || localT > OBJ_RIPPLE_PEAK_MS) continue;
        const n = localT / OBJ_RIPPLE_PEAK_MS;
        const eased = n < 0.4 ? n / 0.4 : 1 - (n - 0.4) / 0.6;
        const v = Math.max(0, eased) * strength;
        energyMap.set(tileId, Math.min(1, (energyMap.get(tileId) || 0) + v));
      }
    }

    const sinceBuild = time - buildStartedAt;
    for (const t of tiles) {
      const buildDepth = buildDepths.get(t.id) || 0;
      const buildLocalT = sinceBuild - buildDepth * OBJ_BUILD_STEP_MS;
      if (buildLocalT < 0) continue;
      const buildRaw = Math.min(1, buildLocalT / OBJ_BUILD_FADE_MS);
      const buildProgress = 1 - Math.pow(1 - buildRaw, 3);
      const rippleE = energyMap.get(t.id) || 0;
      const ambientE = OBJ_AMBIENT_AMPLITUDE * (0.5 + 0.5 * Math.sin(ambientPhase + t.id * 0.073));
      const totalE = Math.min(1, rippleE + ambientE + multiTapEnergy * 0.10);
      const isThick = t.type === 'thick';
      const baseFill = isThick ? palette.thick : palette.thin;
      const baseEdge = isThick ? palette.thickEdge : palette.thinEdge;
      const grow = (0.65 + 0.35 * buildProgress) * (1 + totalE * 0.12);
      const tcx = t.mean.x, tcy = t.mean.y;

      ctx.beginPath();
      t.verts.forEach((v, i) => {
        const [sx, sy] = toScreen(tcx + (v.x - tcx) * grow, tcy + (v.y - tcy) * grow);
        if (i === 0) ctx.moveTo(sx, sy); else ctx.lineTo(sx, sy);
      });
      ctx.closePath();
      ctx.globalAlpha = buildProgress * (0.74 + totalE * 0.24);
      ctx.fillStyle = baseFill;
      ctx.fill();
      ctx.globalAlpha = buildProgress * (0.30 + totalE * 0.60 + multiTapEnergy * 0.10);
      ctx.lineWidth = 0.8 + totalE * 1.8;
      ctx.strokeStyle = baseEdge;
      ctx.stroke();
    }
    ctx.globalAlpha = 1;
  }, [tiles, palette, ripples, time, multiTapEnergy, buildStartedAt, buildDepths]);

  bridge._toWorld = (clientX, clientY) => {
    const c = canvasRef.current;
    const r = c.getBoundingClientRect();
    const { scale } = sizeRef.current;
    const cx = r.width / 2, cy = r.height / 2;
    const px = clientX - r.left, py = clientY - r.top;
    return { x: (px - cx) / scale, y: -(py - cy) / scale };
  };

  return (
    <div style={{
      position: 'absolute',
      top: 116,
      left: 0,
      right: 0,
      bottom: 156,
      pointerEvents: 'none',
    }}>
      <canvas ref={canvasRef} style={{
        position: 'absolute',
        inset: 0,
        width: '100%',
        height: '100%',
        display: 'block',
      }} />
    </div>
  );
}

function ObjectScreen({ goto, daily, progress }) {
  const dailyPreset = useObjMemo(() => objectPresetFromDaily(daily), [daily.date]);
  const initialState = useObjMemo(loadObjectState, []);
  const [preset, setPreset] = useObjState(() => initialState.last || dailyPreset);
  const [stored, setStored] = useObjState(initialState);
  const [ripples, setRipples] = useObjState([]);
  const [time, setTime] = useObjState(performance.now());
  const [multiTapEnergy, setMultiTapEnergy] = useObjState(0);
  const [buildStartedAt, setBuildStartedAt] = useObjState(performance.now());
  const [hint, setHint] = useObjState(true);
  const [learnPrompt, setLearnPrompt] = useObjState(false);
  const [promptDismissed, setPromptDismissed] = useObjState(false);

  const bridge = useObjRef({}).current;
  const lastInteractionRef = useObjRef(performance.now());
  const idleLastPulseRef = useObjRef(0);
  const tickRef = useObjRef({ lastFrameAt: performance.now() });
  const dragRef = useObjRef({
    active: false,
    startX: 0,
    startY: 0,
    lastSpawnAt: 0,
    lastSpawnX: 0,
    lastSpawnY: 0,
    longPressId: null,
    pointerType: 'mouse',
  });

  const history = useObjMemo(objectHistoryPresets, []);
  const paletteEntry = window.PenrosePalettes.getById(preset.paletteId) || window.PenrosePalettes.active();
  const palette = paletteEntry.palette;
  const saved = stored.favorites.some(f => objectPresetId(f) === objectPresetId(preset));
  const tutorialDone = !!progress?.tutorialCompleted;
  const disorder = preset.disorder ?? OBJ_DEFAULT_DISORDER;
  const disorderPct = Math.round(disorder * 100);

  useObjEffect(() => {
    window.PenrosePalettes.setActive(preset.paletteId);
    setStored(storeLastObject(preset));
  }, [objectPresetId(preset)]);

  useObjEffect(() => {
    setBuildStartedAt(performance.now());
    setRipples([]);
  }, [objectPresetId(preset)]);

  useObjEffect(() => {
    const t = setTimeout(() => setHint(false), 3200);
    return () => clearTimeout(t);
  }, []);

  const spawnRipple = useObjCallback((tileId, opts = {}) => {
    if (tileId < 0) return;
    const tiles = bridge._tiles;
    if (!tiles) return;
    const depth = opts.depth || OBJ_RIPPLE_MAX_DEPTH;
    const strength = opts.strength != null ? opts.strength : 1;
    const depths = objectBfsDepths(tiles, tileId, depth);
    setRipples(prev => [...prev, { seedId: tileId, depths, strength, startedAt: performance.now() }]);
  }, [bridge]);

  useObjEffect(() => {
    let raf;
    const tick = (now) => {
      const dt = (now - tickRef.current.lastFrameAt) / 1000;
      tickRef.current.lastFrameAt = now;
      setTime(now);
      setMultiTapEnergy(e => e < 0.001 ? 0 : e * Math.pow(OBJ_MULTITAP_DECAY_PER_S, dt));
      setRipples(prev => {
        const maxAge = (OBJ_RIPPLE_MAX_DEPTH + 2) * OBJ_RIPPLE_STEP_MS + OBJ_RIPPLE_PEAK_MS;
        const live = prev.filter(r => now - r.startedAt < maxAge);
        return live.length === prev.length ? prev : live;
      });
      const buildDone = now - buildStartedAt > 700;
      const idleFor = now - lastInteractionRef.current;
      if (buildDone && idleFor > OBJ_IDLE_MS && now - idleLastPulseRef.current > OBJ_IDLE_PULSE_INTERVAL_MS) {
        const tiles = bridge._tiles;
        if (tiles && tiles.length) spawnRipple(window.PenroseGeo.findCenterTile(tiles), { strength: 0.5 });
        idleLastPulseRef.current = now;
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [bridge, spawnRipple, buildStartedAt]);

  const noteInteraction = useObjCallback(() => {
    const now = performance.now();
    lastInteractionRef.current = now;
    idleLastPulseRef.current = now;
    setHint(false);
    if (!tutorialDone && !promptDismissed) setLearnPrompt(true);
  }, [tutorialDone, promptDismissed]);

  const reseed = useObjCallback((pointerType) => {
    const next = objectFreeformPreset();
    setPreset(next);
    if (pointerType === 'touch' && navigator.vibrate) navigator.vibrate([8, 40, 12]);
  }, []);

  const setDisorderValue = useObjCallback((value) => {
    setPreset(current => objectPresetWithDisorder(current, value));
  }, []);

  const onPointerDown = useObjCallback((e) => {
    noteInteraction();
    const tiles = bridge._tiles;
    const toWorld = bridge._toWorld;
    const boundRadius = bridge._boundRadius;
    if (!tiles || !toWorld) return;

    const w = toWorld(e.clientX, e.clientY);
    const seed = objectHitTile(tiles, w.x, w.y);
    if (e.pointerType === 'touch' && navigator.vibrate) navigator.vibrate(8);
    setMultiTapEnergy(prev => Math.min(1, prev + OBJ_MULTITAP_BOOST));

    if (seed >= 0) {
      spawnRipple(seed);
      if (boundRadius && Math.hypot(w.x, w.y) > OBJ_EDGE_THRESHOLD * boundRadius) {
        const counterTile = objectHitTile(tiles, -w.x, -w.y);
        if (counterTile >= 0 && counterTile !== seed) {
          setTimeout(() => spawnRipple(counterTile, { strength: 0.6 }), 90);
        }
      }
    }

    dragRef.current = {
      active: true,
      startX: e.clientX,
      startY: e.clientY,
      lastSpawnAt: performance.now(),
      lastSpawnX: e.clientX,
      lastSpawnY: e.clientY,
      longPressId: setTimeout(() => {
        reseed(e.pointerType);
        dragRef.current.longPressId = null;
      }, OBJ_LONG_PRESS_MS),
      pointerType: e.pointerType,
    };
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {}
  }, [bridge, noteInteraction, spawnRipple, reseed]);

  const onPointerMove = useObjCallback((e) => {
    const state = dragRef.current;
    if (!state.active) return;
    noteInteraction();
    const tiles = bridge._tiles;
    const toWorld = bridge._toWorld;
    if (!tiles || !toWorld) return;

    const totalMoved = Math.hypot(e.clientX - state.startX, e.clientY - state.startY);
    if (state.longPressId && totalMoved > OBJ_LONG_PRESS_CANCEL_DIST) {
      clearTimeout(state.longPressId);
      state.longPressId = null;
    }

    const moved = Math.hypot(e.clientX - state.lastSpawnX, e.clientY - state.lastSpawnY);
    const dt = performance.now() - state.lastSpawnAt;
    if (moved > OBJ_DRAG_RIPPLE_MIN_DIST && dt > OBJ_DRAG_RIPPLE_INTERVAL_MS) {
      const w2 = toWorld(e.clientX, e.clientY);
      const t2 = objectHitTile(tiles, w2.x, w2.y);
      if (t2 >= 0) {
        spawnRipple(t2, { depth: OBJ_DRAG_RIPPLE_DEPTH, strength: 0.65 });
        state.lastSpawnAt = performance.now();
        state.lastSpawnX = e.clientX;
        state.lastSpawnY = e.clientY;
      }
    }
  }, [bridge, noteInteraction, spawnRipple]);

  const onPointerUp = useObjCallback((e) => {
    const state = dragRef.current;
    if (state.longPressId) {
      clearTimeout(state.longPressId);
      state.longPressId = null;
    }
    state.active = false;
    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) {}
  }, []);

  const cyclePalette = useObjCallback(() => {
    const list = window.PenrosePalettes.list.filter(p => p.free);
    const ix = list.findIndex(p => p.id === preset.paletteId);
    const next = list[(ix + 1) % list.length];
    setPreset({ ...preset, paletteId: next.id });
    objHaptic(4);
  }, [preset]);

  const saveFavorite = useObjCallback((e) => {
    e.stopPropagation();
    setStored(addObjectFavorite(preset));
    objHaptic(12);
  }, [preset]);

  return (
    <div
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerUp}
      onPointerCancel={onPointerUp}
      style={{
        position: 'absolute',
        inset: 0,
        overflow: 'hidden',
        touchAction: 'none',
        background: palette.bg,
        color: palette.text,
        transition: 'background 360ms ease, color 360ms ease',
      }}>
      <ObjectCanvas
        preset={preset}
        palette={palette}
        bridge={bridge}
        ripples={ripples}
        time={time}
        multiTapEnergy={multiTapEnergy}
        buildStartedAt={buildStartedAt}
      />

      <div onPointerDown={(e) => e.stopPropagation()} style={{
        position: 'absolute',
        top: 64,
        left: 16,
        right: 16,
        zIndex: 10,
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        gap: 8,
        pointerEvents: 'none',
      }}>
        <button onClick={() => goto('home')} style={{ ...objectPillBtn(palette), pointerEvents: 'auto' }}>
          HOME
        </button>
        <div style={{ flex: 1, minWidth: 0, pointerEvents: 'none' }}>
          <div style={objectMetaStyle(palette)}>DAILY MOSAIC</div>
          <div style={{ ...objectSeedStyle(palette), overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            SEED {preset.seedLabel} · {preset.date || 'FREEFORM'}
          </div>
        </div>
        <button onClick={saveFavorite} style={{
          ...objectPillBtn(palette),
          pointerEvents: 'auto',
          borderColor: saved ? palette.glow : palette.outlineStrong,
          color: saved ? palette.glow : palette.text,
        }}>
          {saved ? 'SAVED' : 'SAVE'}
        </button>
        <button onClick={cyclePalette} style={{ ...objectPillBtn(palette), pointerEvents: 'auto', display: 'flex', gap: 6, alignItems: 'center' }}>
          <span style={{ width: 9, height: 9, borderRadius: 9, background: palette.thick, display: 'inline-block' }} />
          <span style={{ width: 9, height: 9, borderRadius: 9, background: palette.thin, display: 'inline-block' }} />
        </button>
      </div>

      {multiTapEnergy > 0.05 && (
        <div style={{
          position: 'absolute',
          left: '50%',
          bottom: 164,
          transform: 'translateX(-50%)',
          width: 180,
          height: 2,
          background: 'rgba(255,255,255,0.08)',
          borderRadius: 999,
          overflow: 'hidden',
          pointerEvents: 'none',
          zIndex: 8,
        }}>
          <div style={{
            width: `${Math.round(multiTapEnergy * 100)}%`,
            height: '100%',
            background: palette.glow,
            boxShadow: `0 0 10px ${palette.glow}`,
          }} />
        </div>
      )}

      {learnPrompt && !tutorialDone && (
        <div onPointerDown={(e) => e.stopPropagation()} style={{
          position: 'absolute',
          left: 18,
          right: 18,
          bottom: 172,
          zIndex: 12,
          padding: 14,
          borderRadius: 16,
          background: 'rgba(0,0,0,0.42)',
          border: `1px solid ${palette.outlineStrong}`,
          backdropFilter: 'blur(10px)',
          display: 'flex',
          alignItems: 'center',
          gap: 12,
        }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={objectMetaStyle(palette)}>LEARN THE MOSAIC</div>
            <div style={{ marginTop: 4, fontSize: 12, color: palette.textMuted, lineHeight: 1.35 }}>
              Three tiny levels unlock the Sun.
            </div>
          </div>
          <button onClick={() => goto('level:1')} style={{ ...objectPillBtn(palette), borderColor: palette.glow }}>
            START
          </button>
          <button onClick={() => { setLearnPrompt(false); setPromptDismissed(true); }} style={objectPillBtn(palette)}>
            LATER
          </button>
        </div>
      )}

      {hint && (
        <div style={{
          position: 'absolute',
          left: 0,
          right: 0,
          bottom: 166,
          textAlign: 'center',
          fontFamily: 'JetBrains Mono, monospace',
          fontSize: 10,
          letterSpacing: '0.22em',
          color: palette.textMuted,
          textTransform: 'uppercase',
          pointerEvents: 'none',
          opacity: 0.75,
          zIndex: 7,
        }}>
          tap · drag · hold
        </div>
      )}

      <div onPointerDown={(e) => e.stopPropagation()} style={{
        position: 'absolute',
        left: 0,
        right: 0,
        bottom: 0,
        zIndex: 9,
        padding: '10px 12px 42px',
        background: `linear-gradient(180deg, ${palette.bg}00, ${palette.bg}ee 28%, ${palette.bg})`,
        pointerEvents: 'auto',
      }}>
        <div style={{
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          margin: '0 4px 8px',
        }}>
          <div style={objectMetaStyle(palette)}>LAST 5 OBJECTS</div>
          <button onClick={() => goto('favorites')} style={objectTinyTextBtn(palette)}>
            FAVORITES {stored.favorites.length}/3
          </button>
        </div>
        <div style={objectDisorderRail(palette)}>
          <button
            onClick={() => setDisorderValue(disorder - 0.12)}
            style={objectDisorderButton(palette)}
          >
            ORDER
          </button>
          <input
            type="range"
            min="0"
            max="1"
            step="0.01"
            value={disorder}
            onChange={(e) => setDisorderValue(parseFloat(e.target.value))}
            style={objectDisorderSlider(palette)}
          />
          <button
            onClick={() => setDisorderValue(disorder + 0.12)}
            style={objectDisorderButton(palette)}
          >
            DISORDER {disorderPct}
          </button>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, minmax(0, 1fr))', gap: 6 }}>
          {history.map((item, index) => {
            const selected = objectPresetId(item) === objectPresetId(preset);
            return (
              <button key={item.date} onClick={() => { setPreset(item); objHaptic(5); }} style={{
                border: 'none',
                background: 'transparent',
                padding: 0,
                color: palette.text,
                cursor: 'pointer',
                textAlign: 'center',
                minWidth: 0,
              }}>
                <div style={{ display: 'flex', justifyContent: 'center' }}>
                  <ObjectPreview preset={item} size={56} selected={selected} />
                </div>
                <div style={{
                  marginTop: 4,
                  fontFamily: 'JetBrains Mono, monospace',
                  fontSize: 8,
                  letterSpacing: '0.08em',
                  color: selected ? palette.glow : palette.textMuted,
                  textTransform: 'uppercase',
                  lineHeight: 1.25,
                }}>
                  {index === 0 ? 'TODAY' : item.date.slice(5)}
                  <br />{item.seedLabel}
                </div>
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
}

function objectPillBtn(palette) {
  return {
    height: 30,
    padding: '0 9px',
    borderRadius: 999,
    background: 'rgba(0,0,0,0.28)',
    border: `1px solid ${palette.outlineStrong}`,
    color: palette.text,
    fontFamily: 'JetBrains Mono, monospace',
    fontSize: 8,
    letterSpacing: '0.14em',
    cursor: 'pointer',
    whiteSpace: 'nowrap',
  };
}

function objectDisorderRail(palette) {
  return {
    height: 26,
    display: 'grid',
    gridTemplateColumns: '50px minmax(0, 1fr) 86px',
    alignItems: 'center',
    gap: 8,
    margin: '0 4px 9px',
    color: palette.textMuted,
    fontFamily: 'JetBrains Mono, monospace',
    fontSize: 8,
    letterSpacing: '0.14em',
    textTransform: 'uppercase',
  };
}

function objectDisorderButton(palette) {
  return {
    border: 'none',
    background: 'transparent',
    color: palette.textMuted,
    fontFamily: 'JetBrains Mono, monospace',
    fontSize: 8,
    letterSpacing: '0.12em',
    textTransform: 'uppercase',
    cursor: 'pointer',
    padding: 0,
    whiteSpace: 'nowrap',
    textAlign: 'left',
  };
}

function objectDisorderSlider(palette) {
  return {
    width: '100%',
    minWidth: 0,
    accentColor: palette.glow,
    cursor: 'pointer',
  };
}

function objectTinyTextBtn(palette) {
  return {
    border: 'none',
    background: 'transparent',
    color: palette.textMuted,
    fontFamily: 'JetBrains Mono, monospace',
    fontSize: 9,
    letterSpacing: '0.16em',
    cursor: 'pointer',
    padding: 0,
  };
}

function objectMetaStyle(palette) {
  return {
    fontFamily: 'JetBrains Mono, monospace',
    fontSize: 8,
    letterSpacing: '0.22em',
    color: palette.textMuted,
    textTransform: 'uppercase',
    lineHeight: 1.3,
  };
}

function objectSeedStyle(palette) {
  return {
    fontFamily: 'JetBrains Mono, monospace',
    fontSize: 9,
    letterSpacing: '0.14em',
    color: palette.textDim,
    lineHeight: 1.4,
  };
}

function ObjectTopBar({ title, onBack }) {
  const C = window.PenrosePalettes.COLORS;
  return (
    <div style={{
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'space-between',
      padding: '62px 16px 10px',
      position: 'relative',
      zIndex: 5,
    }}>
      <button onClick={onBack} style={{
        width: 36,
        height: 36,
        borderRadius: 12,
        background: 'rgba(255,255,255,0.06)',
        border: '1px solid rgba(255,255,255,0.1)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        cursor: 'pointer',
        padding: 0,
      }} aria-label="Back">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
          <path d="M15 6l-6 6 6 6" stroke={C.text} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>
      </button>
      <div style={{
        fontFamily: 'JetBrains Mono, monospace',
        fontSize: 12,
        letterSpacing: '0.22em',
        fontWeight: 700,
        color: C.text,
        textTransform: 'uppercase',
      }}>{title}</div>
      <div style={{ width: 36 }} />
    </div>
  );
}

function FavoritesScreen({ goto }) {
  const [stored, setStored] = useObjState(loadObjectState);
  const C = window.PenrosePalettes.COLORS;
  const openFavorite = (favorite) => {
    setStored(storeLastObject(favorite));
    goto('object');
  };
  return (
    <div style={{ position: 'absolute', inset: 0, background: C.bg, color: C.text, display: 'flex', flexDirection: 'column' }}>
      <ObjectTopBar title="Favorites" onBack={() => goto('home')} />
      <div style={{ padding: '4px 18px 80px', overflowY: 'auto', flex: 1 }}>
        <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, letterSpacing: '0.2em', color: C.textMuted, marginBottom: 14 }}>
          SAVED OBJECTS {stored.favorites.length}/3
        </div>
        {stored.favorites.length === 0 ? (
          <div style={{
            minHeight: 360,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            textAlign: 'center',
            color: C.textMuted,
            fontSize: 13,
            lineHeight: 1.55,
            padding: 24,
          }}>
            Save a daily object from the reset screen.
          </div>
        ) : (
          <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 10 }}>
            {stored.favorites.map(fav => {
              const entry = window.PenrosePalettes.getById(fav.paletteId) || window.PenrosePalettes.active();
              return (
                <button key={objectPresetId(fav)} onClick={() => openFavorite(fav)} style={{
                  border: `1px solid ${C.outline}`,
                  background: 'rgba(255,255,255,0.035)',
                  color: C.text,
                  borderRadius: 16,
                  padding: 12,
                  display: 'flex',
                  alignItems: 'center',
                  gap: 12,
                  textAlign: 'left',
                  cursor: 'pointer',
                }}>
                  <ObjectPreview preset={fav} size={86} selected={false} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 15, fontWeight: 700, letterSpacing: '0.08em' }}>
                      SEED {fav.seedLabel}
                    </div>
                    <div style={{ marginTop: 6, fontSize: 12, color: C.textMuted, lineHeight: 1.5 }}>
                      {fav.date || 'Freeform'} · {entry.name}
                    </div>
                    <div style={{ marginTop: 8, fontFamily: 'JetBrains Mono, monospace', fontSize: 9, letterSpacing: '0.16em', color: C.textDim }}>
                      OPEN OBJECT →
                    </div>
                  </div>
                </button>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}

window.PenroseObject = {
  ObjectScreen,
  FavoritesScreen,
  ObjectPreview,
  loadObjectState,
  saveObjectState,
  storeLastObject,
  addObjectFavorite,
  objectHistoryPresets,
};
