// mariusson — THE ATLAS
// One screen. The atlas IS the title page IS the rabbit hole.
// Anonymous viewer is the default. Role chosen only when account is created.
//
// Interaction model:
//   • Click any visible artwork → camera tunnels into it; cluster reorganizes.
//   • Hover an artwork + scroll wheel → the same tunnel animation begins,
//     centered on that artwork. (Scroll without hover = pan-zoom the whole view.)
//   • Forward / back arrows let you walk linear history.
//   • "Surface" steps you up the rabbit-hole trail.

// Bauhaus primaries stay constant across themes — they're the brand snap.
// Everything else (paper, ink, graphite, rule) is a CSS variable so flipping
// `<html data-theme="dark">` re-tints the whole UI without re-rendering.
const BAU = {
  red:       '#E63946',
  yellow:    '#F4C20D',
  blue:      '#1D4ED8',
  ink:       'var(--ink)',
  paper:     'var(--paper)',
  paper2:    'var(--paper-2)',
  rule:      'var(--rule)',
  graphite:  'var(--graphite)',
  graphite2: 'var(--graphite-2)',
};

// Stable-ish dominant palette per work for the reactive background.
function workPalette(work) {
  const r = rng(hash(work.id + work.style + work.theme));
  const hueByStyle = {
    Baroque: 25, Romantic: 18, Modernist: 200, Abstract: 290, Surreal: 320,
    Brutalist: 220, Conceptual: 0, Figurative: 35, Minimal: 40, Expressionist: 350,
  };
  const themeShift = { Solitude: -10, 'The Body': 10, Architecture: 200, Memory: 30, Land: 80, Light: 50, Decay: 20, Ritual: 350, Machine: 210, Dream: 280 };
  const baseH = (hueByStyle[work.style] ?? 30) + (themeShift[work.theme] ?? 0);
  const a = ((baseH + r() * 30) % 360 + 360) % 360;
  const b = ((a + 50 + r() * 40) % 360);
  const c = ((a + 200 + r() * 30) % 360);
  return [
    `oklch(0.62 0.10 ${a})`,
    `oklch(0.55 0.08 ${b})`,
    `oklch(0.42 0.12 ${c})`,
  ];
}

// Cluster ring blueprint — four layers, blur uniform per layer.
// Counts (5/8/7/6 = 26) and radii are the *maximum* density. The
// effective ring counts shrink as the user lowers the density slider.
// Blur values are 4% above the prior tier baselines (0 / 0.5 / 1.5 / 2).
const RING_BASE = [
  { count: 5, radius: 320, depth:  -40, scale: 0.95, tilt: 0,  thetaOffset: -Math.PI / 10, blur: 0     },
  { count: 8, radius: 523, depth: -240, scale: 0.78, tilt: 5,  thetaOffset:  Math.PI / 6,  blur: 0.52  },
  { count: 7, radius: 736, depth: -460, scale: 0.62, tilt: 9,  thetaOffset:  Math.PI / 3,  blur: 1.56  },
  { count: 6, radius: 880, depth: -680, scale: 0.46, tilt: 12, thetaOffset:  Math.PI / 2,  blur: 2.08  },
];
const DENSITY_MAX = RING_BASE.reduce((s, r) => s + r.count, 0); // 26
const DENSITY_MIN = 6;

// Distribute a target density across the rings proportional to base counts,
// floor-then-largest-fractional-remainder so the total exactly equals d.
function ringCountsForDensity(d) {
  const target = Math.max(DENSITY_MIN, Math.min(DENSITY_MAX, d));
  const raw = RING_BASE.map((r) => (r.count / DENSITY_MAX) * target);
  const counts = raw.map((r) => Math.max(1, Math.floor(r)));
  let extra = target - counts.reduce((s, c) => s + c, 0);
  const order = raw.map((r, i) => ({ i, frac: r - Math.floor(r) }))
    .sort((a, b) => b.frac - a.frac);
  let k = 0;
  while (extra > 0) {
    const idx = order[k % order.length].i;
    if (counts[idx] < RING_BASE[idx].count) {
      counts[idx]++;
      extra--;
    }
    k++;
    if (k > 64) break;
  }
  while (extra < 0) {
    let dec = 0;
    for (let i = counts.length - 1; i >= 0 && extra < 0; i--) {
      if (counts[i] > 1) { counts[i]--; extra++; dec++; }
    }
    if (dec === 0) break;
  }
  return counts;
}

