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

// ─── data ────────────────────────────────────────────────────
const WORKS = [
  {
    id: "fluffy",
    title: "Fluffy",
    dimension: "60 × 36 in",
    year: "2026",
    src: "uploads/01 Fluffy 5x3ft.jpeg",
    chips: ["NEW WORK", "AVAILABLE"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Available",
    timeframe: "Current Work",
    note: "A long-eared head rears up in blue-grey gesture, a streak of brick red slashing through the storm of brushwork.",
  },
  {
    id: "tie-affair",
    title: "Tie Affair",
    dimension: "36 × 36 in",
    year: "2026",
    src: "uploads/02 Tie Affair 3x3ft sold.jpeg",
    chips: ["NEW WORK", "SOLD"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Sold",
    timeframe: "Current Work",
    note: "A tightly cropped figure in a white shirt and pink tie, the form dissolving into raw grey-blue brushwork.",
  },
  {
    id: "demure",
    title: "Demure",
    dimension: "36 × 36 in",
    year: "2026",
    src: "uploads/03 Demure 3x3ft.jpg",
    chips: ["NEW WORK", "AVAILABLE"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Available",
    timeframe: "Current Work",
    note: "A face built from cool grey strokes, yellow and magenta flaring at the brow and jaw.",
  },
  {
    id: "distant",
    title: "Distant",
    dimension: "48 × 48 in",
    year: "2026",
    src: "uploads/04 Distant 4x4ft.jpeg",
    chips: ["NEW WORK", "AVAILABLE"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Available",
    timeframe: "Current Work",
    note: "A duck's head tilts skyward against a stormy wash, orange and pink streaking down through the grey.",
  },
  {
    id: "business",
    title: "Business",
    dimension: "36 × 36 in",
    year: "2026",
    src: "uploads/05 Business 3x3ft.jpg",
    chips: ["NEW WORK", "AVAILABLE"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Available",
    timeframe: "Current Work",
    note: "A hatted figure squares up in loose grey-green gesture, pink and lime breaking through the jacket.",
  },
  {
    id: "forgotten",
    title: "Forgotten",
    dimension: "36 × 36 in",
    year: "2026",
    src: "uploads/06 Forgotten 3x3ft.jpeg",
    chips: ["NEW WORK", "AVAILABLE"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Available",
    timeframe: "Current Work",
    note: "A faceless figure in a white shirt and pink tie, the whole form dissolving into soft grey-blue gesture.",
  },
  {
    id: "roughness",
    title: "Roughness",
    dimension: "18 × 18 in",
    year: "2026",
    src: "uploads/08 Roughness 18x18in.jpg",
    chips: ["NEW WORK", "AVAILABLE"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Available",
    timeframe: "Current Work",
    note: "A small profile bust rendered in blue-grey strokes, pink and orange flaring at the collar.",
  },
  {
    id: "rebirth-ii",
    title: "Rebirth II",
    dimension: "24 × 48 in",
    year: "2026",
    src: "uploads/09 Rebirth 2x4ft sold.jpg",
    chips: ["NEW WORK", "SOLD"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Sold",
    timeframe: "Current Work",
    note: "A second radiating burst, purple and ochre exploding outward from a dark centre against pale grey.",
  },
  {
    id: "rooster",
    title: "Rooster",
    dimension: "60 × 36 in",
    year: "2026",
    src: "uploads/10_Rooster 5x3ft.JPG",
    chips: ["NEW WORK", "SOLD"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Sold",
    timeframe: "Current Work",
    note: "An animal's head pushes through a field of gold and ochre, its beak catching a streak of orange.",
  },
  {
    id: "duck",
    title: "Duck",
    dimension: "60 × 36 in",
    year: "2026",
    src: "uploads/11 Duck 5x3ft.JPG",
    chips: ["NEW WORK", "AVAILABLE"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Available",
    timeframe: "Current Work",
    note: "A bird's skull-like profile turns through a swirl of blue-grey, ochre catching along the beak.",
  },
  {
    id: "porky",
    title: "Porky",
    dimension: "48 × 48 in",
    year: "2026",
    src: "uploads/12 Porky 4x4ft soldJPG.JPG",
    chips: ["NEW WORK", "SOLD"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Sold",
    timeframe: "Current Work",
    note: "A pig's snout rendered in swirling grey and white against fields of coral and tan.",
  },
  {
    id: "ralf",
    title: "Ralf",
    dimension: "60 × 48 in",
    year: "2026",
    src: "uploads/13 Ralf 5x4ft.jpeg",
    chips: ["NEW WORK", "AVAILABLE"],
    category: "New Work",
    medium: "Oil on canvas",
    status: "Available",
    timeframe: "Current Work",
    note: "A cow's head rendered in warm rust and cream, a burst of orange sky breaking through at the upper left.",
  },
  {
    id: "riding-high",
    title: "Riding High",
    dimension: "50 × 60 in",
    year: "2024",
    src: "uploads/01_RIDING-HIGH.jpg",
    chips: ["ANIMALS", "OIL ON CANVAS"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A horse breaks out of the white ground in a single climbing gesture — built up wet-into-wet, then cut back to the bare canvas so the drips read as snow.",
    hero: true,
  },
  {
    id: "ghost",
    title: "Ghost",
    dimension: "60 × 72 in",
    year: "2023",
    src: "uploads/02_GHOST_5ftx6ft.jpg",
    chips: ["ANIMALS"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A horse's head plunging through a storm of white — the colour held back to a few embers of ochre and rust at the centre.",
  },
  {
    id: "escape",
    title: "Escape",
    dimension: "48 × 48 in",
    year: "2023",
    src: "uploads/10_ESCAPE_4ftX4ft.jpg",
    chips: ["ANIMALS"],
    category: "Animals",
    medium: "Oil on panel",
    status: "Sold",
    timeframe: "Earlier Work",
    note: "A rearing figure caught mid-bolt; the brown body twists out of a flurry of palette-knife white.",
  },
  {
    id: "falling",
    title: "Falling",
    dimension: "72 × 84 in",
    year: "2022",
    src: "uploads/13_FALLING_6X7.jpg",
    chips: ["HORSES", "SOLD"],
    category: "Animals",
    medium: "Oil & graphite on canvas",
    status: "Sold",
    timeframe: "Earlier Work",
    note: "A horse rears out of a curtain of graphite white, roped together by arcs of cadmium yellow and cobalt.",
  },
  {
    id: "believe",
    title: "Believe",
    dimension: "60 × 48 in",
    year: "2025",
    src: "uploads/11_BELIEVE_5ftx4ft.png",
    chips: ["HORSES"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A pair of horses' heads emerge from a blue field, marked with looping cadmium rings.",
  },
  {
    id: "horse-i",
    title: "Horse I",
    dimension: "60 × 60 in",
    year: "2025",
    src: "uploads/15 HORSE01_5FTX5FT.jpg",
    chips: ["HORSES"],
    category: "Animals",
    medium: "Oil on panel",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "Rendered on raw plywood, a horse's profile bleeds white drips against exposed grain.",
  },
  {
    id: "riding-high-ii",
    title: "Riding High",
    dimension: "72 × 60 in",
    year: "2025",
    src: "uploads/01_RIDING-HIGH_6FTX5FT.jpg",
    chips: ["HORSES"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A horse rears against a driving white ground, the body cut in raw umber and black.",
  },
  {
    id: "horse-ii",
    title: "Horse II",
    dimension: "60 × 60 in",
    year: "2025",
    src: "uploads/16 HORSE 2_5FTX5FT.jpg",
    chips: ["HORSES"],
    category: "Animals",
    medium: "Oil on panel",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A horse faces forward on bare wood, the mane picked out in a single stroke of red.",
  },
  {
    id: "regal",
    title: "Regal",
    dimension: "30 × 48 in",
    year: "2025",
    src: "uploads/10_REGAL_30inx48in.jpg",
    chips: ["HORSES"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A close portrait of a horse's face, drawn in confident black strokes against raw white.",
  },
  {
    id: "adventure",
    title: "Adventure",
    dimension: "60 × 60 in",
    year: "2025",
    src: "uploads/ADVENTURE 5X5FT.jpg",
    chips: ["HORSES"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A horse strides through blue-green pasture, built from long unbroken passes of the brush.",
  },
  {
    id: "discarded",
    title: "Discarded",
    dimension: "60 × 48 in",
    year: "2025",
    src: "uploads/DISCARDED 5X4FT.jpg",
    chips: ["HORSES"],
    category: "Animals",
    medium: "Oil on panel",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A horse's hindquarters, cropped tight and built up in slashing black over bare wood.",
  },
  {
    id: "below",
    title: "Below",
    dimension: "60 × 48 in",
    year: "2025",
    src: "uploads/06_below 5ftx4ft.png",
    chips: ["HORSES"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A horse's head bows low into the white, ochre and rust bleeding through the mane.",
  },
  {
    id: "escape-ii",
    title: "Escape",
    dimension: "48 × 48 in",
    year: "2025",
    src: "uploads/03_ESCAPE_4ftX4ft.jpg",
    chips: ["HORSES"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A tangle of blue and rust breaks toward the light — motion held in a single knotted gesture.",
  },
  {
    id: "buck",
    title: "Buck",
    dimension: "120 × 48 in",
    year: "2025",
    src: "uploads/BUCK_10FTX4FT.jpg",
    chips: ["HORSES"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A wide antlered form kicks across a stained ochre band — the widest canvas in the studio.",
  },
  {
    id: "stallion",
    title: "Stallion",
    dimension: "72 × 48 in",
    year: "2025",
    src: "uploads/STALLION 6X4FT.jpg",
    chips: ["HORSES"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A horse's legs plant into pale ground, articulated in cool blue and burnt orange.",
  },
  {
    id: "gaze",
    title: "Gaze",
    dimension: "72 × 72 in",
    year: "2025",
    src: "uploads/08_Gaze_6ftx6ft.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A weathered face built entirely in blue-black and bone white, the features held together by raking light.",
  },
  {
    id: "karl",
    title: "Karl",
    dimension: "60 × 60 in",
    year: "2025",
    src: "uploads/Boyd_Karl_5ftx5ft_Coca.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A head tilted back against open sky, painted flat and graphic against a hard cerulean field.",
  },
  {
    id: "spitzer",
    title: "Spitzer",
    dimension: "48 × 60 in",
    year: "2025",
    src: "uploads/11_Spitzer-4ftx5ft.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A gaunt, layered portrait dragged apart in cream and umber, the paint left thick at the brow.",
  },
  {
    id: "heavenly",
    title: "Heavenly",
    dimension: "48 × 60 in",
    year: "2025",
    src: "uploads/HEAVENLY_4FTX5FT.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A face built from cool blue-greys, drips left to run the full height of the canvas.",
  },
  {
    id: "dick",
    title: "Dick",
    dimension: "60 × 60 in",
    year: "2025",
    src: "uploads/Dick_5ftx5ft.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas tarp",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A political portrait bled in red, white, and blue across a stitched army tarp stencilled with stars.",
  },
  {
    id: "disguise",
    title: "Disguise",
    dimension: "48 × 48 in",
    year: "2025",
    src: "uploads/DIGUISE_48X48.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A face dissolved into a ring of white, one eye socket left as a well of black.",
  },
  {
    id: "obama-study",
    title: "Obama",
    dimension: "18 × 18 in",
    year: "2025",
    src: "uploads/OBAMA_18X18IN.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A small study, the face built from broken patches of rust, blue, and bone.",
  },
  {
    id: "disturbed",
    title: "Disturbed",
    dimension: "48 × 72 in",
    year: "2025",
    src: "uploads/DISTURBED_4FTX6FT.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A face torn open in red and cream, hair rendered as a single raking slash of scarlet.",
  },
  {
    id: "overcome",
    title: "Overcome",
    dimension: "60 × 60 in",
    year: "2025",
    src: "uploads/Overcome_5ftx5ft copy.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "Two yellow rings mark the eyes of a face otherwise dissolved into dark, looping brushwork.",
  },
  {
    id: "obama",
    title: "Obama",
    dimension: "60 × 60 in",
    year: "2025",
    src: "uploads/Obama_5ftx5ft_big.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas tarp",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A large-scale political portrait on stitched army tarp, the face streaked in red, white and blue.",
  },
  {
    id: "reminded",
    title: "Reminded",
    dimension: "24 × 24 in",
    year: "2025",
    src: "uploads/REMINDED_2FTX2FT.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A small, hooded portrait built from cool blue-white strokes, the features barely emerging.",
  },
  {
    id: "psh",
    title: "PSH",
    dimension: "36 × 48 in",
    year: "2025",
    src: "uploads/PSH_3ftx4ft.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A bandaged head rendered in blue-black and white, the wrap picked out in raw unpainted canvas.",
  },
  {
    id: "cash",
    title: "Cash",
    dimension: "18 × 18 in",
    year: "2025",
    src: "uploads/cash 18X18IN.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A small, weathered head study caught in low, raking light.",
  },
  {
    id: "worn",
    title: "Worn",
    dimension: "18 × 18 in",
    year: "2025",
    src: "uploads/WORN 18X118IN.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A face rendered in teal and ochre, one hand raised into frame at the shoulder.",
  },
  {
    id: "reagan",
    title: "Reagan",
    dimension: "60 × 60 in",
    year: "2025",
    src: "uploads/Reagan 5ftx5ft.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas tarp",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A political portrait on stitched army tarp, dripping red, white and blue over stencilled stars.",
  },
  {
    id: "erased",
    title: "Erased",
    dimension: "18 × 18 in",
    year: "2025",
    src: "uploads/erased 18x18in.png",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A face pulled apart in blue-white strokes until the features nearly vanish.",
  },
  {
    id: "blue-sky",
    title: "Blue Sky",
    dimension: "60 × 48 in",
    year: "2025",
    src: "uploads/blue sky 5x4ft.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A face built from long strokes of white and blue against a matching cool field.",
  },
  {
    id: "papa",
    title: "Papa",
    dimension: "60 × 48 in",
    year: "2025",
    src: "uploads/papa 5x4ft.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A cropped head-and-shoulders portrait, the palette warmed with ochre and rose against teal.",
  },
  {
    id: "on-edge",
    title: "On Edge",
    dimension: "72 × 72 in",
    year: "2025",
    src: "uploads/on edge 6x6ft.jpg",
    chips: ["PORTRAITS"],
    category: "Portraits",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A face rendered in orange and blue, glasses drawn in a single confident line.",
  },
  {
    id: "arch-interior-i",
    title: "Interior I",
    dimension: "36 × 36 in",
    year: "2025",
    src: "uploads/arch 1 - 3x3ft.jpg",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "An interior corner breaks apart into raking white and cobalt strokes, a chair caught at the edge of the frame.",
  },
  {
    id: "midcentury-i",
    title: "Midcentury I",
    dimension: "24 × 24 in",
    year: "2025",
    src: "uploads/midcentury 1 - 2x2ft.jpg",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Mixed media on plywood",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "Flat planes of violet, sky blue, and red float over exposed plywood grain, held by loose pencil framing.",
  },
  {
    id: "alone",
    title: "Alone",
    dimension: "60 × 48 in",
    year: "2025",
    src: "uploads/ALONE 5X4FT.jpg",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A single facade splits the canvas between hard blue sky and blush wall, scaffolding cutting across in gold.",
  },
  {
    id: "midcentury-ii",
    title: "Midcentury II",
    dimension: "24 × 24 in",
    year: "2025",
    src: "uploads/midcentury 2 - 2x2ft..jpg",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Mixed media on plywood",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "Angular blue and violet forms hover over bare wood, a single dark panel anchoring the upper right.",
  },
  {
    id: "arch-stairwell",
    title: "Stairwell",
    dimension: "60 × 48 in",
    year: "2025",
    src: "uploads/arch 2 - 5x4ft..jpg",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A stairwell drawn in cyan and ochre drips, the structure loosely gridded over a glassy wash.",
  },
  {
    id: "midcentury-iii",
    title: "Midcentury III",
    dimension: "24 × 24 in",
    year: "2025",
    src: "uploads/midcentury 3 - 2x2ft.jpg",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Mixed media on plywood",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A zigzag roofline in white and navy sits over bands of green, gold, and sky blue on raw plywood.",
  },
  {
    id: "arch-structure-i",
    title: "Structure I",
    dimension: "36 × 36 in",
    year: "2025",
    src: "uploads/arch 3 - 3x3ft..jpg",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A tangle of scaffolding and glass rendered in grey and mint, a single window picked out in warm light.",
  },
  {
    id: "midcentury-iv",
    title: "Midcentury IV",
    dimension: "24 × 24 in",
    year: "2025",
    src: "uploads/midcentury 4 - 2x2ft.jpg",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Mixed media on plywood",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A compact cluster of coloured planes and diagonals sits mid-canvas, the plywood left bare at the edges.",
  },
  {
    id: "arch-structure-ii",
    title: "Structure II",
    dimension: "48 × 48 in",
    year: "2025",
    src: "uploads/arch 4 - 4x4ft..jpg",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A steep architectural passage rendered in cool grey, ochre bleeding through a lit doorway at the centre.",
  },
  {
    id: "midcentury-v",
    title: "Midcentury V",
    dimension: "48 × 24 in",
    year: "2025",
    src: "uploads/midcentury 5 - 4x2ft.jpg",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Mixed media on plywood",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A wide plywood panel scattered with sharp triangular shards of red, blue, and violet.",
  },
  {
    id: "arch-detail",
    title: "Detail",
    dimension: "36 × 36 in",
    year: "2025",
    src: "uploads/arch 5 - 3x3ft..JPG",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A close, cropped study of a table and chair rendered as facets of glass and steel.",
  },
  {
    id: "midcentury-vi",
    title: "Midcentury VI",
    dimension: "48 × 24 in",
    year: "2025",
    src: "uploads/midcentury 6 - 4x2ft.jpg",
    chips: ["ARCHITECTURE"],
    category: "Architecture",
    medium: "Mixed media on plywood",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "Long diagonal bands of red, blue, and white cut across a wide plywood ground, loosely gridded in pencil.",
  },
  {
    id: "after",
    title: "After",
    dimension: "72 × 72 in",
    year: "2024",
    src: "uploads/AFTER_6FTX6FT_LG.jpg",
    chips: ["BULLS", "OIL ON CANVAS"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A bull turns into the light against a pale sky, slashed through with a single red diagonal. The largest canvas in the room at six feet square.",
  },
  {
    id: "bull-i",
    title: "Bull I",
    dimension: "48 × 72 in",
    year: "2025",
    src: "uploads/04_BULL_I_4FTX6FT.jpg",
    chips: ["BULLS"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A bull's shoulder rendered in rust and black, cut through by a single arcing white gesture.",
  },
  {
    id: "bull-v",
    title: "Bull V",
    dimension: "48 × 72 in",
    year: "2025",
    src: "uploads/05_BULL_V_4FTX6FT_LG.jpg",
    chips: ["BULLS"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A bull's horned head rears against bare board, the body dissolving into raking white and umber.",
  },
  {
    id: "behind",
    title: "Behind",
    dimension: "60 × 48 in",
    year: "2025",
    src: "uploads/08 behind 5x4ft.png",
    chips: ["BULLS"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A bull seen from behind and below, horns raised against grey sky with a streak of red at the right.",
  },
  {
    id: "breakout",
    title: "Breakout",
    dimension: "72 × 72 in",
    year: "2025",
    src: "uploads/12_Breakout_6ftx6ft.jpg",
    chips: ["BULLS"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A bull's head charges forward through a wash of pink and grey, ochre and white knotted at the horns.",
  },
  {
    id: "attack",
    title: "Attack",
    dimension: "48 × 96 in",
    year: "2025",
    src: "uploads/ATTACK_4X8.jpg",
    chips: ["BULLS"],
    category: "Animals",
    medium: "Oil on panel",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A long, low panel with a bull's profile built from blue-tinged strokes on raw wood, red arcing at the side.",
  },
  {
    id: "pedro",
    title: "Pedro",
    dimension: "72 × 72 in",
    year: "2025",
    src: "uploads/PEDRO_6FTx6FT.jpg",
    chips: ["BULLS"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A bull's head built from black and white gesture against pale ground, one gold horn catching the light.",
  },
  {
    id: "sunset",
    title: "Sunset",
    dimension: "72 × 48 in",
    year: "2025",
    src: "uploads/sunset_6FTX4FT.png",
    chips: ["BULLS"],
    category: "Animals",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A bull stands full-length against a pink and gold dusk, the form loosely modelled in cool grey-green.",
  },
  {
    id: "mayhem",
    title: "Mayhem",
    dimension: "128 × 74 in",
    year: "2025",
    src: "uploads/Mayhem 128x74in.png",
    chips: ["TARP"],
    category: "Tarp",
    medium: "Mixed media on canvas tarp",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A bull rendered sparsely on a torn olive tarp, one red mark at the crown and a single yellow streak cutting across.",
  },
  {
    id: "bull-tarp-i",
    title: "Bull Tarp I",
    dimension: "120 × 120 in",
    year: "2025",
    src: "uploads/bull tarp 10x10ft.jpg",
    chips: ["TARP"],
    category: "Tarp",
    medium: "Mixed media on canvas tarp",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A dense tangle of blue-black and teal describes a bull against a worn rust tarp.",
  },
  {
    id: "bull-tarp-ii",
    title: "Bull Tarp II",
    dimension: "144 × 144 in",
    year: "2025",
    src: "uploads/bull tarp 12x12ft.png",
    chips: ["TARP"],
    category: "Tarp",
    medium: "Mixed media on canvas tarp",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A pale blue-white bull floats sparsely over khaki canvas, a streak of yellow arcing through.",
  },
  {
    id: "bull-tarp-iii",
    title: "Bull Tarp III",
    dimension: "168 × 108 in",
    year: "2025",
    src: "uploads/bull tarp 14x9ft.jpg",
    chips: ["TARP"],
    category: "Tarp",
    medium: "Mixed media on canvas tarp",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A wide, grommeted tarp carries a tangled bull in blue, white and ochre, split by a diagonal of yellow.",
  },
  {
    id: "bull-tarp-iv",
    title: "Bull Tarp IV",
    dimension: "84 × 72 in",
    year: "2025",
    src: "uploads/bull tarp 7x6ft.jpg",
    chips: ["TARP"],
    category: "Tarp",
    medium: "Mixed media on canvas tarp",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A bull rendered in deep maroon and black surges across a field of gold and ochre.",
  },
  {
    id: "action",
    title: "Action",
    dimension: "60 × 60 in",
    year: "2025",
    src: "uploads/action 5x5ft.jpg",
    chips: ["SPORTS"],
    category: "Sports",
    medium: "Mixed media on canvas tarp",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A football player charges out of an olive tarp ground, the pads built in slashing blue, white and red.",
  },
  {
    id: "seahawk-i",
    title: "Seahawk I",
    dimension: "48 × 60 in",
    year: "2025",
    src: "uploads/Seahawk_01_4x5ft.jpg",
    chips: ["SPORTS"],
    category: "Sports",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A player breaks through pale grey light, the pads picked out in navy, white and a flash of citrus green.",
  },
  {
    id: "seahawk-ii",
    title: "Seahawk II",
    dimension: "48 × 60 in",
    year: "2025",
    src: "uploads/Seahawk_02_4x5ft.jpg",
    chips: ["SPORTS"],
    category: "Sports",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A player crouches low against cool grey, the uniform loosely built in blue-black and cream.",
  },
  {
    id: "seahawk-iii",
    title: "Seahawk III",
    dimension: "48 × 60 in",
    year: "2025",
    src: "uploads/Seahawk_03_4x5ft.jpg",
    chips: ["SPORTS"],
    category: "Sports",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A player reaches into a pass, coral and blue threading through a field of soft grey.",
  },
  {
    id: "phillies",
    title: "Phillies",
    dimension: "36 × 36 in",
    year: "2025",
    src: "uploads/phillies 3x3ft.JPG",
    chips: ["SPORTS"],
    category: "Sports",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A pitcher mid-windup, leg kicked high, built in grey and white with a streak of burnt orange.",
  },
  {
    id: "soccer",
    title: "Soccer",
    dimension: "36 × 36 in",
    year: "2025",
    src: "uploads/soccer 3x3ft.png",
    chips: ["SPORTS"],
    category: "Sports",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A player follows through on a kick, the motion loosely built in cool grey with coral accents.",
  },
  {
    id: "brett",
    title: "Brett",
    dimension: "36 × 60 in",
    year: "2026",
    src: "uploads/brett 3x5ft.jpg",
    chips: ["SPORTS", "SOLD"],
    category: "Sports",
    medium: "Oil on canvas",
    status: "Sold",
    timeframe: "Current Work",
    note: "A batter uncoils through a swing, the KC cap and pinstripes built in crisp white against cool slate blue.",
  },
  {
    id: "ohara",
    title: "Ohara",
    dimension: "36 × 36 in",
    year: "2026",
    src: "uploads/ohara 3x3ft.jpeg",
    chips: ["SPORTS", "SOLD"],
    category: "Sports",
    medium: "Oil on canvas",
    status: "Sold",
    timeframe: "Current Work",
    note: "A crouched athlete dissolves into slate blue and white gesture, streaks of red marking the motion.",
  },
  {
    id: "megan",
    title: "Megan",
    dimension: "36 × 36 in",
    year: "2026",
    src: "uploads/megan 3x3ft.jpeg",
    chips: ["SPORTS", "SOLD"],
    category: "Sports",
    medium: "Oil on canvas",
    status: "Sold",
    timeframe: "Current Work",
    note: "A second view of the same crouched pose, rendered in the same raking blue-grey and white gesture.",
  },
  {
    id: "bossman",
    title: "Bossman",
    dimension: "48 × 72 in",
    year: "2023",
    src: "uploads/BOSSMAN02_6FTx4FT.jpg",
    chips: ["PEOPLE"],
    category: "People",
    medium: "Oil & acrylic on panel",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A portrait built in earth reds and bone white, set hard against a saffron field and a painted white cross.",
  },
  {
    id: "descent",
    title: "Descent",
    dimension: "24 × 48 in",
    year: "2022",
    src: "uploads/BOYD_DECENT_4FTX2FT.jpg",
    chips: ["PEOPLE"],
    category: "People",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A single figure laid out across a wide grey field — the body modelled in blue and umber, the ground left raw.",
  },
  {
    id: "a-dream",
    title: "A Dream",
    dimension: "48 × 36 in",
    year: "2025",
    src: "uploads/A DREAM 3X4FT.jpg",
    chips: ["COLOR COLLISION"],
    category: "Color Collision",
    medium: "Mixed media on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A wave-like tangle of white and umber cuts across blue, split by hard-edged diagonals of red and yellow.",
  },
  {
    id: "adventure-ii",
    title: "Adventure II",
    dimension: "60 × 60 in",
    year: "2025",
    src: "uploads/ADVENTURE 5X5FT-6822feaf.jpg",
    chips: ["COLOR COLLISION"],
    category: "Color Collision",
    medium: "Mixed media on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "Three horses crowd forward under a red sky, raking yellow and blue bars laid flat across the gestural paint.",
  },
  {
    id: "captive",
    title: "Captive",
    dimension: "48 × 48 in",
    year: "2025",
    src: "uploads/CAPTIVE 4X4FT.jpg",
    chips: ["COLOR COLLISION"],
    category: "Color Collision",
    medium: "Mixed media on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "An antlered head rendered in cobalt, its rack outlined in hard orange and white against a flat blue field.",
  },
  {
    id: "collision",
    title: "Collision",
    dimension: "60 × 48 in",
    year: "2025",
    src: "uploads/COLLISION 5X4FT.jpg",
    chips: ["COLOR COLLISION"],
    category: "Color Collision",
    medium: "Mixed media on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A helmeted racer leans into a turn, pistol in hand, graphic reds and yellows striping through the smoky ground.",
  },
  {
    id: "contingency",
    title: "Contingency",
    dimension: "60 × 48 in",
    year: "2025",
    src: "uploads/CONTINGENCY | 60\" x 48\" .jpg",
    chips: ["COLOR COLLISION"],
    category: "Color Collision",
    medium: "Mixed media on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A scaffold of white and blue struts dissolves into loose orange drips, an antlered form emerging at center.",
  },
  {
    id: "distance",
    title: "Distance",
    dimension: "60 × 60 in",
    year: "2025",
    src: "uploads/DISTANCE 5X5FT.jpg",
    chips: ["COLOR COLLISION"],
    category: "Color Collision",
    medium: "Mixed media on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A ghostly herd hovers above a chromed vintage car, yellow and white bars slicing the composition on the diagonal.",
  },
  {
    id: "onslaught",
    title: "Onslaught",
    dimension: "60 × 48 in",
    year: "2025",
    src: "uploads/ONSLAUGHT_4X5FT.jpg",
    chips: ["COLOR COLLISION"],
    category: "Color Collision",
    medium: "Mixed media on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A bull-like form charges through crossed red and orange lances of paint, rays of white and blue exploding behind it.",
  },
  {
    id: "scrutiny",
    title: "Scrutiny",
    dimension: "48 × 48 in",
    year: "2025",
    src: "uploads/SCRUTINY 4X4FT.jpg",
    chips: ["COLOR COLLISION"],
    category: "Color Collision",
    medium: "Mixed media on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A vintage car noses through a field of stars and diagonal color bars, chrome catching cool blue light.",
  },
  {
    id: "territory",
    title: "Territory",
    dimension: "48 × 48 in",
    year: "2025",
    src: "uploads/TERRITORY 4X4FT.jpg",
    chips: ["COLOR COLLISION"],
    category: "Color Collision",
    medium: "Mixed media on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A horned skull built from blue and red hard-edged planes squares off against a soft painterly ground.",
  },
  {
    id: "above-it",
    title: "Above It",
    dimension: "48 × 48 in",
    year: "2025",
    src: "uploads/above it 4x4ft-f8568bcf.png",
    chips: ["PEOPLE"],
    category: "People",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A figure curls in on itself, built from raking black and white strokes with a single crimson mark at the shoulder.",
  },
  {
    id: "descent-ii",
    title: "Descent II",
    dimension: "48 × 24 in",
    year: "2025",
    src: "uploads/DECENT_4FTX2FT-82e875e0.jpg",
    chips: ["PEOPLE"],
    category: "People",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A body arcs backward mid-fall, rendered in cool blue and umber against a bare grey ground.",
  },
  {
    id: "freefall",
    title: "Freefall",
    dimension: "48 × 48 in",
    year: "2025",
    src: "uploads/freefall 4x4ft-9277cfc1.png",
    chips: ["PEOPLE"],
    category: "People",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A figure tumbles against a pale blue sky, limbs loosely gestured in umber, white, and black.",
  },
  {
    id: "plunge",
    title: "Plunge",
    dimension: "72 × 48 in",
    year: "2025",
    src: "uploads/PLUNGE_6FTX4FT-f7ca7b33.jpg",
    chips: ["PEOPLE"],
    category: "People",
    medium: "Oil on canvas",
    status: "Not for Sale",
    timeframe: "Earlier Work",
    note: "A body drops through cold blue and white, the paint dragged downward in long vertical strokes.",
  },
];

const MORE = [
  { id: "n1", title: "Untitled (Field 04)",         year: "2024" },
  { id: "n2", title: "Plein Air — Studies, vol. 3", year: "2024" },
  { id: "n3", title: "Falling, after Auerbach",     year: "2023" },
  { id: "n4", title: "Three Figures, Backlit",      year: "2023" },
  { id: "n5", title: "Animals (winter)",            year: "2022" },
  { id: "n6", title: "Commission · Private, NYC",   year: "2022" },
  { id: "n7", title: "Notebook, March",             year: "2021" },
  { id: "n8", title: "North Light, IV",             year: "2021" },
  { id: "n9", title: "Six Studies for a Painting",  year: "2020" },
];

const FILTERS = ["All", "Recent", "Available", "Animals", "People", "Abstract", "Commissions"];

// studio + artist photos — shown in the drifting strip atop every page's header
const PHOTOS = [
  { id: "ph-container", src: "uploads/photo-studio-container.jpg", title: "The studio, winter", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "North light through a converted shipping container — the working studio in the cold months." },
  { id: "ph-bw", src: "uploads/photo-artist-bw.jpg", title: "Tracy Boyd", photo: true, category: "Studio", chips: ["PORTRAIT"],
    note: "In the studio, between two large heads in progress." },
  { id: "ph-pug", src: "uploads/photo-studio-pug.jpg", title: "Studio company", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "Afternoons at the easel, with company." },
  { id: "ph-horse", src: "uploads/photo-artist-horse.jpg", title: "Working large", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "Standing back from a horse blocked in on raw, wall-mounted canvas." },
  { id: "ph-canvases", src: "uploads/photo-artist-canvases.jpg", title: "Outside the studio", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "Three finished canvases carried out into the sun." },
  { id: "ph-container2", src: "uploads/_DSF5260-e4b256a2.JPG", title: "The container studio", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "Seated among finished canvases and reference books, north light pouring in." },
  { id: "ph-opening", src: "uploads/11406972_10153117592383096_5616025469867435351_n.jpg", title: "Opening night", photo: true, category: "Studio", chips: ["GALLERY"],
    note: "A gallery show, walls hung floor to ceiling." },
  { id: "ph-alley", src: "uploads/20140513_110351-b49022c2.jpg", title: "Fresh off the wall", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "Three new paintings, carried out into the alley to dry in the sun." },
  { id: "ph-plaid", src: "uploads/boyd_port_01.jpg", title: "Beside a portrait", photo: true, category: "Studio", chips: ["PORTRAIT"],
    note: "Next to a canvas still catching light from the window." },
  { id: "ph-chair", src: "uploads/IMG_0169.JPG", title: "Break time", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "Feet up in the old studio chair, paint-splattered and grinning." },
  { id: "ph-away", src: "uploads/IMG_3969-83cd9771.JPG", title: "Stepping back", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "Reading a big canvas from across the room before the next pass." },
  { id: "ph-victory", src: "uploads/IMG_4089.JPG", title: "Finished", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "Arms up between two large works, raw canvas stretched wall to wall." },
  { id: "ph-pug2", src: "uploads/IMG_5923-15ecc81e.JPG", title: "Studio company, again", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "Keeping watch down the hall while the work in progress waits." },
  { id: "ph-bw-crossed", src: "uploads/TBoyd-%25233.jpg", title: "Tracy Boyd, portrait", photo: true, category: "Studio", chips: ["PORTRAIT"],
    note: "Arms crossed against the subway tile, unfinished heads behind." },
  { id: "ph-bw-close", src: "uploads/TBoyd-%25236-Full.jpg", title: "Tracy Boyd, close", photo: true, category: "Studio", chips: ["PORTRAIT"],
    note: "A close portrait against a wall of raw, worked-over paint." },
  { id: "ph-bw-two", src: "uploads/TBoyd-%25238-7cade999.jpg", title: "Between two heads", photo: true, category: "Studio", chips: ["PORTRAIT"],
    note: "Standing between a finished portrait and one still coming together." },
  { id: "ph-jersey", src: "uploads/TracyBoyd_StudioShot.JPG", title: "Leaning in", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "Sunglasses on, next to a fresh commission in the container." },
  { id: "ph-easels", src: "uploads/IMG_0192-be091488.jpg", title: "Wall of easels", photo: true, category: "Studio", chips: ["STUDIO"],
    note: "Four canvases mid-process, propped against the studio wall under the work light." },
];

// ── a generic dropdown pill (site menu, brand-dark by default) ──
function Pill({ label, children, variant = "plain", align = "left" }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("pointerdown", onDoc);
    return () => document.removeEventListener("pointerdown", onDoc);
  }, [open]);
  return (
    <div className={`pill pill--${variant} ${open ? "is-open" : ""}`} ref={ref}>
      <button className="pill__btn" onClick={() => setOpen((o) => !o)}>
        <span className="pill__label">{label}</span>
        <svg className="pill__chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M6 9l6 6 6-6" /></svg>
      </button>
      {open && (
        <div className={`pill__menu pill__menu--${align}`}>
          {typeof children === "function" ? children(() => setOpen(false)) : children}
        </div>
      )}
    </div>
  );
}

// ─── one floating row (drift / drag / tap-to-open) ──────────────────
function FloatRow({ items, dir, speed, speedMul, gap, captions, onOpen }) {
  const rowRef = useRef(null);
  const trackRef = useRef(null);
  const offset = useRef(0);
  const paused = useRef(false);
  const drag = useRef({ down: false, startX: 0, startOff: 0, moved: 0, id: null });

  const halfWidth = () => {
    const t = trackRef.current;
    return t ? t.scrollWidth / 2 : 0; // items are rendered twice
  };
  const wrap = (x) => {
    const w = halfWidth();
    if (w <= 0) return x;
    while (x <= -w) x += w;
    while (x > 0) x -= w;
    return x;
  };

  // drift loop
  useEffect(() => {
    let raf, last = null;
    const tick = (t) => {
      if (last == null) last = t;
      const dt = (t - last) / 1000;
      last = t;
      if (!paused.current && !drag.current.down) {
        offset.current = wrap(offset.current + dir * speed * speedMul * dt);
      }
      if (trackRef.current) trackRef.current.style.transform = `translate3d(${offset.current}px,0,0)`;
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [dir, speed, speedMul]);

  const onPointerDown = (e) => {
    const row = rowRef.current;
    const tileEl = e.target.closest(".hg-rtile");
    drag.current = { down: true, startX: e.clientX, startOff: offset.current, moved: 0, id: e.pointerId, wid: tileEl && tileEl.dataset.wid };
    row.classList.add("is-drag");
    try { row.setPointerCapture && row.setPointerCapture(e.pointerId); } catch (err) {}
  };
  const onPointerMove = (e) => {
    if (!drag.current.down) return;
    const dx = e.clientX - drag.current.startX;
    drag.current.moved = Math.max(drag.current.moved, Math.abs(dx));
    offset.current = wrap(drag.current.startOff + dx);
  };
  const endDrag = (e) => {
    const row = rowRef.current;
    if (!row) return;
    const wasTap = drag.current.down && drag.current.moved <= 6 && drag.current.wid;
    drag.current.down = false;
    row.classList.remove("is-drag");
    if (e && e.pointerId != null && row.releasePointerCapture) {
      try { row.releasePointerCapture(e.pointerId); } catch (err) {}
    }
    // open on a clean tap — done on pointerup so pointer-capture can't swallow it
    if (wasTap) {
      const w = items.find((it) => it.id === drag.current.wid);
      if (w) onOpen(w);
    }
  };

  // render items twice for a seamless loop
  const loop = items.concat(items);

  return (
    <div
      className="hg-row"
      ref={rowRef}
      style={{ "--tile-gap": gap + "px" }}
      onMouseEnter={() => { paused.current = true; }}
      onMouseLeave={() => { paused.current = false; }}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={endDrag}
      onPointerCancel={endDrag}
    >
      <div className="hg-row__track" ref={trackRef}>
        {loop.map((w, i) => {
          return (
            <a
              key={w.id + "-" + i}
              className={"hg-rtile" + (w.photo ? " is-photo" : "")}
              href={"#" + w.id}
              data-wid={w.id}
              onClick={(e) => e.preventDefault()}
            >
              <img className="hg-rtile__img" src={w.src} alt={w.title} draggable="false" />
              {!w.photo && w.status === "Available" && (
                <span className="hg-rtile__sale">For sale</span>
              )}
              <div className={"hg-rtile__cap" + (captions ? "" : " is-off") + (w.photo ? " is-off" : "")}>
                <span className="name">{w.title}</span>
                {!w.photo && (
                  <React.Fragment>
                    <span className="meta">{w.dimension}</span>
                    <span className="meta">{w.year}</span>
                  </React.Fragment>
                )}
              </div>
            </a>
          );
        })}
      </div>
    </div>
  );
}

// ─── artist photo strip: one continuously-drifting row, sits above the bar ─────﻿
function PhotoBar({ photos, onOpen }) {
  return (
    <div className="hg__photobar">
      <FloatRow items={photos} dir={-1} speed={7} speedMul={1} gap={8} captions={true} onOpen={onOpen} />
    </div>
  );
}

// ─── full-page photo viewer: full-bleed image, blue circle prev/next,
// chevron-down + X to close. Tapping the photo itself also advances. ───
function PhotoViewer({ photos, index, onClose, onPrev, onNext }) {
  const open = index != null;
  useEffect(() => {
    const onKey = (e) => {
      if (!open) return;
      if (e.key === "Escape") onClose();
      else if (e.key === "ArrowLeft") onPrev();
      else if (e.key === "ArrowRight") onNext();
    };
    if (open) window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose, onPrev, onNext]);
  useEffect(() => {
    document.body.style.overflow = open ? "hidden" : "";
  }, [open]);
  if (!open) return null;
  const p = photos[index];
  return (
    <div className="pv" onClick={onNext} role="dialog" aria-modal="true">
      <img className="pv__img" src={p.src} alt={p.title} draggable="false" />
      <button className="pv__close" onClick={(e) => { e.stopPropagation(); onClose(); }} aria-label="Close">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M6 9l6 6 6-6" /></svg>
      </button>
      <button className="pv__x" onClick={(e) => { e.stopPropagation(); onClose(); }} aria-label="Close">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
      </button>
      <button className="pv__nav pv__nav--prev" onClick={(e) => { e.stopPropagation(); onPrev(); }} aria-label="Previous photo">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M15 5l-7 7 7 7" /></svg>
      </button>
      <button className="pv__nav pv__nav--next" onClick={(e) => { e.stopPropagation(); onNext(); }} aria-label="Next photo">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M9 5l7 7-7 7" /></svg>
      </button>
    </div>
  );
}

// ─── site header: photo strip + bar (menu, brand link home, view toggle,
// artwork ingress). Used at the top of every page except Home (which
// keeps its own copy directly above the drifting painting rows).
const NAV_PAGES = [
  { key: "Home", href: "Home.html", label: "home" },
  { key: "Artwork", href: "Artwork.html", label: "artwork" },
  { key: "Bio", href: "Bio.html", label: "bio" },
  { key: "Statement", href: "Statement.html", label: "statement" },
  { key: "CV", href: "CV.html", label: "cv" },
  { key: "Contact", href: "Contact.html", label: "contact" },
];
function SiteHeader({ current, view, onView, photoIndex, onOpenPhoto, onClosePhoto, extra }) {
  const here = NAV_PAGES.find((p) => p.key === current) || NAV_PAGES[0];
  const others = NAV_PAGES.filter((p) => p.key !== current);
  return (
    <React.Fragment>
      <div className="hg__bar">
        <div className="hg__bar-left">
          <div className="hg__menu-slot">
            <Pill label={here.label} variant="brand" align="left">
              {(close) => others.map((p) => (
                <a key={p.key} className="pill__item" href={p.href} onClick={close}>{p.label}</a>
              ))}
            </Pill>
          </div>
          <a className="hg__brand" href="Home.html">
            <h2 className="hg__title">tracy boyd · painter</h2>
          </a>
        </div>
        <div className="hg__bar-right">
          {extra}
          <div className="hg__viewtoggle" role="tablist">
            {["All Art", "For Sale"].map((o) => (
              <button
                key={o}
                className={"hg__vt-btn" + (view === o ? " is-active" : "")}
                role="tab"
                aria-selected={view === o}
                onClick={() => onView(o)}
              >{o}</button>
            ))}
          </div>
        </div>
      </div>
      <PhotoViewer
        photos={PHOTOS}
        index={photoIndex}
        onClose={onClosePhoto}
        onPrev={() => onOpenPhoto(PHOTOS[(photoIndex - 1 + PHOTOS.length) % PHOTOS.length])}
        onNext={() => onOpenPhoto(PHOTOS[(photoIndex + 1) % PHOTOS.length])}
      />
    </React.Fragment>
  );
}

// ─── header ──────────────────────────────────────────────────
function Header({ active, onNav, compact, onToggleCompact, navTypo = "exhibitions" }) {
  const exhibitionsLabel = navTypo === "exihibitions" ? "EXIHIBITIONS" : "EXHIBITIONS";
  const items = ["WORK", "ABOUT", exhibitionsLabel, "CONTACT"];
  const activeKey = active === "EXHIBITIONS" || active === "EXIHIBITIONS" ? exhibitionsLabel : active;
  return (
    <header className="site-header">
      <div className="shell site-header__inner">
        <a
          href="#home"
          className="wordmark"
          onClick={(e) => { e.preventDefault(); onNav && onNav("WORK"); window.scrollTo({ top: 0, behavior: "smooth" }); }}
        >
          TRACY BOYD
        </a>
        <nav className="nav" aria-label="Primary">
          {items.map((label) => (
            <a
              key={label}
              href={"#" + label.toLowerCase()}
              className={label === activeKey ? "is-active" : ""}
              onClick={(e) => { e.preventDefault(); onNav && onNav(label === "EXIHIBITIONS" ? "EXHIBITIONS" : label); }}
            >
              {label}
            </a>
          ))}
        </nav>
        <button className="view-toggle" onClick={onToggleCompact} aria-pressed={compact}>
          {compact ? "EXPANDED" : "COMPACT"}
        </button>
      </div>
    </header>
  );
}

// ─── intro ───────────────────────────────────────────────────
function Intro() {
  return (
    <section id="home" className="intro">
      <div className="shell">
        <div className="intro__grid">
          <div />
          <p className="intro__body entry">
            A large scale expressive figurative painter. Her original and fearless painting style spans portrait, landscape and abstract — built in oil, sized to overwhelm a room, and made in the same north-light studio for the past nineteen years.
          </p>
        </div>
      </div>
    </section>
  );
}

// ─── filter row ──────────────────────────────────────────────
function FilterRow({ value, onChange }) {
  return (
    <section className="work">
      <div className="shell">
        <div className="work__eyebrow">SELECTED ART WORK</div>
        <div className="work__head">
          <div className="filters" role="tablist">
            {FILTERS.map((opt) => (
              <button
                key={opt}
                role="tab"
                aria-selected={opt === value}
                className={opt === value ? "is-active" : ""}
                onClick={() => onChange(opt)}
              >
                {opt}
              </button>
            ))}
          </div>
          <a className="zoom-ctl" href="Interactive Gallery.html" aria-label="Open the interactive gallery — zoom out to all works">
            <span className="zoom-ctl__btn">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4"
                   strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <path d="M4 9 V4 H9" />
                <path d="M20 15 V20 H15" />
              </svg>
            </span>
            <span className="zoom-ctl__label">zoom out</span>
          </a>
        </div>
      </div>
    </section>
  );
}

// ─── tile ────────────────────────────────────────────────────
function Tile({ work, size = "small", onOpen }) {
  return (
    <a
      className={`tile tile--${size}`}
      href={`#${work.id}`}
      onClick={(e) => { e.preventDefault(); onOpen(work); }}
    >
      <div className="tile__media">
        {work.chips && size === "hero" && (
          <div className="tile__chips">
            {work.chips.map((c) => <span key={c} className="chip">{c}</span>)}
          </div>
        )}
        <div className="tile__img" style={{ backgroundImage: `url("${work.src}")` }} />
      </div>
      <div className="tile__caption">
        <span className="name">{work.title}</span>
        <span className="sep">|</span>
        <span className="meta">{work.dimension}</span>
        <span className="sep">|</span>
        <span className="meta">{work.year}</span>
      </div>
    </a>
  );
}

// ─── grid: asymmetric (one large + two-up rows) ──────────────
function Grid({ works, compact, onOpen }) {
  if (compact) {
    return (
      <section className="shell">
        <div className="grid grid--compact">
          {works.map((w) => <Tile key={w.id} work={w} size="small" onOpen={onOpen} />)}
        </div>
      </section>
    );
  }
  // group every 3: [hero, small, small]
  const groups = [];
  for (let i = 0; i < works.length; i += 3) groups.push(works.slice(i, i + 3));
  return (
    <section className="shell">
      <div className="grid">
        {groups.map((g, i) => (
          <div className="grid__group" key={i}>
            {g[0] && <Tile work={g[0]} size="hero" onOpen={onOpen} />}
            {(g[1] || g[2]) && (
              <div className="grid__pair">
                {g[1] && <Tile work={g[1]} size="small" onOpen={onOpen} />}
                {g[2] && <Tile work={g[2]} size="small" onOpen={onOpen} />}
              </div>
            )}
          </div>
        ))}
      </div>
    </section>
  );
}

// ─── explore more ────────────────────────────────────────────
function Explore({ items, onOpen }) {
  return (
    <section className="explore">
      <div className="shell">
        <div className="explore__grid">
          <h2 className="explore__title">Explore more</h2>
          <ul className="explore__list">
            {items.map((it, i) => (
              <li
                key={it.id}
                className="explore__row"
                onClick={() => onOpen && onOpen({ ...WORKS[i % WORKS.length], title: it.title, year: it.year })}
              >
                <span className="explore__name">{it.title}</span>
                <span className="explore__meta">{it.year}</span>
              </li>
            ))}
          </ul>
        </div>
      </div>
    </section>
  );
}

// ─── empty state ─────────────────────────────────────────────
function Empty({ filter }) {
  return (
    <div className="shell" style={{ padding: "60px 0 0", color: "rgba(255,255,255,0.45)", fontSize: 15 }}>
      No <span style={{ color: "#fff" }}>{filter.toLowerCase()}</span> work to show at the moment. Check back, or write to the studio.
    </div>
  );
}

// ─── footer ──────────────────────────────────────────────────
function Footer() {
  return (
    <footer className="site-footer">
      <div className="shell site-footer__inner">
        <div className="footer-left">
          <div className="footer-wm">TRACY BOYD</div>
          <div className="footer-note">© {new Date().getFullYear()} Tracy Boyd. All works reproduced with permission. Studio open by appointment.</div>
        </div>
        <div className="footer-cols">
          <div className="footer-col">
            <h4>Follow</h4>
            <a href="https://www.instagram.com/tracy_boyd_art" target="_blank" rel="noopener">Instagram</a>
          </div>
          <div className="footer-col">
            <h4>Contact</h4>
            <a href="mailto:tracyboyd1@gmail.com">tracyboyd1@gmail.com</a>
          </div>
        </div>
      </div>
    </footer>
  );
}

// ─── placeholder for invented pages ──────────────────────────
function Placeholder({ which }) {
  return (
    <section className="shell" style={{ padding: "120px 0", minHeight: "60vh" }}>
      <div style={{ fontSize: 11, letterSpacing: "0.06em", textTransform: "uppercase", color: "rgba(255,255,255,0.45)", marginBottom: 18 }}>
        This page is invented
      </div>
      <h1 style={{ margin: 0, fontWeight: 700, fontSize: 38, color: "#fff", letterSpacing: "-0.005em" }}>
        {which.charAt(0) + which.slice(1).toLowerCase()}
      </h1>
      <p style={{ marginTop: 22, fontSize: 15, lineHeight: 1.45, color: "rgba(255,255,255,0.7)", maxWidth: 575 }}>
        The Figma supplied only the home screen. This placeholder follows the home's gutter, type scale and tone — a real {which.toLowerCase()} page would slot in here without changes to the chrome.
      </p>
    </section>
  );
}

// ─── lightbox / detail overlay ───────────────────────────────
function Overlay({ open, work, onClose, onPrev, onNext }) {
  useEffect(() => {
    const onKey = (e) => {
      if (!open) return;
      if (e.key === "Escape") onClose();
      else if (e.key === "ArrowLeft" && onPrev) onPrev();
      else if (e.key === "ArrowRight" && onNext) onNext();
    };
    if (open) window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose, onPrev, onNext]);
  useEffect(() => {
    document.body.style.overflow = open ? "hidden" : "";
  }, [open]);
  return (
    <div className={`overlay ${open ? "open" : ""}`} onClick={onClose} role="dialog" aria-modal="true">
      {work && onPrev && (
        <button
          className="overlay__nav overlay__nav--prev"
          onClick={(e) => { e.stopPropagation(); onPrev(); }}
          aria-label="Previous painting"
        >
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 5l-7 7 7 7" /></svg>
        </button>
      )}
      {work && onNext && (
        <button
          className="overlay__nav overlay__nav--next"
          onClick={(e) => { e.stopPropagation(); onNext(); }}
          aria-label="Next painting"
        >
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 5l7 7-7 7" /></svg>
        </button>
      )}
      {work && (
        <div className="overlay__shell" onClick={(e) => e.stopPropagation()}>
          <button className="overlay__close" onClick={onClose}>CLOSE</button>
          <div className="overlay__body">
            <div
              className={"overlay__img" + (work.photo && onNext ? " is-tappable" : "")}
              style={{ backgroundImage: `url("${work.src}")` }}
              onClick={work.photo && onNext ? onNext : undefined}
            />
            <div className="overlay__meta">
              <div className="overlay__tags">{(work.chips || []).join(" · ")}</div>
              <h2 className="overlay__title">{work.title}</h2>
              {work.photo ? (
                <p className="overlay__note">{work.note}</p>
              ) : (
                <React.Fragment>
                  <dl className="overlay__spec">
                    <div><dt>Dimension</dt><dd>{work.dimension}</dd></div>
                    <div><dt>Year</dt><dd>{work.year}</dd></div>
                    <div><dt>Medium</dt><dd>{work.medium}</dd></div>
                    <div><dt>Timeframe</dt><dd>{work.timeframe}</dd></div>
                  </dl>
                  <p className="overlay__note">{work.note}</p>
                  <a href="mailto:tracyboyd1@gmail.com" className="overlay__cta">Enquire →</a>
                </React.Fragment>
              )}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ─── full-page artwork viewer: full-bleed, uncropped painting on black,
// meta card + For Sale tag + Similar Art ingress bottom-right. ───────
function ArtworkViewer({ work, onClose, onPrev, onNext }) {
  const open = !!work;
  useEffect(() => {
    const onKey = (e) => {
      if (!open) return;
      if (e.key === "Escape") onClose();
      else if (e.key === "ArrowLeft" && onPrev) onPrev();
      else if (e.key === "ArrowRight" && onNext) onNext();
    };
    if (open) window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose, onPrev, onNext]);
  useEffect(() => {
    document.body.style.overflow = open ? "hidden" : "";
  }, [open]);
  if (!open) return null;
  const forSale = work.status === "Available";
  return (
    <div className="aw" onClick={onClose} role="dialog" aria-modal="true">
      <img className="aw__img" src={work.src} alt={work.title} draggable="false" />

      <button className="aw__x" onClick={(e) => { e.stopPropagation(); onClose(); }} aria-label="Close">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
      </button>

      {onPrev && (
        <button className="aw__nav aw__nav--prev" onClick={(e) => { e.stopPropagation(); onPrev(); }} aria-label="Previous artwork">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M15 5l-7 7 7 7" /></svg>
        </button>
      )}
      {onNext && (
        <button className="aw__nav aw__nav--next" onClick={(e) => { e.stopPropagation(); onNext(); }} aria-label="Next artwork">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M9 5l7 7-7 7" /></svg>
        </button>
      )}

      <div className="aw__meta" onClick={(e) => e.stopPropagation()}>
        <div className="aw__tags">
          <span className={"aw__tag" + (forSale ? " is-sale" : "")}>{forSale ? "For Sale" : (work.status || "Sold")}</span>
          {work.category && (
            <a className="aw__similar" href={`Artwork.html?category=${encodeURIComponent(work.category)}`}>
              Similar Art
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 5l7 7-7 7" /></svg>
            </a>
          )}
        </div>
        <div className="aw__card">
          <h2 className="aw__title">{work.title}</h2>
          <dl className="aw__spec">
            <div><dt>Dimensions</dt><dd>{work.dimension}</dd></div>
            <div><dt>Timeframe</dt><dd>{work.timeframe}</dd></div>
          </dl>
        </div>
      </div>
    </div>
  );
}

// expose globally
Object.assign(window, {
  Header, Intro, FilterRow, Grid, Explore, Footer, Placeholder, Overlay, ArtworkViewer, Empty,
  Pill, FloatRow, PhotoBar, PhotoViewer, SiteHeader, NAV_PAGES,
  WORKS, MORE, FILTERS, PHOTOS,
});