function Atlas({ catalogue }) {
  // Viewport size — drives a different mobile posture: the chrome card
  // and NowPlaying card collapse to minimal launchers, the wheel scales
  // down so the wedge ring fits a narrow phone screen. matchMedia, not
  // resize, so we only re-render when crossing the breakpoint.
  const [isMobile, setIsMobile] = React.useState(() =>
    typeof window !== 'undefined' && window.matchMedia('(max-width: 760px)').matches
  );
  React.useEffect(() => {
    if (typeof window === 'undefined') return;
    const mq = window.matchMedia('(max-width: 760px)');
    const onChange = () => setIsMobile(mq.matches);
    if (mq.addEventListener) mq.addEventListener('change', onChange);
    else mq.addListener(onChange);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener('change', onChange);
      else mq.removeListener(onChange);
    };
  }, []);

  // Wheel filter path
  const [path, setPath] = React.useState([]);
  // RadialWheel anchor in atlas-surface coordinates (offset from the
  // surface centre). `null` = wheel closed. Empty-space clicks toggle it.
  const [wheel, setWheel] = React.useState(null);
  // Mobile "More" drawer (Settings / Density / Motion / Consign)
  const [mobileMoreOpen, setMobileMoreOpen] = React.useState(false);
  // Mobile tab — replaces the spatial atlas on phone. Cosmos-shape:
  // 'feed' = masonry of all works (filtered by wheel path), 'search' =
  // visual category browse + text input, 'saved' = same masonry
  // filtered to the user's saved set.
  const [mobileTab, setMobileTab] = React.useState('feed');
  // Mobile push-detail — the work being viewed full-screen, or null.
  // On desktop, drilling uses the cluster zoom; on mobile, tap-to-detail
  // pushes a full-screen view (Cosmos pattern) with swipe-down dismiss.
  const [mobileDetailWork, setMobileDetailWork] = React.useState(null);
  // Mount-during-exit pattern: `wheelRendered` keeps the wheel in the
  // DOM during its collapse-out animation; `wheelVisible` flips a frame
  // after mount to fire the bloom-in, then off before unmount to fire
  // the collapse-out. `lastWheelAnchor` preserves position during exit.
  const [wheelRendered, setWheelRendered] = React.useState(false);
  const [wheelVisible, setWheelVisible] = React.useState(false);
  const lastWheelAnchor = React.useRef(null);
  React.useEffect(() => {
    if (wheel) {
      lastWheelAnchor.current = wheel;
      setWheelRendered(true);
      // Two RAFs: first paint at scale(0.55)/opacity 0, then transition in.
      const r = requestAnimationFrame(() => {
        requestAnimationFrame(() => setWheelVisible(true));
      });
      return () => cancelAnimationFrame(r);
    } else {
      setWheelVisible(false);
      const t = setTimeout(() => setWheelRendered(false), 320);
      return () => clearTimeout(t);
    }
  }, [wheel]);

  // Cluster density — how many tiles fill the surface. Persisted so the
  // user's preference survives reloads. Defaults to MAX so it matches the
  // prior layout out of the box.
  const [density, setDensity] = React.useState(() => {
    try {
      const v = parseInt(localStorage.getItem('mariusson:density') || '', 10);
      if (Number.isFinite(v) && v >= DENSITY_MIN && v <= DENSITY_MAX) return v;
    } catch {}
    return DENSITY_MAX;
  });
  React.useEffect(() => {
    try { localStorage.setItem('mariusson:density', String(density)); } catch {}
  }, [density]);

  // Unified trail — linear stack of visited works, with a pointer.
  //   trail[trailIdx]  = current center
  //   trailIdx > 0     → can go back
  //   trailIdx < end   → can go forward
  // Drilling into a work truncates anything past trailIdx then appends.
  // The Surface counter is just (trailIdx + 1).
  const [trail, setTrail] = React.useState([catalogue[0]]);
  const [trailIdx, setTrailIdx] = React.useState(0);
  const center = trail[trailIdx];

  // Spatial state
  const [seed, setSeed] = React.useState(0);
  // orbit (cursor position relative to surface centre) drives every
  // parallax in the atlas — orbs, the rotation wrapper, per-tile drift.
  // We write it through CSS variables on the surface root instead of
  // React state so 60-Hz mouse-move events don't re-render 19 tiles.
  const orbitRef = React.useRef({ x: 0, y: 0 });
  const surfaceElRef = React.useRef(null);
  const orbitRafRef = React.useRef(0);

  // Zoom transition
  const [zoom, setZoom] = React.useState(null); // { workId, x, y } during animation
  const zoomTimer = React.useRef(null);

  // Account — backed by localStorage via auth.jsx, so signing in/out
  // persists across reloads and between tabs.
  const auth = useAuth();
  const account = auth.account;
  const role = account?.role || 'viewer';
  const [gate, setGate] = React.useState(null);
  const [roleSwitchOpen, setRoleSwitchOpen] = React.useState(false);
  const [profileOpen, setProfileOpen] = React.useState(false);
  const [studioOpen, setStudioOpen] = React.useState(false);
  // Public collector profile target (set by other surfaces — Studio
  // receipts, Room comments — when you click on a collector's name).
  const [collectorTarget, setCollectorTarget] = React.useState(null);

  // Surfaces
  const [profileTarget, setProfileTarget] = React.useState(null);
  const [detailOpen, setDetailOpen] = React.useState(false);
  const [roomOpen, setRoomOpen] = React.useState(false);
  const [savedOpen, setSavedOpen] = React.useState(false);
  const [commission, setCommission] = React.useState(null);
  const [submitOpen, setSubmitOpen] = React.useState(false);

  // saved/liked are persisted per-user via interactions.jsx, so they
  // survive a refresh and follow the user across tabs.
  const savedIds = useSavedSet();
  const likedIds = useLikedSet();
  const [toast, setToast] = React.useState(null);
  const showToast = (t) => { setToast(t); setTimeout(() => setToast(null), 1800); };

  // Filtered universe
  const universe = React.useMemo(() => catalogue.filter((w) => {
    if (path[0] && w.region !== path[0]) return false;
    if (path[1] && w.style !== path[1]) return false;
    if (path[2] && w.method !== path[2]) return false;
    if (path[3] && w.theme !== path[3]) return false;
    return true;
  }), [path, catalogue]);

  // Cluster ordered by similarity to center.  Tiles sit on three concentric
  // bands (close, mid, far). Each band has a fixed slot count and even
  // angular distribution — guarantees no two tiles overlap at the same
  // depth.  Bands are placed at distinct z-depths and slightly rotated
  // relative to each other so cards never z-fight.  Each tile renders
  // with an explicit zIndex = (depthBand) * 10 + rotational position so
  // the painter's algorithm is deterministic.
  const cluster = React.useMemo(() => {
    const c = center;
    // When no filters are active we draw from the entire catalogue. As soon
    // as a filter is committed we draw exclusively from the matching set —
    // even if it's small — so the surface honestly reflects the drill-down
    // instead of silently padding with non-matching tiles.
    const pool = path.length === 0 ? catalogue : universe;
    const scored = pool
      .filter((w) => w.id !== c.id)
      .map((w) => ({
        w,
        score:
          (w.style === c.style ? 4 : 0) +
          (w.theme === c.theme ? 3 : 0) +
          (w.method === c.method ? 2 : 0) +
          (w.region === c.region ? 1 : 0) +
          ((Math.abs(w.year - c.year) < 30) ? 1 : 0),
      }))
      .sort((a, b) => b.score - a.score);

    // Four ring layers (5/8/7/6 = 26 max), with the deepest 23% being a
    // new heavily-blurred back plane. Counts are scaled down by the user's
    // density preference; ring 4 (deepest) keeps proportional share so
    // the back-plane doesn't disappear at low densities.
    //
    // Depth blur is per-RING (per-layer), not per-rank: every tile in a
    // ring gets the same blur. Ranking-based blur created a visible
    // asymmetry where one half of the outer ring was sharp because the
    // "furthest" tiles in the ranked list were clustered to one side.
    const dynamicCounts = ringCountsForDensity(density);
    const RINGS = RING_BASE.map((r, i) => ({ ...r, count: dynamicCounts[i] }));

    // Spread is bound to the same slider as density. At max density the
    // cluster uses its full radii; at min density it shrinks to 63% of
    // that (i.e. 37% less spread — a gentler floor than the prior 40%
    // collapse, which packed tiles too close to the centre). Linear
    // between the two so dragging the slider feels like a single
    // zoom-in/zoom-out gesture.
    const tDensity = (density - DENSITY_MIN) / (DENSITY_MAX - DENSITY_MIN);
    const spreadFactor = 0.63 + 0.37 * tDensity;

    const out = [];
    let cursor = 0;
    // Wheel repulsion — push tiles radially outward from the wheel centre
    // so the picker never gets covered. Buffer is per-ring (bigger tiles
    // need more clearance). Tiles outside the disc keep their orbits.
    // On mobile the wheel is a bottom-anchored modal over a dimmed atlas;
    // the cluster does NOT need to scatter around it — keeping the tiles
    // anchored lets the user watch them re-filter in place as they scrub.
    const wheelOR = (wheel && !isMobile) ? window.rwOuterRadius(path) : 0;
    RINGS.forEach((ring, ringIdx) => {
      // Skew azimuth a meaningful amount per click so EVERY tile visibly
      // rotates around the centre, not just the few that swap in/out. The
      // earlier 0.21 step (~12°) was too subtle — the cluster looked
      // mostly static between drills. ~155° is enough that even tiles
      // whose ranking hasn't changed still travel a long arc to their new
      // slot, and the rearrangement reads as a deliberate orbit.
      const seedSkew = (seed * 2.7) % (Math.PI * 2);
      const effectiveRadius = ring.radius * spreadFactor;
      for (let i = 0; i < ring.count; i++) {
        const w = scored[cursor]?.w;
        if (!w) return;
        cursor++;
        const theta = ring.thetaOffset + seedSkew + (i / ring.count) * Math.PI * 2;
        // Mild elliptical squash (slight wide bias)
        const baseX = Math.cos(theta) * effectiveRadius * 0.95;
        const baseY = Math.sin(theta) * effectiveRadius * 0.65;
        // Per-tile micro-jitter — keeps them off a perfect circle without
        // creating overlaps (tiny offsets, never enough to collide)
        const jitterX = ((cursor * 53) % 41) - 20;
        const jitterY = ((cursor * 37) % 29) - 14;
        let finalX = baseX + jitterX;
        let finalY = baseY + jitterY;
        if (wheel && !isMobile) {
          // Tile half-width scales with the ring's scale factor. We pad
          // by 28px so the tile clears the wheel by a visible margin
          // instead of just kissing its edge.
          const tileHalf = 84 * ring.scale;
          const pushR = wheelOR + tileHalf + 28;
          const dx = finalX - wheel.ax;
          const dy = finalY - wheel.ay;
          const d = Math.sqrt(dx * dx + dy * dy);
          if (d < pushR) {
            const pushAng = d < 0.5
              ? (cursor * 0.41 + ringIdx) % (Math.PI * 2)
              : Math.atan2(dy, dx);
            finalX = wheel.ax + Math.cos(pushAng) * pushR;
            finalY = wheel.ay + Math.sin(pushAng) * pushR;
          }
        }
        const tiltY = (i % 2 === 0 ? -1 : 1) * ring.tilt;

        // Depth blur — uniform per ring so the back layer reads as a
        // single soft plane instead of half-sharp / half-fuzzy.
        const blurPx = ring.blur;

        out.push({
          ...w,
          _rank: out.length,
          _ring: ringIdx,
          _slot: i,
          x: finalX,
          y: finalY,
          z: ring.depth,
          scl: ring.scale,
          tilt: tiltY,
          sway: 0,
          blurPx,
        });
      }
    });
    return out;
  }, [center, universe, catalogue, seed, density, wheel, path, isMobile]);

  // Center tile push — the centerpiece is 320 px wide, so plain cluster
  // repulsion isn't enough. We move it just far enough to clear the
  // wheel's outermost ring, with a hard cap so it never flies off-screen
  // when the user clicks right on top of the current centre.
  const centerOffset = React.useMemo(() => {
    if (!wheel || isMobile) return { x: 0, y: 0 };
    const { ax, ay } = wheel;
    const d = Math.hypot(ax, ay);
    const wheelOR = window.rwOuterRadius(path);
    const targetClear = wheelOR + 170;          // 320/2 + small margin
    if (d >= targetClear) return { x: 0, y: 0 };
    const moveDist = Math.min(targetClear - d, 260);
    if (d < 0.5) return { x: -targetClear, y: 0 };
    const dirX = -ax / d;
    const dirY = -ay / d;
    return { x: dirX * moveDist, y: dirY * moveDist };
  }, [wheel, path, isMobile]);

  // If filter changes and current center isn't in universe → relocate.
  // Use a non-history move (replace, not push) so the wheel doesn't pad
  // the rabbit-hole stack.
  React.useEffect(() => {
    if (universe.length > 0 && !universe.some((u) => u.id === center.id)) {
      const target = universe[0];
      setTrail((t) => {
        const next = [...t];
        next[trailIdx] = target;
        return next;
      });
      setSeed((s) => s + 1);
    }
  }, [path, universe.length]);

  const onMove = (e) => {
    const r = e.currentTarget.getBoundingClientRect();
    orbitRef.current = {
      x: (e.clientX - r.left) / r.width - 0.5,
      y: (e.clientY - r.top) / r.height - 0.5,
    };
    if (orbitRafRef.current) return;
    orbitRafRef.current = requestAnimationFrame(() => {
      orbitRafRef.current = 0;
      const el = surfaceElRef.current;
      if (!el) return;
      el.style.setProperty('--orb-x', orbitRef.current.x.toFixed(4));
      el.style.setProperty('--orb-y', orbitRef.current.y.toFixed(4));
      // --bg-orb-x/y is the background-orb-only signal. On desktop we
      // mirror the cursor; on mobile gyro can write here independently
      // so tile parallax stays zero (taps stay accurate).
      el.style.setProperty('--bg-orb-x', orbitRef.current.x.toFixed(4));
      el.style.setProperty('--bg-orb-y', orbitRef.current.y.toFixed(4));
    });
  };

  // ── Mobile pan + pinch (the canvas itself is pan/zoom-able) ──────
  // One ref-tracked transform (translate + scale) is written directly
  // to the canvas wrapper element on each pointer move; no React state
  // means no re-render per finger sample. Reset to identity whenever
  // the wheel opens or a drill kicks off (so the wheel anchors to the
  // viewport centre).
  const canvasWrapperRef = React.useRef(null);
  const canvasStateRef = React.useRef({ x: 0, y: 0, s: 1 });
  const applyCanvasTransform = React.useCallback(() => {
    const el = canvasWrapperRef.current;
    if (!el) return;
    const { x, y, s } = canvasStateRef.current;
    el.style.transform = `translate(${x.toFixed(2)}px, ${y.toFixed(2)}px) scale(${s.toFixed(4)})`;
  }, []);
  // When the wheel opens or the user drills into a tile we want the
  // canvas to return to identity (0,0,scale 1). If the user had panned,
  // an instant reset feels like a snap — animate it back so the cluster
  // glides into place underneath the new centerpiece transition.
  const resetCanvasTransform = React.useCallback((animated = false) => {
    const el = canvasWrapperRef.current;
    const { x, y, s } = canvasStateRef.current;
    canvasStateRef.current = { x: 0, y: 0, s: 1 };
    if (!el) return;
    const drifted = Math.hypot(x, y) > 1 || Math.abs(s - 1) > 0.005;
    if (animated && drifted && typeof el.animate === 'function') {
      const fromTransform = `translate(${x.toFixed(2)}px, ${y.toFixed(2)}px) scale(${s.toFixed(4)})`;
      el.style.transform = 'translate(0px, 0px) scale(1)';
      el.animate(
        [{ transform: fromTransform }, { transform: 'translate(0px, 0px) scale(1)' }],
        { duration: 460, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'none' }
      );
    } else {
      applyCanvasTransform();
    }
  }, [applyCanvasTransform]);
  React.useEffect(() => {
    if (!isMobile) return;
    if (wheel || zoom) {
      resetCanvasTransform(true);
      return;
    }
    const surface = surfaceElRef.current;
    if (!surface) return;
    const pointers = new Map();
    let pinchStart = null;
    let panActive = false;
    let panStart = null;

    const insideUI = (target) => target && target.closest && target.closest('[data-atlas-panel], button, input, textarea, select, a');

    const onDown = (e) => {
      if (e.pointerType === 'mouse') return; // desktop unaffected
      if (insideUI(e.target)) return;
      pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
      if (pointers.size === 1) {
        panActive = false;
        panStart = { x: e.clientX, y: e.clientY };
      } else if (pointers.size === 2) {
        const [a, b] = [...pointers.values()];
        const d0 = Math.hypot(b.x - a.x, b.y - a.y);
        const rect = surface.getBoundingClientRect();
        const sCx = (a.x + b.x) / 2;
        const sCy = (a.y + b.y) / 2;
        // Convert centroid into the surface's centred local coords
        const localCx = sCx - (rect.left + rect.width / 2);
        const localCy = sCy - (rect.top + rect.height / 2);
        pinchStart = {
          d: d0,
          cxLocal: localCx,
          cyLocal: localCy,
          scale: canvasStateRef.current.s,
          x: canvasStateRef.current.x,
          y: canvasStateRef.current.y,
        };
        panActive = false;
      }
    };
    const onMove = (e) => {
      if (!pointers.has(e.pointerId)) return;
      pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
      if (pointers.size === 1 && panStart) {
        const dx = e.clientX - panStart.x;
        const dy = e.clientY - panStart.y;
        if (!panActive && Math.hypot(dx, dy) > 8) panActive = true;
        if (panActive) {
          const st = canvasStateRef.current;
          canvasStateRef.current = { x: st.x + dx, y: st.y + dy, s: st.s };
          panStart = { x: e.clientX, y: e.clientY };
          applyCanvasTransform();
        }
      } else if (pointers.size === 2 && pinchStart) {
        const [a, b] = [...pointers.values()];
        const d1 = Math.hypot(b.x - a.x, b.y - a.y);
        const rect = surface.getBoundingClientRect();
        const sCx = (a.x + b.x) / 2;
        const sCy = (a.y + b.y) / 2;
        const localCx = sCx - (rect.left + rect.width / 2);
        const localCy = sCy - (rect.top + rect.height / 2);
        const ratio = d1 / pinchStart.d;
        const newScale = Math.max(0.5, Math.min(3, pinchStart.scale * ratio));
        // Keep the START centroid stationary relative to the canvas
        // content, then offset the result by the centroid's drift so
        // the user can pan-while-pinch in one gesture.
        const startContentX = (pinchStart.cxLocal - pinchStart.x) / pinchStart.scale;
        const startContentY = (pinchStart.cyLocal - pinchStart.y) / pinchStart.scale;
        const newX = localCx - startContentX * newScale;
        const newY = localCy - startContentY * newScale;
        canvasStateRef.current = { x: newX, y: newY, s: newScale };
        applyCanvasTransform();
      }
    };
    const onUp = (e) => {
      if (!pointers.has(e.pointerId)) return;
      pointers.delete(e.pointerId);
      if (pointers.size < 2) pinchStart = null;
      if (pointers.size === 0) {
        panStart = null;
        // Clamp the canvas back if it's drifted way off (no inertia for v1)
      } else if (pointers.size === 1) {
        const [pt] = [...pointers.values()];
        panStart = { x: pt.x, y: pt.y };
        panActive = false;
      }
    };
    surface.addEventListener('pointerdown', onDown);
    surface.addEventListener('pointermove', onMove);
    surface.addEventListener('pointerup', onUp);
    surface.addEventListener('pointercancel', onUp);
    return () => {
      surface.removeEventListener('pointerdown', onDown);
      surface.removeEventListener('pointermove', onMove);
      surface.removeEventListener('pointerup', onUp);
      surface.removeEventListener('pointercancel', onUp);
    };
  }, [isMobile, wheel, zoom, applyCanvasTransform, resetCanvasTransform]);

  // ── Gyro parallax (mobile only, permission-gated, reduced-motion aware)
  // Writes ONLY --bg-orb-x/--bg-orb-y so the background orbs breathe,
  // while --orb-x/--orb-y stays 0 and tiles don't tilt under the user's
  // finger (keeps tap targets accurate, per research consensus).
  const [gyroState, setGyroState] = React.useState(() => {
    if (typeof window === 'undefined') return 'idle';
    try {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return 'reduced';
      const stored = localStorage.getItem('mariusson:gyro');
      if (stored === 'granted') return 'granted';
      if (stored === 'denied') return 'denied';
    } catch {}
    if (typeof DeviceOrientationEvent === 'undefined') return 'unsupported';
    if (typeof DeviceOrientationEvent.requestPermission === 'function') return 'needs-permission';
    return 'auto'; // android/others: enabled w/o explicit consent
  });
  const requestGyro = React.useCallback(async () => {
    if (typeof DeviceOrientationEvent === 'undefined') {
      setGyroState('unsupported');
      return;
    }
    if (typeof DeviceOrientationEvent.requestPermission === 'function') {
      try {
        const perm = await DeviceOrientationEvent.requestPermission();
        const next = perm === 'granted' ? 'granted' : 'denied';
        setGyroState(next);
        try { localStorage.setItem('mariusson:gyro', next); } catch {}
      } catch {
        setGyroState('denied');
      }
    } else {
      setGyroState('granted');
      try { localStorage.setItem('mariusson:gyro', 'granted'); } catch {}
    }
  }, []);
  React.useEffect(() => {
    if (!isMobile) return;
    const active = gyroState === 'granted' || gyroState === 'auto';
    if (!active) return;
    const onOrient = (e) => {
      if (!surfaceElRef.current) return;
      const gamma = e.gamma || 0; // -90..90 left-right tilt
      const beta = e.beta || 0;   // -180..180 front-back tilt; ~30 = comfortable hold
      const ox = Math.max(-0.5, Math.min(0.5, gamma / 40));
      const oy = Math.max(-0.5, Math.min(0.5, (beta - 30) / 40));
      surfaceElRef.current.style.setProperty('--bg-orb-x', ox.toFixed(4));
      surfaceElRef.current.style.setProperty('--bg-orb-y', oy.toFixed(4));
    };
    window.addEventListener('deviceorientation', onOrient);
    return () => window.removeEventListener('deviceorientation', onOrient);
  }, [isMobile, gyroState]);

  // Drill into a new work — truncates forward, appends, advances pointer.
  const drillTo = (w) => {
    setTrail((t) => {
      const truncated = t.slice(0, trailIdx + 1);
      if (truncated[truncated.length - 1]?.id === w.id) return truncated;
      const next = [...truncated, w];
      // Cap depth at 40
      const start = Math.max(0, next.length - 40);
      const sliced = next.slice(start);
      setTrailIdx(sliced.length - 1);
      return sliced;
    });
  };

  // === Smooth zoom into a work ===========================================
  // Swap center immediately so surviving tiles can glide to their new
  // positions; departing tiles fade out, new ones fade in (handled inside
  // SpatialScene via id-keyed display set).  A brief zoom flag biases the
  // wrapper toward the click focal for the falling-in feel.
  const enterArtwork = (w, focalPoint) => {
    if (w.id === center.id) { setDetailOpen(true); return; }
    // Drilling into a work is a "I'm done filtering here" signal — close
    // the wheel so the new surface arrives clean.
    if (wheel) setWheel(null);
    setZoom({ workId: w.id, x: focalPoint?.x ?? 50, y: focalPoint?.y ?? 50 });
    drillTo(w);
    setSeed((s) => s + 1);
    if (zoomTimer.current) clearTimeout(zoomTimer.current);
    zoomTimer.current = setTimeout(() => setZoom(null), 900);
  };

  // Empty-space click on the atlas surface:
  //   • wheel closed → open it at the click point
  //   • wheel open   → close it (smooth exit)
  // Tile clicks stopPropagation in SceneTile so they never reach this
  // handler; chrome / panels are skipped via the `closest` filter.
  const onSurfaceClick = (e) => {
    if (zoom) return;
    const t = e.target;
    if (t && t.closest && t.closest('[data-atlas-panel], button, input, textarea, select, a, [role="dialog"]')) return;
    // On mobile the wheel is opened from the bottom-sheet Filter button.
    // Tap on empty canvas re-summons the bottom sheet (handled there) and
    // must NOT also create a new wheel. Tap on empty space WHILE a wheel
    // is open still closes it (so click-off-to-close keeps working).
    if (wheel) {
      setWheel(null);
      return;
    }
    if (isMobile) return;
    const r = e.currentTarget.getBoundingClientRect();
    const ax = e.clientX - (r.left + r.width / 2);
    const ay = e.clientY - (r.top + r.height / 2);
    setWheel({ ax, ay });
  };

  // Wheel/scroll behavior:
  //  • If hovering a non-center tile + scroll up → tunnel into it.
  //  • If NOT hovering a tile + scroll DOWN → walk BACK through trail (zoom out).
  //  • If NOT hovering a tile + scroll UP at min zoom → walk forward (if any).
  //  • Otherwise → camera dolly.
  const hoveredTileRef = React.useRef(null);
  const wheelDebounce = React.useRef(0);
  // Scroll = surface navigation only. We deliberately don't scale the
  // whole UI on wheel — that was disorienting. Hovered tile up = drill in;
  // empty-space down = step back; empty-space up = step forward.
  // Wheels originating inside any open panel/dialog/drawer are ignored —
  // we don't want scrolling artist results, country lists, or comments to
  // change the underlying surface.
  const onWheel = (e) => {
    if (zoom) return;
    if (Math.abs(e.deltaY) < 4) return;
    if (e.target && e.target.closest && e.target.closest('[data-atlas-panel]')) return;
    const now = performance.now();
    const target = hoveredTileRef.current;
    const cooled = now - wheelDebounce.current > 600;
    if (!cooled) return;
    if (target && e.deltaY < 0) {
      wheelDebounce.current = now;
      enterArtwork(target.work, target.focal);
      return;
    }
    if (!target && e.deltaY > 0 && trailIdx > 0) {
      wheelDebounce.current = now;
      goBack();
      return;
    }
    if (!target && e.deltaY < 0 && trailIdx < trail.length - 1) {
      wheelDebounce.current = now;
      goForward();
      return;
    }
  };

  // Trail navigation — back/forward arrows walk the rabbit-hole stack.
  // The Surface readout reflects (trailIdx + 1).
  const goBack = () => {
    if (trailIdx > 0) {
      setTrailIdx(trailIdx - 1);
      setSeed((s) => s + 1);
    }
  };
  const goForward = () => {
    if (trailIdx < trail.length - 1) {
      setTrailIdx(trailIdx + 1);
      setSeed((s) => s + 1);
    }
  };

  // Action gating
  const tryAction = (action, payload) => {
    const free = ['open-detail', 'open-artist-anon'];
    if (free.includes(action) || account) runAction(action, payload);
    else setGate({ action, payload });
  };
  const runAction = (action, payload) => {
    if (action === 'save') {
      const next = window.AN_INTERACTIONS.toggleSave(payload);
      showToast(next.has(payload) ? 'Saved to collection' : 'Removed from collection');
    } else if (action === 'like') {
      window.AN_INTERACTIONS.toggleLike(payload);
    } else if (action === 'open-room') setRoomOpen(true);
    else if (action === 'commission') setCommission(payload); // payload may be {work, bio} or just work
    else if (action === 'open-saved') setSavedOpen(true);
    else if (action === 'open-artist' || action === 'open-artist-anon') setProfileTarget(payload);
    else if (action === 'submit-work') setSubmitOpen(true);
    else if (action === 'open-detail') setDetailOpen(true);
  };
  // Called from AccountGate after a successful sign-in or registration.
  // The account itself is already in localStorage by then; auth's hook has
  // already updated `account`. We just resume the action that gated us.
  const acceptGate = (newAccount) => {
    if (gate?.action) runAction(gate.action, gate.payload);
    setGate(null);
    showToast(`Welcome — ${newAccount.role}`);
  };

  const palette = workPalette(center);

  return (
    <div data-atlas-surface
      ref={surfaceElRef}
      onMouseMove={onMove}
      onClick={onSurfaceClick}
      onWheel={(e) => {
        // Don't preventDefault — and don't run the surface handler — when
        // the wheel originated inside a panel/dialog/drawer. That lets the
        // browser scroll the panel content natively, and protects the
        // atlas surface from incidental drift.
        if (e.target && e.target.closest && e.target.closest('[data-atlas-panel]')) return;
        e.preventDefault();
        onWheel(e);
      }}
      style={{
        position: 'relative', width: '100%', height: '100%',
        overflow: 'hidden', background: BAU.paper,
        cursor: zoom ? 'wait' : 'default',
        // Initial orbit values; onMove updates these directly via setProperty.
        '--orb-x': 0, '--orb-y': 0,
      }}>

      {/* Background orbs render on both desktop AND mobile so the
          parallax / gyro layer breathes everywhere. The orbs use
          --bg-orb-x / --bg-orb-y which gyro updates on mobile. */}
      <ReactiveBackground palette={palette} />
      <BauhausBackdrop />

      {/* Spatial atlas — the 4-ring cluster + centerpiece + parallax.
          Renders on both desktop AND mobile. SceneTile sizes itself
          smaller on mobile so the cluster fits a phone screen. */}
      <div
        ref={canvasWrapperRef}
        style={{
          position: 'absolute', inset: 0,
          transformOrigin: 'center center',
          willChange: isMobile ? 'transform' : 'auto',
          touchAction: isMobile ? 'none' : 'auto',
          pointerEvents: 'none',
        }}
      >
        <div style={{ position: 'absolute', inset: 0, pointerEvents: 'auto' }}>
          <SpatialScene
            center={center}
            cluster={cluster}
            centerOffset={centerOffset}
            zoom={zoom}
            mobile={isMobile}
            onEnter={enterArtwork}
            onCenterClick={() => tryAction('open-detail', center)}
            registerHover={(work, focal) => { hoveredTileRef.current = work ? { work, focal } : null; }} />
        </div>
      </div>
      {/* Mobile search overlay — when the Search tab is active, this
          full-screen view sits above the atlas with category cards +
          a text input. Drops away when the user picks something. */}
      {isMobile && mobileTab === 'search' && (
        <MobileSearch
          REGIONS={window.REGIONS || []}
          STYLES={window.STYLES || []}
          METHODS={window.METHODS || []}
          THEMES={window.THEMES || []}
          allWorks={catalogue}
          onTap={(w) => { setMobileTab('feed'); enterArtwork(w, { x: 50, y: 50 }); }}
          onPickCategory={(field, value) => {
            const idx = { region: 0, style: 1, method: 2, theme: 3 }[field];
            if (idx == null) return;
            setPath((p) => {
              const np = ['', '', '', ''];
              for (let i = 0; i < idx; i++) np[i] = p[i] || '';
              np[idx] = value;
              return np.slice(0, idx + 1);
            });
            setMobileTab('feed');
          }} />
      )}

      {/* Tunnel overlay */}
      {/* Subtle drill-in halo — a faint radial bloom at the click focal,
          tinted with the new work's accent. No streak lines, no white
          flash, no screen-blend; just a soft wash that fades in and out
          over the cluster handoff so there's a hint of "you went into
          something" without any flashing-light noise. */}
      {zoom && (
        <div style={{
          position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 30,
          background: `radial-gradient(circle at ${zoom.x}% ${zoom.y}%, ${palette[2]} 0%, transparent 55%)`,
          opacity: 0.12,
          animation: 'an-drill-halo 700ms cubic-bezier(0.65, 0, 0.35, 1) forwards',
        }} />
      )}

      {/* DESKTOP: chrome + NowPlaying stacked in a top-left column. */}
      {!isMobile && (
        <div data-atlas-panel style={{
          position: 'absolute', top: 20, left: 20, zIndex: 24,
          width: 380, maxWidth: 'calc(100vw - 24px)',
          display: 'flex', flexDirection: 'column', gap: 12,
          pointerEvents: 'none',
        }}>
          <MinimalChrome
            mobile={false}
            account={account} role={role}
            savedCount={savedIds.size}
            trail={trail}
            trailIdx={trailIdx}
            canBack={trailIdx > 0}
            canForward={trailIdx < trail.length - 1}
            density={density}
            densityMin={DENSITY_MIN}
            densityMax={DENSITY_MAX}
            onDensity={setDensity}
            onBack={goBack}
            onForward={goForward}
            onSignIn={() => setGate({ action: null })}
            onOpenProfile={() => account ? setProfileOpen(true) : setGate({ action: null })}
            onSaved={() => tryAction('open-saved')}
            onConsign={() => tryAction('submit-work')}
            onStudio={() => account ? setStudioOpen(true) : setGate({ action: null })}
            searchSlot={
              <GlobalSearch
                catalogue={catalogue}
                onPickWork={(w) => enterArtwork(w, { x: 50, y: 50 })}
                onPickArtist={(payload) => tryAction('open-artist-anon', payload)} />
            } />

          <NowPlaying
            mobile={false}
            work={center}
            role={role}
            depth={trail.length - 1}
            liked={likedIds.has(center.id)}
            saved={savedIds.has(center.id)}
            onLike={() => tryAction('like', center.id)}
            onSave={() => tryAction('save', center.id)}
            onArtist={() => tryAction('open-artist-anon', { artistName: center.artist, highlightWork: center })}
            onRoom={() => tryAction('open-room', center)}
            onDetail={() => setDetailOpen(true)} />
        </div>
      )}

      {/* MOBILE: Cosmos-shaped chrome — small wordmark header up top
          (purely visual identity, no controls) + 5-slot bottom tab bar
          with proper SVG icons. */}
      {isMobile && (
        <MobileHeader wheelOpen={!!wheel} path={path} onClearPath={() => setPath([])} />
      )}
      {isMobile && (
        <MobileTabBar
          tab={mobileTab}
          account={account}
          activeRole={role}
          savedCount={savedIds.size}
          wheelOpen={!!wheel}
          onTab={(t) => {
            // Tab switch — Atlas closes any open drawer + wheel and
            // returns to the spatial atlas. Saved opens SavedDrawer.
            if (wheel) setWheel(null);
            if (t === 'feed') {
              setDetailOpen(false); setSavedOpen(false); setRoomOpen(false);
              setProfileOpen(false); setStudioOpen(false);
              setProfileTarget(null); setCollectorTarget(null);
              setSubmitOpen(false); setRoleSwitchOpen(false);
              setGate(null); setCommission(null);
              setMobileTab('feed');
            } else if (t === 'saved') {
              setMobileTab('feed');
              setSavedOpen(true);
            } else if (t === 'search') {
              setMobileTab('search');
            }
          }}
          onFilter={() => {
            if (wheel) return setWheel(null);
            setWheel({ ax: 0, ay: 0 });
          }}
          onProfile={() => account ? setProfileOpen(true) : setGate({ action: null })} />
      )}

      {/* MOBILE: filter wheel renders as a viewport-centred modal
          (NOT a bottom sheet) so the hub text lands in the middle of
          the screen, matching the desktop centring. Backdrop tap +
          floating Done close it. */}
      {isMobile && wheelRendered && (
        <div data-atlas-panel onClick={() => setWheel(null)} style={{
          position: 'absolute', inset: 0, zIndex: 65,
          // No backdrop blur on mobile — the whole point of live scrub
          // is letting the user see the surface re-filter behind the
          // wheel. A faint ink wash keeps the picker readable without
          // obscuring the artworks.
          background: wheelVisible ? 'rgba(14, 14, 12, 0.10)' : 'rgba(14, 14, 12, 0)',
          transition: 'background 240ms var(--ease-out)',
          pointerEvents: 'auto',
        }}>
          {/* Bottom-anchored region — exactly as tall as the scaled
              wheel (WHEEL_MAX_SIZE × mobileScale ≈ 440 px) and sitting
              flush with the safe-area bottom. The wheel's own top:50%
              then resolves to the centre of this strip, putting the
              wheel just above the bottom of the screen so the user's
              thumb naturally reaches the outer wedges. */}
          <div onClick={(e) => e.stopPropagation()} style={{
            position: 'absolute',
            bottom: 'env(safe-area-inset-bottom, 0px)',
            left: 'env(safe-area-inset-left, 0px)',
            right: 'env(safe-area-inset-right, 0px)',
            height: 440,
            pointerEvents: 'none',
          }}>
            <RadialWheel
              anchor={{ ax: 0, ay: 0 }}
              visible={wheelVisible}
              mobile={true}
              path={path}
              setPath={setPath}
              catalogue={catalogue}
              onClose={() => setWheel(null)} />
          </div>
          {/* Floating Done at top-right; Clear (only when path is set)
              at top-left. Both override the backdrop tap behaviour. */}
          {path.some(Boolean) && (
            <button
              onClick={(e) => { e.stopPropagation(); setPath([]); }}
              style={{
                position: 'absolute', top: 'max(env(safe-area-inset-top, 8px), 12px)', left: 12,
                background: 'rgba(255, 122, 36, 0.92)',
                color: BAU.paper, border: 'none',
                borderRadius: 'var(--r-pill)',
                padding: '8px 16px', minHeight: 36,
                fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.16em',
                textTransform: 'uppercase', cursor: 'pointer',
                pointerEvents: 'auto', zIndex: 1,
                boxShadow: '0 0 12px rgba(255, 122, 36, 0.55), 0 2px 6px rgba(0, 0, 0, 0.10)',
              }}>Clear</button>
          )}
          <button
            onClick={(e) => { e.stopPropagation(); setWheel(null); }}
            style={{
              position: 'absolute', top: 'max(env(safe-area-inset-top, 8px), 12px)', right: 12,
              background: BAU.ink, color: BAU.paper, border: 'none',
              borderRadius: 'var(--r-pill)',
              padding: '8px 16px', minHeight: 36,
              fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.16em',
              textTransform: 'uppercase', cursor: 'pointer',
              pointerEvents: 'auto', zIndex: 1,
            }}>Done · {universe.length}</button>
        </div>
      )}

      {/* DESKTOP wheel renders as a floating overlay on the canvas;
          mobile uses the sheet above. */}
      {!isMobile && wheelRendered && (
        <RadialWheel
          anchor={wheel || lastWheelAnchor.current}
          visible={wheelVisible}
          mobile={false}
          path={path}
          setPath={setPath}
          catalogue={catalogue}
          onClose={() => setWheel(null)} />
      )}

      {/* Empty-universe notice — only when the user has actually narrowed
          things down to zero. The center tile is still on screen (it's the
          last work they were viewing); this just makes it explicit that
          no other works match, with a one-click "Clear filters" out. */}
      {path.length > 0 && universe.length === 0 && (
        <div data-atlas-panel style={{
          position: 'absolute', top: '50%', left: '50%',
          transform: 'translate(-50%, calc(-50% - 200px))',
          zIndex: 22, pointerEvents: 'auto',
          padding: '18px 24px',
          background: BAU.paper, border: `1px solid ${BAU.ink}`,
          borderRadius: 'var(--r-lg)',
          boxShadow: 'var(--shadow-soft)',
          maxWidth: 380,
          textAlign: 'center',
        }}>
          <div style={{
            fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.18em',
            color: BAU.graphite, textTransform: 'uppercase', marginBottom: 6,
          }}>No works match</div>
          <div style={{ fontFamily: 'var(--serif)', fontStyle: 'italic', fontSize: 18, color: BAU.ink, marginBottom: 12 }}>
            {path.filter(Boolean).join(' · ')}
          </div>
          <button onClick={() => setPath([])} style={{
            background: BAU.ink, color: BAU.paper, border: 'none',
            borderRadius: 'var(--r-pill)',
            padding: '7px 16px', cursor: 'pointer',
            fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em',
            textTransform: 'uppercase',
          }}>Clear filters</button>
        </div>
      )}

      {/* Each panel renders during its exit animation via PanelMount. */}
      <PanelMount open={detailOpen}>
        {(visible) => (
          <DetailModal work={center} role={role} visible={visible}
            onClose={() => setDetailOpen(false)}
            onArtist={() => { setDetailOpen(false); tryAction('open-artist-anon', { artistName: center.artist, highlightWork: center }); }} />
        )}
      </PanelMount>
      <PanelMount open={!!profileTarget}>
        {(visible) => profileTarget && (
          <ArtistProfile
            artistName={profileTarget.artistName}
            highlightWork={profileTarget.highlightWork}
            catalogue={catalogue} role={role} visible={visible}
            onClose={() => setProfileTarget(null)}
            onCommission={(w) => { setProfileTarget(null); tryAction('commission', w); }}
            onPickWork={(w) => { setProfileTarget(null); enterArtwork(w, { x: 60, y: 50 }); }} />
        )}
      </PanelMount>
      <PanelMount open={roomOpen}>
        {(visible) => (
          <RoomDrawer work={center} visible={visible}
            onClose={() => setRoomOpen(false)}
            onPost={(text) => {
              if (!text || !text.trim()) return;
              window.AN_INTERACTIONS.addPost(center.id, {
                who: account ? `@${account.handle}` : 'anon',
                text,
              });
              showToast('Posted to The Room');
            }} />
        )}
      </PanelMount>
      <PanelMount open={savedOpen}>
        {(visible) => (
          <SavedDrawer saved={savedIds} catalogue={catalogue} visible={visible}
            onClose={() => setSavedOpen(false)}
            onPick={(w) => { setSavedOpen(false); enterArtwork(w, { x: 50, y: 50 }); }} />
        )}
      </PanelMount>
      <PanelMount open={!!commission}>
        {(visible) => commission && (
          <CommissionDialog
            work={commission.work || commission}
            bio={commission.bio}
            buyer={account}
            visible={visible}
            onClose={() => setCommission(null)}
            onSubmit={(payload) => {
              // Don't close immediately — the dialog now shows a receipt step
              // and dismisses itself when the user clicks Done. Just toast.
              showToast(payload?.mode === 'donate'
                ? `Donated €${payload.amount} — thank you`
                : `Commission paid · €${payload.amount}`);
            }} />
        )}
      </PanelMount>
      <PanelMount open={submitOpen}>
        {(visible) => (
          <SubmitDialog visible={visible}
            defaultRegion={account?.region}
            onClose={() => setSubmitOpen(false)}
            onSubmit={async (form, done) => {
              try {
                const work = await window.AN_UPLOADS.addWork(account, form.file, form);
                if (done) done();
                setSubmitOpen(false);
                showToast(`Consigned · ${work.title}`);
              } catch (err) {
                if (done) done(err);
              }
            }} />
        )}
      </PanelMount>
      <PanelMount open={!!gate}>
        {(visible) => gate && (
          <AccountGate intent={gate.action} visible={visible}
            onAccept={acceptGate}
            onCancel={() => setGate(null)} />
        )}
      </PanelMount>
      <PanelMount open={profileOpen}>
        {(visible) => (
          <UserProfileDrawer
            account={account} role={role} visible={visible}
            savedCount={savedIds.size}
            onClose={() => setProfileOpen(false)}
            onSave={(draft) => {
              auth.updateAccount(draft);
              setProfileOpen(false);
              showToast(`Profile saved · ${draft.role}`);
            }}
            onSignOut={() => { auth.logout(); setProfileOpen(false); showToast('Signed out'); }} />
        )}
      </PanelMount>
      <PanelMount open={studioOpen}>
        {(visible) => (
          <StudioDrawer
            account={account} role={role} visible={visible}
            savedIds={savedIds} catalogue={catalogue}
            onClose={() => setStudioOpen(false)}
            onPickWork={(w) => { setStudioOpen(false); enterArtwork(w, { x: 50, y: 50 }); }}
            onPickCollector={(payload) => setCollectorTarget(payload)} />
        )}
      </PanelMount>
      <PanelMount open={!!collectorTarget}>
        {(visible) => collectorTarget && (
          <CollectorPublicProfile
            ownerId={collectorTarget.ownerId}
            collector={collectorTarget.collector}
            visible={visible} catalogue={catalogue}
            onClose={() => setCollectorTarget(null)} />
        )}
      </PanelMount>
      <PanelMount open={roleSwitchOpen}>
        {(visible) => (
          <RoleSwitchDialog role={role} visible={visible}
            onClose={() => setRoleSwitchOpen(false)}
            onPick={(next) => {
              if (!account) {
                setRoleSwitchOpen(false);
                setGate({ action: null });
                return;
              }
              auth.updateAccount({ role: next });
              setRoleSwitchOpen(false);
              showToast(`Now viewing as — ${next}`);
            }} />
        )}
      </PanelMount>
      {toast && (
        <div style={{
          position: 'absolute', top: 80, left: '50%', transform: 'translateX(-50%)',
          background: BAU.ink, color: BAU.paper,
          padding: '10px 20px',
          borderRadius: 'var(--r-pill)',
          fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.16em',
          textTransform: 'uppercase', zIndex: 100,
          boxShadow: 'var(--shadow-soft)',
          animation: 'an-fade-up 0.3s var(--ease-out)',
        }}>{toast}</div>
      )}
    </div>
  );
}

// ─── Reactive background ────────────────────────────────────
function ReactiveBackground({ palette }) {
  // Three soft orbs of work-derived colour, washed back to museum-cream by
  // an aggressive desaturating filter, low orb opacities, AND a translucent
  // paper veil on top. Triple-muted so the tint reads as "the gallery wall
  // has caught a faint colour" rather than "the wall is purple now". Colour
  // transitions are long and slow to keep the change unobtrusive — closer
  // to a fade-through than a swap.
  // Translates use CSS calc against --orb-x/--orb-y which Atlas writes via
  // rAF, so these layers update without React re-rendering.
  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 0, background: BAU.paper, overflow: 'hidden' }}>
      <div style={{ position: 'absolute', inset: 0, filter: 'saturate(0.55)' }}>
        <div style={{
          position: 'absolute', top: '20%', left: '15%', width: '50vw', height: '50vw',
          background: palette[0], borderRadius: '50%', filter: 'blur(130px)', opacity: 0.30,
          transform: 'translate(calc(var(--bg-orb-x, 0) * -40px), calc(var(--bg-orb-y, 0) * -40px))',
          transition: 'background 3s cubic-bezier(0.65, 0, 0.35, 1)',
          willChange: 'transform',
        }} />
        <div style={{
          position: 'absolute', bottom: '5%', right: '15%', width: '45vw', height: '45vw',
          background: palette[1], borderRadius: '50%', filter: 'blur(150px)', opacity: 0.30,
          transform: 'translate(calc(var(--bg-orb-x, 0) * 50px), calc(var(--bg-orb-y, 0) * 50px))',
          transition: 'background 3s cubic-bezier(0.65, 0, 0.35, 1)',
          willChange: 'transform',
        }} />
        <div style={{
          position: 'absolute', top: '50%', left: '60%', width: '35vw', height: '35vw',
          background: palette[2], borderRadius: '50%', filter: 'blur(170px)', opacity: 0.22,
          transform: 'translate(-50%, -50%) translate(calc(var(--bg-orb-x, 0) * 30px), calc(var(--bg-orb-y, 0) * -30px))',
          transition: 'background 3s cubic-bezier(0.65, 0, 0.35, 1)',
          willChange: 'transform',
        }} />
      </div>
      <div style={{ position: 'absolute', inset: 0, background: 'var(--paper-veil)' }} />
    </div>
  );
}

function BauhausBackdrop() {
  return (
    <svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: 1, opacity: 0.04 }}>
      <defs>
        <pattern id="bau-grid" width="80" height="80" patternUnits="userSpaceOnUse">
          <path d="M 80 0 L 0 0 0 80" fill="none" stroke={BAU.ink} strokeWidth="0.5" />
        </pattern>
      </defs>
      <rect width="100%" height="100%" fill="url(#bau-grid)" />
    </svg>
  );
}

// ─── Spatial scene ──────────────────────────────────────────
// Unified scene: center + cluster tiles share one positioned pool keyed by
// work.id so identity persists across recomputes.  When the user clicks a
// tile, the cluster regenerates around the new center; surviving tiles
// glide to their new positions, the clicked tile travels INTO the center,
// and any tiles that drop out of the new cluster fade away while new ones
// fade in.  No hard cuts — the surface stays itself, just rearranges.
function SpatialScene({ center, cluster, centerOffset, zoom, mobile, onEnter, onCenterClick, registerHover }) {
  const surfaceRef = React.useRef(null);
  const cx = centerOffset?.x || 0;
  const cy = centerOffset?.y || 0;

  // Build target scene from current center + cluster.
  const target = React.useMemo(() => {
    const out = [];
    out.push({
      id: center.id, work: center,
      x: cx, y: cy, z: 80, scl: 1.0, tilt: 0, sway: 0,
      role: 'center', _ring: -1, _slot: 0,
      blurPx: 0,
    });
    cluster.forEach((w) => {
      out.push({
        id: w.id, work: w,
        x: w.x, y: w.y, z: w.z, scl: w.scl,
        tilt: w.tilt, sway: w.sway,
        role: 'cluster', _ring: w._ring, _slot: w._slot,
        blurPx: w.blurPx || 0,
      });
    });
    return out;
  }, [center, cluster, cx, cy]);

  // Display list = current target + any in-flight leavers.
  //
  // The earlier "simple" version replaced display wholesale on every retarget,
  // which truncated mid-flight fade-outs from the previous click — that's the
  // snap. We now keep a *ref-tracked pool of leavers* so two rapid drills
  // don't cancel each other's exits. Each leaver carries its `_leftAt`
  // timestamp; a periodic sweep drops it once its fade has elapsed.
  const FADE_MS = 1500;
  const [display, setDisplay] = React.useState(target);
  const prevRef = React.useRef(target);
  const leaversRef = React.useRef([]);

  React.useEffect(() => {
    const targetIds = new Set(target.map((t) => t.id));
    const now = Date.now();

    // 1. Tiles that just dropped out of target → new leavers.
    const newLeavers = prevRef.current
      .filter((p) => !targetIds.has(p.id))
      .map((p) => ({ ...p, leaving: true, _leftAt: now }));

    // 2. Carry over earlier leavers that:
    //    (a) haven't expired (still within FADE_MS of their _leftAt), and
    //    (b) haven't re-entered target (they'd be a fresh tile in `target`).
    const stillFading = leaversRef.current.filter((l) =>
      (now - (l._leftAt || 0)) < FADE_MS && !targetIds.has(l.id)
    );

    // 3. Merge — newer leavers win if same id (shouldn't normally happen).
    const newIds = new Set(newLeavers.map((l) => l.id));
    leaversRef.current = [
      ...stillFading.filter((l) => !newIds.has(l.id)),
      ...newLeavers,
    ];

    setDisplay([...target, ...leaversRef.current]);
    prevRef.current = target;
  }, [target]);

  // Periodic sweep — drops expired leavers from both the ref pool and the
  // visible display. Idle (no interval running) when nothing's leaving.
  React.useEffect(() => {
    const id = setInterval(() => {
      if (!leaversRef.current.length) return;
      const now = Date.now();
      const fresh = leaversRef.current.filter((l) => (now - (l._leftAt || 0)) < FADE_MS);
      if (fresh.length === leaversRef.current.length) return;
      leaversRef.current = fresh;
      setDisplay((cur) => [
        ...cur.filter((d) => !d.leaving),
        ...fresh,
      ]);
    }, 250);
    return () => clearInterval(id);
  }, []);

  // No camera dolly on zoom — that caused a snap-back when zoom cleared.
  // The tunnel overlay sells the falling-in feel on its own; the wrapper
  // just sits still and lets tiles glide to their new positions.
  return (
    <div ref={surfaceRef} style={{
      position: 'absolute', inset: 0,
      perspective: 1500, perspectiveOrigin: 'center',
      zIndex: 2, pointerEvents: 'none',
    }}>
      {/* Parallax wrapper — driven by --orb-x/--orb-y CSS vars set on the
          surface root, so rotation tracks the cursor at refresh rate without
          re-rendering React. */}
      <div style={{
        position: 'absolute', inset: 0,
        transformStyle: 'preserve-3d',
        transform: 'rotateY(calc(var(--orb-x, 0) * 6deg)) rotateX(calc(var(--orb-y, 0) * -4deg))',
        pointerEvents: 'none',
        willChange: 'transform',
      }}>
        {display.map((t) => (
          <SceneTile key={t.id} entry={t}
            surfaceRef={surfaceRef}
            mobile={mobile}
            onEnter={onEnter}
            onCenterClick={onCenterClick}
            registerHover={registerHover} />
        ))}
      </div>
    </div>
  );
}

function SceneTile({ entry, surfaceRef, mobile, onEnter, onCenterClick, registerHover }) {
  const { work, x, y, z, scl, tilt, sway, role, leaving, blurPx = 0 } = entry;
  const isCenter = role === 'center';
  // On phones we want the CENTRE artwork to read as the hero — bump it
  // up against a soft fraction of the viewport width so it scales with
  // device size without ever flooding the chrome. Cluster tiles stay
  // small so the orbit reads behind it.
  const heroW = mobile && typeof window !== 'undefined'
    ? Math.max(220, Math.min(300, Math.floor(window.innerWidth * 0.74)))
    : 320;
  const tileW = isCenter
    ? heroW
    : (mobile ? 112 : 168);
  const tileH = Math.round(tileW / (work.aspect || 1));
  const ref = React.useRef(null);

  // Per-tile randomised durations — both transform and opacity get their
  // own duration in [500 ms, 1300 ms], stable across re-renders for this
  // tile. Means cluster rearrangements are organic and asynchronous.
  const { transformDur, opacityDur } = React.useMemo(() => {
    if (isCenter) return { transformDur: 700, opacityDur: 900 };
    const seed = (parseInt(String(entry.id).replace(/\D/g, ''), 10) || 0) + 1;
    const r = (() => { let a = seed >>> 0; return () => {
      a |= 0; a = a + 0x6D2B79F5 | 0;
      let t = Math.imul(a ^ a >>> 15, 1 | a);
      t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
      return ((t ^ t >>> 14) >>> 0) / 4294967296;
    }; })();
    return {
      transformDur: 500 + Math.floor(r() * 800),
      opacityDur:   500 + Math.floor(r() * 800),
    };
  }, [entry.id, isCenter]);

  // Phase machine for opacity — class-driven. CSS animations fire on class
  // addition deterministically, no React batching races.
  const [phase, setPhase] = React.useState(leaving ? 'leave' : 'mount');
  React.useEffect(() => {
    if (leaving && phase !== 'leave') setPhase('leave');
    else if (!leaving && phase === 'leave') setPhase('mount');
  }, [leaving, phase]);
  const onAnimEnd = (e) => {
    if (e.target !== ref.current) return;
    if (e.animationName === 'an-tile-mount-fade' && phase === 'mount') setPhase('visible');
  };
  const animClass = phase === 'mount' ? 'an-tile-mount' : phase === 'leave' ? 'an-tile-leave' : '';

  // Web Animations API for the transform — bypasses React's render pipeline,
  // so transitions fire reliably even when multiple tiles update at once.
  // Animates from the *previous* transform (whatever the browser is showing)
  // to the new target, every time x/y/z/scl change.
  const lastTransformRef = React.useRef(null);
  React.useLayoutEffect(() => {
    const el = ref.current;
    if (!el) return;
    const newTransform = `translate3d(${x}px, ${y}px, ${z}px) scale(${scl}) rotateY(${tilt * 0.4}deg) rotateX(${sway * 0.2}deg)`;
    const prev = lastTransformRef.current;
    if (prev && prev !== newTransform) {
      // Animate from previous to new via Web Animations API
      try {
        el.animate(
          [{ transform: prev }, { transform: newTransform }],
          { duration: transformDur, easing: 'cubic-bezier(0.22, 0.61, 0.36, 1)', fill: 'forwards' }
        );
      } catch {}
    }
    el.style.transform = newTransform;
    lastTransformRef.current = newTransform;
  }, [x, y, z, scl, tilt, sway, transformDur]);

  // Per-tile parallax strength.
  const parallaxX = isCenter ? 0 : Math.round(z / 8 * 100) / 100;
  const parallaxY = isCenter ? 0 : Math.round(z / 14 * 100) / 100;

  const computeFocal = () => {
    if (!ref.current || !surfaceRef.current) return { x: 50, y: 50 };
    const r = ref.current.getBoundingClientRect();
    const s = surfaceRef.current.getBoundingClientRect();
    return {
      x: ((r.left + r.width / 2 - s.left) / s.width) * 100,
      y: ((r.top + r.height / 2 - s.top) / s.height) * 100,
    };
  };

  const handleClick = (e) => {
    e.stopPropagation();
    if (isCenter) onCenterClick && onCenterClick();
    else onEnter(work, computeFocal());
  };

  return (
    <div ref={ref}
      className={animClass}
      onAnimationEnd={onAnimEnd}
      onClick={handleClick}
      onMouseEnter={() => !isCenter && registerHover(work, computeFocal())}
      onMouseLeave={() => !isCenter && registerHover(null)}
      style={{
        position: 'absolute',
        top: '50%', left: '50%',
        width: tileW, height: tileH,
        marginLeft: -tileW / 2,
        marginTop: -tileH / 2,
        // Initial transform — useLayoutEffect overrides this with Web
        // Animations on every change. Inline value here is just so the
        // first-paint position is correct.
        transform: `translate3d(${x}px, ${y}px, ${z}px) scale(${scl}) rotateY(${tilt * 0.4}deg) rotateX(${sway * 0.2}deg)`,
        // Depth blur + cast shadow. The drop-shadow is faint enough that
        // it almost vanishes against the paper background but reads as a
        // soft dark gradient where tiles overlap each other — giving the
        // cluster a layered, depth-cued feel without darkening the
        // surface as a whole. Center tile gets a slightly stronger
        // shadow because it sits on top of every overlapping ring.
        filter: [
          blurPx ? `blur(${blurPx}px)` : null,
          isCenter
            ? 'drop-shadow(4px 6px 10px rgba(0, 0, 0, 0.22))'
            : 'drop-shadow(3px 4px 7px rgba(0, 0, 0, 0.18))',
        ].filter(Boolean).join(' '),
        transition: 'filter 700ms cubic-bezier(0.4, 0, 0.2, 1)',
        // Per-tile CSS animation timing for the opacity classes.
        '--an-tile-dur': `${opacityDur}ms`,
        '--an-tile-delay': '0ms',
        cursor: 'pointer',
        zIndex: isCenter ? 20 : (10 - (entry._ring ?? 0) * 2 + (entry._slot ?? 0) % 2),
        pointerEvents: leaving ? 'none' : 'auto',
        willChange: 'transform, opacity, filter',
      }}>
      {/* INNER wrapper — parallax offset, driven by --orb-x/--orb-y at the
          CSS level so each tile updates without React re-rendering. */}
      <div style={{
        position: 'relative', width: '100%', height: '100%',
        transform: `translate3d(calc(var(--orb-x, 0) * ${parallaxX}px), calc(var(--orb-y, 0) * ${parallaxY}px), 0)`,
        willChange: 'transform',
        boxShadow: isCenter
          ? '0 50px 100px rgba(0,0,0,0.22), 0 0 0 1px rgba(0,0,0,0.04)'
          : '0 30px 60px rgba(0,0,0,0.18)',
      }}>
        <div style={{
          position: 'relative', width: '100%', height: '100%',
          transition: 'transform 0.4s var(--ease-out)',
        }}
        onMouseEnter={(e) => { if (!isCenter) { e.currentTarget.style.transform = 'scale(1.1)'; } }}
        onMouseLeave={(e) => { if (!isCenter) { e.currentTarget.style.transform = 'scale(1)'; } }}>
          <ArtTile work={work} width={tileW} height={tileH} />
          {/* Subtle gloss — static angle now, since making it cursor-reactive
              required React state and that was burning frames. */}
          <div style={{
            position: 'absolute', inset: 0, pointerEvents: 'none',
            background: 'linear-gradient(115deg, rgba(255,255,255,0) 40%, rgba(255,255,255,0.14) 50%, rgba(255,255,255,0) 60%)',
            mixBlendMode: 'overlay',
          }} />
        </div>
      </div>
      {!isCenter && (
        <div style={{
          position: 'absolute', top: 'calc(100% + 6px)', left: 0,
          fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.14em',
          color: BAU.graphite, textTransform: 'uppercase', whiteSpace: 'nowrap',
        }}>
          {work.region} · {work.style}
        </div>
      )}
    </div>
  );
}



// ─── Minimal chrome ─────────────────────────────────────────
// Function-first composition. Top: brand. Hero: search (the primary
// action). Then primary destinations (Studio / Saved / Consign).
// Then a single hairline rule, then tertiary utilities below (nav,
// surface counter, density). The visual hierarchy maps to how
// important each control is, not to design flourish.
function MinimalChrome({ mobile, account, role, savedCount, trail, trailIdx, canBack, canForward, density, densityMin, densityMax, onDensity, onBack, onForward, onSignIn, onOpenProfile, onSaved, onConsign, onStudio, searchSlot }) {
  // mobile=true is no longer used here — Atlas routes the phone view to
  // MobileBottomNav + MobileMoreDrawer. This component is desktop-only.
  const accent = role === 'artist' ? BAU.red : role === 'collector' ? BAU.yellow : BAU.ink;
  const [moreOpen, setMoreOpen] = React.useState(false);

  return (
    <div data-atlas-panel style={{
      background: BAU.paper,
      border: `1px solid var(--rule)`,
      borderRadius: 'var(--r-lg)',
      boxShadow: 'var(--shadow-soft)',
      padding: '12px 16px',
      display: 'flex', flexDirection: 'column', gap: 10,
      pointerEvents: 'auto',
      backdropFilter: 'blur(6px)',
      WebkitBackdropFilter: 'blur(6px)',
      position: 'relative',
    }}>
      {/* PRIMARY row — brand, search, the two everyday actions (Saved +
          account), and a "More" toggle. That's it. Everything niche
          lives under the More toggle. */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        <BauhausWordmark />
        <div style={{ flex: 1, minWidth: 8 }} />
        <button onClick={onSaved} title="Your saved works" style={{
          display: 'inline-flex', alignItems: 'center', gap: 7,
          background: 'transparent', border: `1px solid ${BAU.ink}`,
          borderRadius: 'var(--r-pill)',
          padding: '5px 12px',
          fontFamily: 'var(--mono)', fontSize: 10.5, letterSpacing: '0.16em',
          textTransform: 'uppercase', cursor: 'pointer',
          color: BAU.ink,
        }}>
          <span style={{ width: 7, height: 7, borderRadius: '50%', background: BAU.yellow }} />
          Saved
          <span style={{ color: BAU.graphite, marginLeft: 2, fontVariantNumeric: 'tabular-nums' }}>{savedCount}</span>
        </button>
        {account ? (
          <button onClick={onOpenProfile} title={`Profile · ${role}`} style={{
            display: 'flex', alignItems: 'center', gap: 8,
            background: 'transparent', border: 'none', cursor: 'pointer', padding: 0,
          }}>
            <div style={{
              width: 32, height: 32, background: accent,
              borderRadius: '50%',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              fontFamily: 'var(--serif)', fontSize: 14, fontStyle: 'italic',
              color: role === 'collector' ? BAU.ink : BAU.paper, fontWeight: 600,
              border: `1px solid ${BAU.ink}`,
              transition: 'transform 0.25s var(--ease-out)',
            }}>{(account.name || '?')[0].toUpperCase()}</div>
          </button>
        ) : (
          <button onClick={onSignIn} style={{
            background: BAU.ink, color: BAU.paper, border: 'none',
            borderRadius: 'var(--r-pill)',
            padding: '6px 14px',
            fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em',
            textTransform: 'uppercase', cursor: 'pointer',
          }}>Enter</button>
        )}
        <button onClick={() => setMoreOpen(!moreOpen)} title={moreOpen ? 'Hide more' : 'Show more'} aria-label={moreOpen ? 'Hide more options' : 'Show more options'} style={{
          width: 32, height: 32,
          background: 'transparent', border: `1px solid ${BAU.rule}`,
          borderRadius: '50%',
          color: BAU.ink, cursor: 'pointer',
          fontFamily: 'var(--mono)', fontSize: 14, lineHeight: 1,
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0,
          transition: 'background 0.2s var(--ease-out), transform 0.2s var(--ease-out)',
          transform: moreOpen ? 'rotate(180deg)' : 'rotate(0)',
        }}>⌄</button>
      </div>

      {/* Search — always visible. */}
      <div style={{ display: 'flex', width: '100%' }}>
        {searchSlot}
      </div>

      {/* MORE — collapsible row with the niche bits: trail nav, the
          page-of-pages counter, the "how many" slider, and the role-
          gated actions (Studio link / Sell). */}
      {moreOpen && (
        <>
          <div style={{ height: 1, background: 'var(--rule-soft)' }} />
          <div style={{
            display: 'flex', alignItems: 'center', gap: 10,
            flexWrap: 'wrap',
          }}>
            <div style={{ display: 'flex', gap: 4 }}>
              <NavBtn onClick={onBack} disabled={!canBack} title="Back" glyph="←" />
              <NavBtn onClick={onForward} disabled={!canForward} title="Forward" glyph="→" />
            </div>
            <div title="Where you are in your history" style={{
              fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.16em',
              color: BAU.graphite, textTransform: 'uppercase',
              display: 'inline-flex', alignItems: 'baseline', gap: 5,
              userSelect: 'none', whiteSpace: 'nowrap',
            }}>
              <span>Level</span>
              <span style={{
                color: BAU.ink,
                fontFamily: 'var(--serif)', fontStyle: 'italic', fontSize: 13,
                letterSpacing: '-0.01em',
              }}>{trailIdx + 1}</span>
              <span style={{ color: BAU.graphite2 }}>/ {trail.length}</span>
            </div>
            <div style={{ flex: 1 }} />
            {typeof density === 'number' && onDensity && (
              <div title="How many tiles fit on screen" style={{
                display: 'inline-flex', alignItems: 'center', gap: 8,
              }}>
                <span style={{
                  fontFamily: 'var(--mono)', fontSize: 8.5, letterSpacing: '0.18em',
                  color: BAU.graphite, textTransform: 'uppercase',
                }}>Zoom</span>
                <input
                  type="range"
                  className="an-density"
                  min={densityMin}
                  max={densityMax}
                  step={1}
                  value={density}
                  onChange={(e) => onDensity(parseInt(e.target.value, 10))}
                  aria-label="Zoom"
                  style={{ width: 76 }}
                />
              </div>
            )}
          </div>

          <div style={{
            display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap',
          }}>
            <ThemeToggle />
            {account && (
              <TextLink onClick={onStudio} dot={BAU.blue} dotShape="square">Profile</TextLink>
            )}
            {role === 'artist' && (
              <TextLink onClick={onConsign} emphasize>Sell</TextLink>
            )}
          </div>
        </>
      )}
    </div>
  );
}

// TextLink — chrome action link. Optional leading colour dot (square or
// circle, in Bauhaus primaries) hints at the destination's identity
// without resorting to icon clutter.
function TextLink({ onClick, emphasize, dot, dotShape = 'circle', children }) {
  return (
    <button onClick={onClick} style={{
      background: 'transparent', border: 'none', padding: '4px 0', cursor: 'pointer',
      fontFamily: 'var(--mono)', fontSize: 10.5, letterSpacing: '0.16em',
      textTransform: 'uppercase',
      color: emphasize ? BAU.red : BAU.ink,
      display: 'inline-flex', alignItems: 'center', gap: 7,
      borderBottom: '1px solid transparent',
      transition: 'border-color 0.2s var(--ease-out), color 0.2s var(--ease-out)',
    }}
    onMouseEnter={(e) => { e.currentTarget.style.borderBottomColor = emphasize ? BAU.red : BAU.ink; }}
    onMouseLeave={(e) => { e.currentTarget.style.borderBottomColor = 'transparent'; }}>
      {dot && (
        <span aria-hidden="true" style={{
          width: 7, height: 7,
          borderRadius: dotShape === 'square' ? 2 : '50%',
          background: dot,
        }} />
      )}
      <span style={{ display: 'inline-flex', alignItems: 'baseline', gap: 0 }}>
        {children}
      </span>
    </button>
  );
}

function NavBtn({ onClick, disabled, title, glyph }) {
  return (
    <button onClick={onClick} disabled={disabled} title={title} style={{
      width: 28, height: 28,
      background: BAU.paper, border: `1px solid ${BAU.ink}`,
      borderRadius: '50%',
      marginRight: 6,
      cursor: disabled ? 'not-allowed' : 'pointer',
      opacity: disabled ? 0.3 : 1,
      fontFamily: 'var(--mono)', fontSize: 13, color: BAU.ink,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      transition: 'background 0.2s var(--ease-out)',
    }}
    onMouseEnter={(e) => { if (!disabled) e.currentTarget.style.background = BAU.paper2; }}
    onMouseLeave={(e) => { e.currentTarget.style.background = BAU.paper; }}>
      {glyph}
    </button>
  );
}

function BauhausWordmark() {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
        <span style={{ width: 14, height: 14, borderRadius: '50%', background: BAU.red }} />
        <span style={{ width: 14, height: 14, background: BAU.yellow }} />
        <span style={{ width: 0, height: 0, borderLeft: '7px solid transparent', borderRight: '7px solid transparent', borderBottom: `14px solid ${BAU.blue}` }} />
      </div>
      <span style={{ fontFamily: 'var(--serif)', fontSize: 22, color: BAU.ink, letterSpacing: '-0.01em' }}>
        art<span style={{ display: 'inline-block', width: 5, height: 5, background: BAU.red, borderRadius: '50%', margin: '0 4px 4px 4px', verticalAlign: 'middle' }} /><span style={{ fontStyle: 'italic' }}>nation</span>
      </span>
    </div>
  );
}

// ─── Mobile immersive chrome ────────────────────────────────
// Tiny decorative logomark top-left.
function MobileLogomark({ wheelOpen }) {
  return (
    <div style={{
      position: 'absolute', top: 12, left: 12, zIndex: 24,
      pointerEvents: 'none',
      opacity: wheelOpen ? 0 : 0.9,
      transform: wheelOpen ? 'translateY(-12px)' : 'translateY(0)',
      transition: 'opacity 240ms var(--ease-out), transform 280ms var(--ease-out)',
    }}>
      <BauhausWordmark />
    </div>
  );
}

// ─── Mobile (Cosmos.so-shaped) ─────────────────────────────────
// Small header up top (wordmark only + active-filter chip when set),
// full-bleed 2-col masonry feed in the middle, 5-slot tab bar at the
// bottom. Tap a tile → push-detail with swipe-down dismiss. Tap the
// Filter tab → wheel opens in a bottom sheet. Tap a tile's heart →
// toggle save.

function MobileHeader({ wheelOpen, path, onClearPath }) {
  const active = path.filter(Boolean);
  return (
    <div data-atlas-panel style={{
      position: 'absolute', top: 0, left: 0, right: 0, zIndex: 24,
      display: 'flex', alignItems: 'center', gap: 12,
      padding: '12px 16px 8px',
      paddingTop: 'max(env(safe-area-inset-top, 8px), 12px)',
      background: 'linear-gradient(to bottom, var(--paper) 70%, transparent)',
      pointerEvents: wheelOpen ? 'none' : 'auto',
      opacity: wheelOpen ? 0 : 1,
      transition: 'opacity 220ms var(--ease-out)',
    }}>
      <BauhausWordmark />
      {active.length > 0 && (
        <button onClick={onClearPath} style={{
          marginLeft: 'auto',
          display: 'inline-flex', alignItems: 'center', gap: 6,
          padding: '5px 10px',
          background: 'rgba(255, 122, 36, 0.92)',
          color: BAU.paper, border: 'none',
          borderRadius: 'var(--r-pill)',
          fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.14em',
          textTransform: 'uppercase', cursor: 'pointer',
        }}>
          <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 160 }}>
            {active.join(' · ')}
          </span>
          <span>×</span>
        </button>
      )}
    </div>
  );
}

function MobileTabBar({ tab, account, activeRole, savedCount, wheelOpen, onTab, onFilter, onProfile }) {
  const accountAccent = activeRole === 'artist' ? BAU.red : activeRole === 'collector' ? BAU.yellow : BAU.ink;
  return (
    <div data-atlas-panel style={{
      position: 'absolute', left: 0, right: 0, bottom: 0, zIndex: 28,
      background: BAU.paper,
      borderTop: '1px solid var(--rule)',
      boxShadow: '0 -8px 24px rgba(0,0,0,0.06)',
      paddingTop: 4,
      paddingBottom: 'max(env(safe-area-inset-bottom, 4px), 6px)',
      paddingLeft: 'env(safe-area-inset-left, 0px)',
      paddingRight: 'env(safe-area-inset-right, 0px)',
      pointerEvents: wheelOpen ? 'none' : 'auto',
      transform: wheelOpen ? 'translateY(110%)' : 'translateY(0)',
      opacity: wheelOpen ? 0 : 1,
      transition: 'transform 320ms var(--ease-out), opacity 240ms var(--ease-out)',
    }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)' }}>
        <TabBarSlot active={tab === 'feed'} label="Atlas" onClick={() => onTab('feed')}>
          <IconAtlas />
        </TabBarSlot>
        <TabBarSlot active={tab === 'search'} label="Search" onClick={() => onTab('search')}>
          <IconSearch />
        </TabBarSlot>
        <TabBarSlot label="Filter" onClick={onFilter} accent={BAU.red}>
          <IconFilter />
        </TabBarSlot>
        <TabBarSlot active={tab === 'saved'} label="Saved" onClick={() => onTab('saved')} badge={savedCount > 0 ? savedCount : null}>
          <IconHeart />
        </TabBarSlot>
        <TabBarSlot label={account ? activeRole : 'Sign in'} onClick={onProfile}>
          {account ? (
            <span style={{
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              width: 22, height: 22, borderRadius: '50%',
              background: accountAccent, color: BAU.paper,
              fontFamily: 'var(--serif)', fontStyle: 'italic', fontSize: 12, fontWeight: 600,
              border: `1px solid ${BAU.ink}`,
            }}>{(account.name || '?')[0].toUpperCase()}</span>
          ) : (
            <IconProfile />
          )}
        </TabBarSlot>
      </div>
    </div>
  );
}

function TabBarSlot({ active, label, onClick, badge, accent, children }) {
  return (
    <button onClick={onClick} aria-label={label} aria-current={active ? 'page' : undefined} style={{
      display: 'flex', flexDirection: 'column', alignItems: 'center',
      gap: 3, padding: '8px 0 6px',
      background: 'transparent', border: 'none', cursor: 'pointer',
      position: 'relative',
      minHeight: 50,
      color: accent ? accent : (active ? BAU.ink : BAU.graphite),
    }}>
      <span style={{
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        width: 24, height: 24,
      }}>{children}</span>
      <span style={{
        fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.14em',
        textTransform: 'uppercase',
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        maxWidth: '100%',
      }}>{label}</span>
      {active && (
        <span style={{
          position: 'absolute', top: 2, left: '50%', width: 22, height: 2,
          background: BAU.ink, borderRadius: 999,
          transform: 'translateX(-50%)',
        }} />
      )}
      {badge != null && (
        <span style={{
          position: 'absolute', top: 4, right: 'calc(50% - 22px)',
          minWidth: 16, height: 16, padding: '0 4px',
          background: BAU.ink, color: BAU.paper, borderRadius: 8,
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          fontFamily: 'var(--mono)', fontSize: 8.5, fontWeight: 600,
        }}>{badge}</span>
      )}
    </button>
  );
}

const IconAtlas = () => (
  <svg width="22" height="22" viewBox="0 0 22 22" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
    <rect x="3" y="3" width="7" height="7" rx="0.5" />
    <rect x="12" y="3" width="7" height="7" rx="0.5" />
    <rect x="3" y="12" width="7" height="7" rx="0.5" />
    <rect x="12" y="12" width="7" height="7" rx="0.5" />
  </svg>
);
const IconSearch = () => (
  <svg width="22" height="22" viewBox="0 0 22 22" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
    <circle cx="10" cy="10" r="6" />
    <line x1="14.5" y1="14.5" x2="18.5" y2="18.5" />
  </svg>
);
const IconFilter = () => (
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
    <circle cx="12" cy="12" r="9" />
    <circle cx="12" cy="12" r="3.5" fill="currentColor" />
  </svg>
);
const IconHeart = () => (
  <svg width="22" height="22" viewBox="0 0 22 22" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round">
    <path d="M11 18.5 C 4.5 13.5 3 10 4.5 7 C 6 4 9.5 4 11 7 C 12.5 4 16 4 17.5 7 C 19 10 17.5 13.5 11 18.5 Z" />
  </svg>
);
const IconProfile = () => (
  <svg width="22" height="22" viewBox="0 0 22 22" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
    <circle cx="11" cy="8" r="3.5" />
    <path d="M3.5 19 C 5 14.5 8 13 11 13 C 14 13 17 14.5 18.5 19" />
  </svg>
);

function MobileMain({ works, allWorks, tab, savedIds, path, onTap, onToggleSave, onOpenFilter, onClearFilter, REGIONS, STYLES, METHODS, THEMES, onPickCategory }) {
  if (tab === 'search') {
    return (
      <MobileSearch
        REGIONS={REGIONS} STYLES={STYLES} METHODS={METHODS} THEMES={THEMES}
        allWorks={allWorks}
        onPickCategory={onPickCategory}
        onTap={onTap} />
    );
  }
  const list = tab === 'saved'
    ? allWorks.filter((w) => savedIds.has(w.id))
    : works;
  const empty = list.length === 0;
  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 5,
      paddingTop: 'calc(env(safe-area-inset-top, 0px) + 56px)',
      paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 72px)',
      paddingLeft: 'env(safe-area-inset-left, 0px)',
      paddingRight: 'env(safe-area-inset-right, 0px)',
      overflowY: 'auto',
      overflowX: 'hidden',
      background: BAU.paper,
    }}>
      {empty ? (
        <div style={{
          textAlign: 'center', padding: '60px 24px',
          color: BAU.graphite,
          fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.16em',
          textTransform: 'uppercase',
        }}>
          <div>{tab === 'saved' ? 'Nothing saved yet.' : 'No works match.'}</div>
          {tab === 'saved' ? null : (
            <button onClick={onClearFilter} style={{
              marginTop: 16,
              background: 'transparent', border: `1px solid ${BAU.ink}`,
              borderRadius: 'var(--r-pill)',
              padding: '8px 14px',
              fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em',
              textTransform: 'uppercase', cursor: 'pointer', color: BAU.ink,
            }}>Clear filter</button>
          )}
        </div>
      ) : (
        <MobileFeed works={list} savedIds={savedIds} onTap={onTap} onToggleSave={onToggleSave} />
      )}
    </div>
  );
}

function MobileFeed({ works, savedIds, onTap, onToggleSave }) {
  const cols = [[], []];
  const heights = [0, 0];
  works.forEach((w) => {
    const h = 1 / (w.aspect || 1);
    const c = heights[0] <= heights[1] ? 0 : 1;
    cols[c].push(w);
    heights[c] += h + 0.04;
  });
  return (
    <div style={{ display: 'flex', gap: 6, padding: 6 }}>
      {cols.map((col, i) => (
        <div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 6 }}>
          {col.map((w) => (
            <FeedCard key={w.id} work={w} saved={savedIds.has(w.id)} onTap={() => onTap(w)} onToggleSave={() => onToggleSave(w.id)} />
          ))}
        </div>
      ))}
    </div>
  );
}

function FeedCard({ work, saved, onTap, onToggleSave }) {
  return (
    <button onClick={onTap} style={{
      position: 'relative', width: '100%', padding: 0,
      background: BAU.paper2, border: 'none', cursor: 'pointer',
      aspectRatio: String(work.aspect || 1),
      display: 'block', overflow: 'hidden',
    }}>
      <div style={{ position: 'absolute', inset: 0 }}>
        <ArtTile work={work} width={400} height={Math.round(400 / (work.aspect || 1))} />
      </div>
      <span style={{
        position: 'absolute', left: 8, bottom: 8, right: 44,
        fontFamily: 'var(--mono)', fontSize: 8.5, letterSpacing: '0.14em',
        color: BAU.paper, textTransform: 'uppercase',
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        textShadow: '0 1px 2px rgba(0,0,0,0.6)',
      }}>{work.region} · {work.style}</span>
      <button
        onClick={(e) => { e.stopPropagation(); onToggleSave(); }}
        aria-label={saved ? 'Remove from saved' : 'Save'}
        style={{
          position: 'absolute', top: 8, right: 8,
          width: 32, height: 32, padding: 0,
          background: saved ? 'rgba(255, 122, 36, 0.95)' : 'rgba(14,14,12,0.55)',
          border: 'none', borderRadius: '50%',
          color: BAU.paper, cursor: 'pointer',
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          backdropFilter: 'blur(8px)',
          WebkitBackdropFilter: 'blur(8px)',
        }}>
        <IconHeart />
      </button>
    </button>
  );
}

function MobileSearch({ REGIONS, STYLES, METHODS, THEMES, allWorks, onPickCategory, onTap }) {
  const [q, setQ] = React.useState('');
  const results = React.useMemo(() => {
    const t = q.trim().toLowerCase();
    if (!t) return null;
    const m = (s) => s && s.toLowerCase().includes(t);
    return allWorks.filter((w) => m(w.title) || m(w.artist) || m(w.region) || m(w.style) || m(w.theme) || m(w.method)).slice(0, 30);
  }, [q, allWorks]);
  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 5,
      paddingTop: 'calc(env(safe-area-inset-top, 0px) + 56px)',
      paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 72px)',
      paddingLeft: 'env(safe-area-inset-left, 0px)',
      paddingRight: 'env(safe-area-inset-right, 0px)',
      overflowY: 'auto',
      background: BAU.paper,
    }}>
      <div style={{ padding: '6px 16px 14px' }}>
        <div style={{
          display: 'flex', alignItems: 'center', gap: 10,
          background: BAU.paper2, border: '1px solid var(--rule)',
          borderRadius: 'var(--r-pill)', padding: '10px 14px',
        }}>
          <IconSearch />
          <input
            type="search"
            value={q}
            onChange={(e) => setQ(e.target.value)}
            placeholder="Artists, works, regions, styles…"
            style={{
              flex: 1, border: 'none', outline: 'none', background: 'transparent',
              fontFamily: 'var(--sans)', fontSize: 16, color: BAU.ink,
            }} />
          {q && (
            <button onClick={() => setQ('')} aria-label="Clear" style={{
              background: 'transparent', border: 'none', cursor: 'pointer',
              fontFamily: 'var(--mono)', fontSize: 14, color: BAU.graphite,
              padding: 0,
            }}>×</button>
          )}
        </div>
      </div>
      {results ? (
        <MobileFeed works={results} savedIds={new Set()} onTap={onTap} onToggleSave={() => {}} />
      ) : (
        <div style={{ padding: '4px 16px 16px', display: 'flex', flexDirection: 'column', gap: 12 }}>
          <CategorySection title="Country" items={REGIONS} field="region" onPick={onPickCategory} cols={3} />
          <CategorySection title="Style" items={STYLES} field="style" onPick={onPickCategory} cols={2} />
          <CategorySection title="Method" items={METHODS} field="method" onPick={onPickCategory} cols={2} />
          <CategorySection title="Theme" items={THEMES} field="theme" onPick={onPickCategory} cols={2} />
        </div>
      )}
    </div>
  );
}

function CategorySection({ title, items, field, onPick, cols }) {
  return (
    <div>
      <div style={{
        fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.18em',
        color: BAU.graphite, textTransform: 'uppercase',
        margin: '8px 4px',
      }}>{title}</div>
      <div style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: 6 }}>
        {items.map((it) => (
          <button key={it} onClick={() => onPick(field, it)} style={{
            padding: '14px 10px',
            background: BAU.paper2, border: '1px solid var(--rule)',
            borderRadius: 'var(--r-md)',
            fontFamily: 'var(--serif)', fontStyle: 'italic', fontSize: 14,
            color: BAU.ink, cursor: 'pointer',
            textAlign: 'left',
          }}>{it}</button>
        ))}
      </div>
    </div>
  );
}

function MobileFilterSheet({ visible, onClose, path, onClearPath, universeCount, children }) {
  return (
    <div
      data-atlas-panel
      onClick={onClose}
      style={{
        position: 'absolute', inset: 0, zIndex: 65,
        background: visible ? 'rgba(14, 14, 12, 0.42)' : 'rgba(14, 14, 12, 0)',
        backdropFilter: visible ? 'blur(6px)' : 'blur(0px)',
        WebkitBackdropFilter: visible ? 'blur(6px)' : 'blur(0px)',
        transition: 'background 240ms var(--ease-out), backdrop-filter 240ms var(--ease-out)',
      }}
    >
      <div
        data-atlas-drawer
        onClick={(e) => e.stopPropagation()}
        style={{
          position: 'absolute', left: 0, right: 0, bottom: 0,
          background: BAU.paper,
          borderTopLeftRadius: 'var(--r-lg)',
          borderTopRightRadius: 'var(--r-lg)',
          boxShadow: 'var(--shadow-softer)',
          height: 'min(100vh, 720px)',
          transform: visible ? 'translateY(0)' : 'translateY(100%)',
          transition: 'transform 320ms var(--ease-out)',
          display: 'flex', flexDirection: 'column',
          paddingBottom: 'max(env(safe-area-inset-bottom, 8px), 12px)',
        }}
      >
        <div style={{ display: 'flex', justifyContent: 'center', padding: '10px 0 6px' }}>
          <span style={{
            display: 'block', width: 36, height: 4,
            background: 'var(--rule)', borderRadius: 999,
          }} aria-hidden="true" />
        </div>
        <div style={{
          padding: '4px 16px 8px',
          display: 'flex', alignItems: 'center', gap: 10,
        }}>
          <span style={{
            fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.18em',
            color: BAU.graphite, textTransform: 'uppercase',
          }}>Filter</span>
          <span style={{
            fontFamily: 'var(--mono)', fontSize: 10.5, letterSpacing: '0.14em',
            color: BAU.ink,
          }}>{universeCount} works</span>
          <div style={{ flex: 1 }} />
          {path.some(Boolean) && (
            <button onClick={onClearPath} style={{
              background: 'transparent', color: BAU.ink, border: '1px solid var(--rule)',
              borderRadius: 'var(--r-pill)',
              padding: '6px 12px',
              fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em',
              textTransform: 'uppercase', cursor: 'pointer',
            }}>Clear</button>
          )}
          <button onClick={onClose} aria-label="Done" style={{
            background: BAU.ink, color: BAU.paper, border: 'none',
            borderRadius: 'var(--r-pill)',
            padding: '6px 14px',
            fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em',
            textTransform: 'uppercase', cursor: 'pointer',
          }}>Done</button>
        </div>
        <div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
          {children}
        </div>
      </div>
    </div>
  );
}

function MobileDetail({ work, role, saved, liked, onClose, onArtist, onToggleSave, onToggleLike, onRoom, onDrillCenter }) {
  const [dragY, setDragY] = React.useState(0);
  const startYRef = React.useRef(null);
  const onTouchStart = (e) => { startYRef.current = e.touches[0].clientY; };
  const onTouchMove = (e) => {
    if (startYRef.current == null) return;
    const dy = e.touches[0].clientY - startYRef.current;
    if (dy > 0) setDragY(dy);
  };
  const onTouchEnd = () => {
    if (dragY > 90) onClose();
    else setDragY(0);
    startYRef.current = null;
  };
  return (
    <div data-atlas-panel data-atlas-drawer style={{
      position: 'absolute', inset: 0, zIndex: 70,
      background: BAU.paper,
      transform: `translateY(${dragY}px)`,
      transition: dragY === 0 ? 'transform 220ms var(--ease-out)' : 'none',
      display: 'flex', flexDirection: 'column',
      overflow: 'hidden',
    }} onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd}>
      <div style={{
        display: 'flex', justifyContent: 'center', padding: '10px 0 6px',
        paddingTop: 'max(env(safe-area-inset-top, 8px), 10px)',
      }}>
        <span style={{
          display: 'block', width: 36, height: 4,
          background: 'var(--rule)', borderRadius: 999,
        }} aria-hidden="true" />
      </div>
      <div style={{
        padding: '0 16px 8px',
        display: 'flex', alignItems: 'center', gap: 10,
      }}>
        <button onClick={onClose} aria-label="Back" style={{
          width: 36, height: 36,
          background: 'transparent', border: `1px solid ${BAU.ink}`,
          borderRadius: '50%',
          color: BAU.ink, cursor: 'pointer',
          fontFamily: 'var(--mono)', fontSize: 16, lineHeight: 1,
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0,
        }}>←</button>
        <div style={{ flex: 1 }} />
        <button onClick={onToggleSave} aria-label={saved ? 'Unsave' : 'Save'} style={{
          width: 36, height: 36,
          background: saved ? 'rgba(255, 122, 36, 0.92)' : 'transparent',
          color: saved ? BAU.paper : BAU.ink,
          border: '1px solid ' + (saved ? 'rgba(255, 122, 36, 0.92)' : BAU.ink),
          borderRadius: '50%',
          cursor: 'pointer',
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0,
        }}><IconHeart /></button>
      </div>
      <div style={{
        flex: 1, overflowY: 'auto', overflowX: 'hidden',
        display: 'flex', flexDirection: 'column', gap: 12,
        padding: '4px 16px 24px',
        paddingBottom: 'max(env(safe-area-inset-bottom, 12px), 24px)',
      }}>
        <div style={{
          width: '100%', aspectRatio: String(work.aspect || 1),
          background: BAU.paper2, position: 'relative', overflow: 'hidden',
        }}>
          <div style={{ position: 'absolute', inset: 0 }}>
            <ArtTile work={work} width={800} height={Math.round(800 / (work.aspect || 1))} />
          </div>
        </div>
        <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.18em', color: BAU.graphite, textTransform: 'uppercase' }}>
          {work.id} · {work.year}
        </div>
        <h2 style={{
          fontFamily: 'var(--serif)', fontSize: 28, margin: 0,
          lineHeight: 1.05, letterSpacing: '-0.015em', fontWeight: 400,
        }}>
          <span style={{ fontStyle: 'italic' }}>{work.title}</span>
        </h2>
        <button onClick={onArtist} style={{
          alignSelf: 'flex-start',
          padding: 0, background: 'transparent', border: 'none',
          fontSize: 15, color: BAU.ink, cursor: 'pointer',
          borderBottom: `1px solid ${BAU.ink}`,
          fontFamily: 'var(--sans)',
        }}>{work.artist} · {work.region} →</button>
        <div style={{
          display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 10, marginTop: 6,
        }}>
          <DetailMetaRow label="Style" value={work.style} />
          <DetailMetaRow label="Method" value={work.method} />
          <DetailMetaRow label="Theme" value={work.theme} />
          <DetailMetaRow label="Year" value={String(work.year)} />
        </div>
        <p style={{ fontSize: 14, color: BAU.ink, lineHeight: 1.55, margin: '6px 0 0', fontFamily: 'var(--serif)' }}>
          A {work.method.toLowerCase()} on {work.style.toLowerCase()} register, made in {work.year}. Part of an ongoing study on {work.theme.toLowerCase()}.
        </p>
        {work.price && (
          <div style={{
            padding: 14, background: BAU.paper2, border: '1px solid var(--rule)',
            borderRadius: 'var(--r-md)',
            display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          }}>
            <div>
              <div style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.16em', color: BAU.graphite, textTransform: 'uppercase' }}>Listed</div>
              <div style={{ fontFamily: 'var(--serif)', fontSize: 22, fontStyle: 'italic' }}>€{work.price.toLocaleString()}</div>
            </div>
            <button onClick={onArtist} style={{
              background: BAU.ink, color: BAU.paper, border: 'none',
              borderRadius: 'var(--r-pill)',
              padding: '10px 16px',
              fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.16em',
              textTransform: 'uppercase', cursor: 'pointer',
            }}>Buy</button>
          </div>
        )}
        <button onClick={onRoom} style={{
          background: 'transparent', color: BAU.ink, border: '1px solid var(--rule)',
          borderRadius: 'var(--r-pill)',
          padding: '12px 16px', minHeight: 44,
          fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.16em',
          textTransform: 'uppercase', cursor: 'pointer',
        }}>Room · comments</button>
      </div>
    </div>
  );
}

function DetailMetaRow({ label, value }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column' }}>
      <span style={{ fontFamily: 'var(--mono)', fontSize: 8.5, letterSpacing: '0.16em', color: BAU.graphite, textTransform: 'uppercase' }}>{label}</span>
      <span style={{ fontFamily: 'var(--serif)', fontSize: 14, fontStyle: 'italic', color: BAU.ink, lineHeight: 1.2 }}>{value}</span>
    </div>
  );
}

Object.assign(window, { Atlas, BAU, workPalette });
