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

// ----- i18n -----
const I18N = {
  en: {
    'series.nus':        'Nudes',
    'series.erotiques':  'Erotic Works',
    'series.peintures':  'Paintings',
    'series.portraits':  'Portraits',
    'series.penombres':  'Penumbra',
    'series.autres':     'Other Works',
    'tagline':           'A gallery of drawings and paintings — on bodies, portraits, and what remains in shadow.',
    'enter':             'Enter the gallery →',
    'containsNudity':    'Contains artistic nudity',
    'open':              'Open ↗',
    'view':              'View ↗',
    'backToSeries':      '← Back to series',
    'works':             'works',
    'cover':             'Cover',
    'about':             'About',
    'aboutText':         'Drawings and paintings, from 1991 to 2026. Pencil, ink, gouache. The artist remains in the shadows, by choice, not by mystery.',
    'legal':             'Legal notice',
    'legalP1':           'Black Iris Gallery is a personal, non-commercial site. Editorship remains anonymous by artistic choice.',
    'legalP2':           'For any legal, editorial, or other inquiry: contact@blackirisgallery.com',
    'legalP3':           'Hosting: Cloudflare, Inc., 101 Townsend St., San Francisco, CA 94107, United States.',
    'previous':          'Previous',
    'next':              'Next',
    'close':             'Close · Esc',
    'clickToReveal':     'Click to reveal',
    'nudeContent':       'Artistic nude content',
    'blurred':           '· blurred',
    'visible':           '· visible',
    'reveal':            'Reveal',
    'blur':              'Blur',
    'frames':            'works',
    'copyright':         '© 2026 Black Iris Gallery',
  },
  fr: {
    'series.nus':        'Nus',
    'series.erotiques':  'Érotiques',
    'series.peintures':  'Peintures',
    'series.portraits':  'Portraits',
    'series.penombres':  'Pénombres',
    'series.autres':     'Autres',
    'tagline':           'Une galerie de dessins et peintures — corps, portraits, et ce qui reste dans l\u2019ombre.',
    'enter':             'Entrer dans la galerie →',
    'containsNudity':    'Contient des nus artistiques',
    'open':              'Ouvrir ↗',
    'view':              'Voir ↗',
    'backToSeries':      '← Retour aux séries',
    'works':             'œuvres',
    'cover':             'Couverture',
    'about':             'À propos',
    'aboutText':         'Dessins et peintures, de 1991 à 2026. Crayon, encre, gouache. L’artiste reste dans l’ombre, par préférence, pas par mystère.',
    'legal':             'Mentions',
    'legalP1':           'Black Iris Gallery est un site personnel à but non commercial. L’édition reste anonyme par choix artistique.',
    'legalP2':           'Pour toute demande légale, éditoriale ou autre : contact@blackirisgallery.com',
    'legalP3':           'Hébergement : Cloudflare, Inc., 101 Townsend St., San Francisco, CA 94107, États-Unis.',
    'previous':          'Précédent',
    'next':              'Suivant',
    'close':             'Fermer · Esc',
    'clickToReveal':     'Cliquer pour révéler',
    'nudeContent':       'Contenu artistique — nus',
    'blurred':           '· voilé',
    'visible':           '· visible',
    'reveal':            'Révéler',
    'blur':              'Voiler',
    'frames':            'œuvres',
    'copyright':         '© 2026 Black Iris Gallery',
  },
};

// English number words 0–12 (we won't need more).
const NUM_WORDS_EN = ['zero','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve'];
const NUM_WORDS_FR = ['zéro','un','deux','trois','quatre','cinq','six','sept','huit','neuf','dix','onze','douze'];
function numWord(n, lang = 'en') {
  const arr = lang === 'fr' ? NUM_WORDS_FR : NUM_WORDS_EN;
  return arr[n] || String(n);
}

const LangContext = createContext({ lang: 'en', setLang: () => {} });
function useLang() { return useContext(LangContext); }
function useT() {
  const { lang } = useLang();
  return useCallback((key) => (I18N[lang] && I18N[lang][key]) || I18N.en[key] || key, [lang]);
}

// Format an ISO date into a long-form, locale-aware string. Accepts:
//   "2010"        → "2010"
//   "2010-09"     → "September 2010" / "septembre 2010"
//   "2010-09-21"  → "21 September 2010" / "21 septembre 2010"
// Returns null if no date.
const MONTHS_FR = ['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
function formatDate(iso, lang = 'en') {
  if (!iso) return null;
  const months = lang === 'fr' ? MONTHS_FR : MONTHS_EN;
  const parts = String(iso).split('-').map(Number);
  const [y, m, d] = parts;
  if (d) {
    if (lang === 'fr') {
      return d === 1 ? `1er ${months[m-1]} ${y}` : `${d} ${months[m-1]} ${y}`;
    }
    return `${d} ${months[m-1]} ${y}`;
  }
  if (m) return `${months[m-1]} ${y}`;
  return String(y);
}
function yearOf(iso) {
  if (!iso) return null;
  return String(iso).slice(0, 4);
}

// Sort works newest first. Real works (with `date`) come before placeholders.
// Works without a date stay at the bottom of the real works.
function sortByDateDesc(arr) {
  return [...arr].sort((a, b) => {
    if (a.placeholder && !b.placeholder) return 1;
    if (!a.placeholder && b.placeholder) return -1;
    if (a.placeholder && b.placeholder) return 0;
    const ad = a.date || '';
    const bd = b.date || '';
    if (ad === bd) return 0;
    return bd.localeCompare(ad);
  });
}

// ----- Series data (drawings & paintings) -----

// Placeholder helper for slots we haven't filled yet.
function makePlaceholders(label, specs) {
  return specs.map((s, i) => ({
    placeholder: true,
    label: `${label} / ${String(i+1).padStart(2,'0')}`,
    w: s[0], h: s[1],
  }));
}

const NUS = sortByDateDesc([
  { src: 'drawings/nus/01.jpg', w: 4888, h: 6577, nude: true, date: '2023-09-01' },
  { src: 'drawings/nus/02.jpg', w: 4822, h: 6451, nude: true, date: '2023-12-27' },
  { src: 'drawings/nus/03.jpg', w: 4917, h: 6608, nude: true, date: '2010' },
  { src: 'drawings/nus/04.jpg', w: 4625, h: 6081, nude: true, date: '2024-03-15' },
  { src: 'drawings/nus/05.jpg', w: 4652, h: 3300, nude: true, date: '2000' },
  { src: 'drawings/nus/06.jpg', w: 4680, h: 3400, nude: true, date: '2000' },
  { src: 'drawings/nus/07.jpg', w: 4653, h: 3315, nude: true, date: '2000' },
  { src: 'drawings/nus/08.jpg', w: 3297, h: 2477, nude: true, date: '2006' },
  { src: 'drawings/nus/09.jpg', w: 3301, h: 2546, nude: true, date: '2006' },
  { src: 'drawings/nus/10.jpg', w: 6649, h: 4912, nude: true, date: '2024-01-03' },
  { src: 'drawings/nus/11.jpg', w: 4000, h: 5388, nude: true, date: '2026-05-21' },
  { src: 'drawings/nus/12.jpg', w: 5308, h: 3942, nude: true, date: '2023-12-19' },
  { src: 'drawings/nus/13.jpg', w: 6639, h: 4912, nude: true, date: '2006' },
  { src: 'drawings/nus/14.jpg', w: 3312, h: 2450, nude: true, date: '2005' },
  { src: 'drawings/nus/15.jpg', w: 1649, h: 2220, nude: true, date: '2004' },
  { src: 'drawings/nus/16.jpg', w: 6518, h: 4625, date: '2023-08' },
  { src: 'drawings/nus/17.jpg', w: 4946, h: 6663, date: '2023-08-28' },
  { src: 'drawings/nus/18.jpg', w: 4919, h: 6605, nude: true, date: '2023-12-31' },
  { src: 'drawings/nus/19.jpg', w: 5337, h: 4493, nude: true, date: '2003' },
  { src: 'drawings/nus/20.jpg', w: 5854, h: 4537, nude: true, date: '2008' },
  { src: 'drawings/nus/21.jpg', w: 4100, h: 5595, date: '2008' },
  { src: 'drawings/nus/22.jpg', w: 4762, h: 3510, nude: true, date: '2005' },
  { src: 'drawings/nus/23.jpg', w: 4517, h: 6126, date: '2023-12-23' },
  { src: 'drawings/nus/24.jpg', w: 2552, h: 3441, nude: true, date: '1998-08-21' },
  { src: 'drawings/nus/25.jpg', w: 4927, h: 6628, nude: true, date: '2006' },
]);

const EROTIQUES = sortByDateDesc([
  { src: 'drawings/erotiques/02.jpg', w: 6628, h: 4933, nude: true, date: '2010' },
  { src: 'drawings/erotiques/03.jpg', w: 6644, h: 4936, nude: true, date: '2010' },
  { src: 'drawings/erotiques/04.jpg', w: 5548, h: 4349, nude: true, date: '2009' },
  { src: 'drawings/erotiques/05.jpg', w: 6627, h: 4920, nude: true, date: '2024-01-03' },
  { src: 'drawings/erotiques/06.jpg', w: 5176, h: 3732, nude: true, date: '2009' },
  { src: 'drawings/erotiques/07.jpg', w: 4937, h: 6615, nude: true, date: '2024-01-03' },
]);

const PEINTURES = sortByDateDesc([
  { src: 'drawings/peintures/01.jpg', w: 1353, h: 4948, date: '1998-06-18' },
  { src: 'drawings/peintures/02.jpg', w: 4955, h: 7003, date: '2002' },
]);
const PORTRAITS = sortByDateDesc([
  { src: 'drawings/portraits/01.jpg', w: 3176, h: 4927, date: '2023-09-21' },
  { src: 'drawings/portraits/02.jpg', w: 5820, h: 5096, date: '2008' },
  { src: 'drawings/portraits/03.jpg', w: 4662, h: 3308, date: '1991-09-22' },
]);
const PENOMBRES = sortByDateDesc([
  { src: 'drawings/penombres/01.jpg', w: 4872, h: 6450, date: '2023-12-04' },
  { src: 'drawings/penombres/02.jpg', w: 4910, h: 5252, date: '2023-11-27' },
  { src: 'drawings/penombres/03.jpg', w: 4926, h: 6652, date: '2023-12-07' },
  { src: 'drawings/penombres/05.jpg', w: 4937, h: 6557, date: '2023-12-12' },
  { src: 'drawings/penombres/06.jpg', w: 6340, h: 4743, date: '2023-12-18' },
  { src: 'drawings/penombres/07.jpg', w: 6573, h: 4893, date: '2023-12-15' },
]);
const AUTRES = sortByDateDesc([
  { src: 'drawings/autres/01.jpg', w: 6215, h: 4630, date: '2024-01-02' },
  { src: 'drawings/autres/02.jpg', w: 4914, h: 4920, date: '2010' },
  { src: 'drawings/autres/03.jpg', w: 6267, h: 4675, date: '2023-12-20' },
]);

// Count of real (non-placeholder) works in a series.
const realCount = (arr) => arr.filter(p => !p.placeholder).length;

const SERIES = [
  { id: 'portraits',  title: 'Portraits',  count: realCount(PORTRAITS),  photos: PORTRAITS,  coverIndex: 0 },
  { id: 'nus',        title: 'Nus',        count: realCount(NUS),        photos: NUS,        coverIndex: 8 },
  { id: 'penombres',  title: 'Pénombres',  count: realCount(PENOMBRES),  photos: PENOMBRES,  coverIndex: 2 },
  { id: 'peintures',  title: 'Peintures',  count: realCount(PEINTURES),  photos: PEINTURES,  coverIndex: 0 },
  { id: 'erotiques',  title: 'Érotiques',  count: realCount(EROTIQUES),  photos: EROTIQUES,  coverIndex: 0 },
  { id: 'autres',     title: 'Autres',     count: realCount(AUTRES),     photos: AUTRES,     coverIndex: 0 },
];

// ----- Theme tokens -----
// Three tones × three backgrounds. Tone changes type system + chrome.
// Background changes paper.
const TONES = {
  gallery: {
    label: 'Gallery',
    serif: '"EB Garamond", "Cormorant Garamond", Georgia, serif',
    sans:  '"Inter Tight", "Helvetica Neue", Helvetica, Arial, sans-serif',
    mono:  '"JetBrains Mono", ui-monospace, monospace',
    titleFamily: 'serif', titleWeight: 400, titleTracking: '-0.02em', titleStyle: 'italic',
    navFamily: 'sans', navWeight: 400, navTracking: '0.14em', navUpper: true, navSize: 11,
    ratio: 'tight',
  },
  editorial: {
    label: 'Editorial',
    serif: '"Fraunces", "Times New Roman", serif',
    sans:  '"Inter Tight", system-ui, sans-serif',
    mono:  '"JetBrains Mono", ui-monospace, monospace',
    titleFamily: 'serif', titleWeight: 300, titleTracking: '-0.04em', titleStyle: 'normal',
    navFamily: 'serif', navWeight: 400, navTracking: '0', navUpper: false, navSize: 18,
    ratio: 'editorial',
  },
  brutalist: {
    label: 'Brutalist',
    serif: '"Space Grotesk", "Helvetica Neue", sans-serif',
    sans:  '"Space Grotesk", "Helvetica Neue", sans-serif',
    mono:  '"JetBrains Mono", ui-monospace, monospace',
    titleFamily: 'sans', titleWeight: 700, titleTracking: '-0.05em', titleStyle: 'normal',
    navFamily: 'mono', navWeight: 500, navTracking: '0.02em', navUpper: true, navSize: 11,
    ratio: 'brutalist',
  },
};

const PAPERS = {
  bone:  { bg: '#f3efe8', fg: '#1a1815', muted: '#88827a', rule: '#1a181522', accent: '#1a1815' },
  paper: { bg: '#ece6d6', fg: '#231f17', muted: '#7e7665', rule: '#231f1722', accent: '#231f17' },
  ink:   { bg: '#0e0e0e', fg: '#ebe7df', muted: '#7a7770', rule: '#ebe7df22', accent: '#ebe7df' },
  white: { bg: '#fafaf8', fg: '#0e0e0e', muted: '#9a9690', rule: '#0e0e0e1f', accent: '#0e0e0e' },
};

// Layouts: how photos arrange inside a series.
const LAYOUTS = {
  grid:       { label: 'Even grid',     desc: 'Regular columns' },
  editorial:  { label: 'Editorial',     desc: 'Mixed sizes, asymmetric' },
  river:      { label: 'River',         desc: 'Single column, full width' },
  filmstrip:  { label: 'Filmstrip',     desc: 'Horizontal scroll' },
};

// ----- Tweaks -----
const DEFAULTS = /*EDITMODE-BEGIN*/{
  "tone": "gallery",
  "paper": "ink",
  "layout": "grid",
  "showIndex": true,
  "blurNudes": true
}/*EDITMODE-END*/;

// ----- Layout components -----
function Photo({ photo, onClick, fit = 'cover', priority = false, blur = false, slotAr = null, zoom = false, thumb = false }) {
  if (photo.placeholder) {
    return <PlaceholderTile photo={photo} onClick={onClick} />;
  }
  // Per-photo opt-out: a photo without `nude:true` is never blurred even if
  // `blur` is on for the series.
  const apply = blur && photo.nude;
  // If the photo is squarer or wider than its slot, fit:contain it and center
  // vertically so we don't crop off the top/bottom. slotAr is "w/h" string or
  // a number; defaults to portrait 2/3 if not provided.
  let useFit = fit;
  if (fit === 'cover' && slotAr) {
    const photoAr = photo.w / photo.h;
    const slot = typeof slotAr === 'number' ? slotAr
      : (() => { const [a,b] = String(slotAr).split('/').map(Number); return a/b; })();
    // Photo is less portrait than the slot (i.e. squarer or landscape) → contain.
    if (photoAr >= slot * 0.98) useFit = 'contain';
  }
  return (
    <div className={"photo-tile" + (zoom ? " photo-zoom" : "")} onClick={onClick}
      onContextMenu={(e) => e.preventDefault()}
      onDragStart={(e) => e.preventDefault()}
      style={{
      position:'relative', overflow:'hidden',
      background:'var(--rule)',
      width:'100%', height:'100%',
      display:'flex', alignItems:'center', justifyContent:'center',
    }}>
      <img src={thumb && photo.src ? photo.src.replace(/\.jpg$/i, '_thumb.jpg') : photo.src} loading={priority ? 'eager' : 'lazy'} style={{
        width:'100%', height:'100%', objectFit: useFit, objectPosition: 'center center', display:'block',
        filter: apply ? 'blur(28px)' : 'none',
        transform: apply ? 'scale(1.06)' : 'none',
        transition: 'filter 320ms ease, transform 320ms ease',
      }}/>
    </div>
  );
}

// PhotoCard wraps a Photo with the hover treatment used on series pages:
// rise + filet + meta line ("07" / "View ↗") fading in below the image.
function PhotoCard({ photo, index, total, onClick, fit, priority, blur, slotAr, ar }) {
  const t = useT();
  return (
    <button
      onClick={onClick}
      onContextMenu={(e) => e.preventDefault()}
      onDragStart={(e) => e.preventDefault()}
      className="photo-card"
      style={{
        all: 'unset', cursor: 'pointer', display: 'flex', flexDirection: 'column',
        width: '100%',
      }}
    >
      <div style={{aspectRatio: ar, width: '100%', overflow: 'hidden'}}>
        <Photo photo={photo} fit={fit} priority={priority} blur={blur} slotAr={slotAr} thumb/>
      </div>
      {!photo.placeholder && (
        <>
          <span className="photo-card-rule" aria-hidden="true"></span>
          <div className="photo-card-meta" style={{
            fontFamily: 'var(--mono)', fontSize: 10,
            letterSpacing: '0.22em', textTransform: 'uppercase',
            color: 'var(--fg)',
            display: 'flex', justifyContent: 'space-between',
          }}>
            <span>
              {String(index + 1).padStart(2, '0')}
              {photo.date && <span style={{marginLeft: 10, color:'var(--muted)'}}>{yearOf(photo.date)}</span>}
            </span>
            <span>{t('view')}</span>
          </div>
        </>
      )}
    </button>
  );
}

function PlaceholderTile({ photo, onClick }) {
  return (
    <div className="placeholder-tile" onClick={onClick} style={{
      cursor:'pointer', position:'relative', width:'100%', height:'100%',
      background:'repeating-linear-gradient(135deg, var(--rule) 0 1px, transparent 1px 8px)',
      border: '1px solid var(--rule)',
      display:'flex', alignItems:'center', justifyContent:'center',
      fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--muted)',
      letterSpacing: '0.1em', textTransform: 'uppercase',
    }}>
      <span>{photo.label}</span>
    </div>
  );
}

// ----- Per-layout grids -----
function GridLayout({ photos, onOpen, blur }) {
  return (
    <div style={{
      display:'grid',
      gridTemplateColumns: 'repeat(3, 1fr)',
      gap: 'var(--gap)',
    }}>
      {photos.map((p, i) => (
        <PhotoCard key={i} photo={p} index={i} total={photos.length}
          onClick={() => onOpen(i)} priority={i < 3} blur={blur} slotAr="2/3" ar="2/3"/>
      ))}
    </div>
  );
}

function EditorialLayout({ photos, onOpen, blur }) {
  // 12-col grid with a hand-tuned rhythm. Cycles through patterns so it works
  // for any photo count.
  const patterns = [
    { col: 'span 5', row: 'span 2', ar: '2/3' },
    { col: 'span 7', row: 'span 1', ar: '3/2' },
    { col: 'span 4', row: 'span 1', ar: '1/1' },
    { col: 'span 3', row: 'span 1', ar: '2/3' },
    { col: 'span 8', row: 'span 2', ar: '3/2' },
    { col: 'span 4', row: 'span 1', ar: '2/3' },
    { col: 'span 6', row: 'span 1', ar: '3/2' },
    { col: 'span 6', row: 'span 1', ar: '3/2' },
    { col: 'span 4', row: 'span 2', ar: '2/3' },
    { col: 'span 8', row: 'span 1', ar: '16/9' },
    { col: 'span 5', row: 'span 1', ar: '2/3' },
    { col: 'span 7', row: 'span 1', ar: '3/2' },
  ];
  return (
    <div style={{
      display:'grid',
      gridTemplateColumns: 'repeat(12, 1fr)',
      gridAutoRows: 'minmax(180px, auto)',
      gap: 'var(--gap)',
    }}>
      {photos.map((p, i) => {
        const pat = patterns[i % patterns.length];
        return (
          <div key={i} style={{gridColumn: pat.col}}>
            <PhotoCard photo={p} index={i} total={photos.length}
              onClick={() => onOpen(i)} priority={i < 4} blur={blur} slotAr={pat.ar} ar={pat.ar}/>
          </div>
        );
      })}
    </div>
  );
}

function RiverLayout({ photos, onOpen, blur }) {
  return (
    <div style={{display:'flex', flexDirection:'column', gap:'calc(var(--gap) * 4)', alignItems:'center'}}>
      {photos.map((p, i) => (
        <div key={i} style={{
          width: i % 3 === 0 ? '90%' : i % 3 === 1 ? '60%' : '75%',
        }}>
          <PhotoCard photo={p} index={i} total={photos.length}
            onClick={() => onOpen(i)} fit="cover" priority={i < 2} blur={blur}
            slotAr={p.placeholder ? `${p.w}/${p.h}` : '2/3'}
            ar={p.placeholder ? `${p.w}/${p.h}` : '2/3'}/>
        </div>
      ))}
    </div>
  );
}

function FilmstripLayout({ photos, onOpen, blur }) {
  return (
    <div style={{
      display:'flex', gap:'var(--gap)', overflowX:'auto', overflowY:'hidden',
      paddingBottom: 24, scrollSnapType: 'x mandatory',
    }}>
      {photos.map((p, i) => (
        <div key={i} style={{
          flex: '0 0 auto',
          height: 'min(72vh, 720px)',
          aspectRatio: '2/3',
          scrollSnapAlign: 'start',
        }}>
          <PhotoCard photo={p} index={i} total={photos.length}
            onClick={() => onOpen(i)} priority={i < 2} blur={blur} slotAr="2/3" ar="2/3"/>
        </div>
      ))}
    </div>
  );
}

const LAYOUT_COMPS = {
  grid: GridLayout,
  editorial: EditorialLayout,
  river: RiverLayout,
  filmstrip: FilmstripLayout,
};

// ----- Content notice toggle -----
function ContentNotice({ blurNudes, onToggle, compact = false }) {
  const t = useT();
  return (
    <div style={{
      display:'flex', alignItems:'center', justifyContent: 'space-between', gap: 24,
      padding: compact ? '10px 14px' : '14px 18px',
      border: '1px solid var(--rule)',
      marginBottom: compact ? 24 : 40,
      fontFamily: 'var(--mono)', fontSize: 11,
      letterSpacing: '0.14em', textTransform: 'uppercase',
      color: 'var(--muted)',
    }}>
      <span style={{display:'flex', alignItems:'center', gap: 10}}>
        <span style={{
          display:'inline-block', width: 6, height: 6, borderRadius: '50%',
          background: 'var(--fg)', opacity: 0.45,
        }}/>
        {t('nudeContent')} {blurNudes ? t('blurred') : t('visible')}
      </span>
      <button onClick={onToggle} style={{
        all: 'unset', cursor: 'pointer',
        padding: '6px 14px',
        border: '1px solid var(--rule)',
        color: 'var(--fg)',
        fontFamily: 'var(--mono)', fontSize: 11,
        letterSpacing: '0.14em', textTransform: 'uppercase',
      }}>
        {blurNudes ? t('reveal') : t('blur')}
      </button>
    </div>
  );
}

// A series "contains nudes" if any photo is flagged. Used to decide whether
// to show the content notice on the index/series page.
function hasNudePhoto(series) {
  return series.photos.some(p => p && p.nude);
}

// ----- Landing -----
function Landing({ tone, onEnter }) {
  const { lang, setLang } = useLang();
  const t = useT();
  const seriesCount = SERIES.length;
  const worksCount = SERIES.reduce((sum, s) => sum + s.count, 0);
  const seriesWord = lang === 'fr' ? 'séries' : 'series';
  return (
    <div style={{
      minHeight: '100vh',
      display: 'grid',
      gridTemplateColumns: '1fr 1fr',
      gap: 0,
    }}>
      {/* Left: title block */}
      <div style={{
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'space-between',
        padding: '48px 64px',
      }}>
        {/* Top: mark + lang toggle */}
        <div style={{
          display:'flex', justifyContent:'space-between', alignItems:'center',
        }}>
          <div style={{
            fontFamily: 'var(--mono)',
            fontSize: 11,
            letterSpacing: '0.22em',
            textTransform: 'uppercase',
            color: 'var(--muted)',
          }}>
            Black Iris Gallery
          </div>
          <div style={{
            display:'flex', alignItems:'center',
            border:'1px solid var(--rule)',
            fontFamily:'var(--mono)', fontSize: 11,
            letterSpacing: '0.22em', textTransform: 'uppercase',
          }}>
            <button onClick={() => setLang('en')} style={{
              all:'unset', cursor:'pointer', padding:'6px 10px',
              opacity: lang === 'en' ? 1 : 0.4,
              background: lang === 'en' ? 'var(--rule)' : 'transparent',
              color: 'var(--fg)',
              transition:'opacity 150ms ease, background 150ms ease',
            }}>EN</button>
            <button onClick={() => setLang('fr')} style={{
              all:'unset', cursor:'pointer', padding:'6px 10px',
              opacity: lang === 'fr' ? 1 : 0.4,
              background: lang === 'fr' ? 'var(--rule)' : 'transparent',
              color: 'var(--fg)',
              transition:'opacity 150ms ease, background 150ms ease',
            }}>FR</button>
          </div>
        </div>

        {/* Center: title + tagline */}
        <div style={{ maxWidth: 560 }}>
          <h1 style={{
            fontFamily: 'var(--serif)',
            fontWeight: 400,
            fontSize: 'clamp(56px, 7.4vw, 116px)',
            lineHeight: 0.96,
            letterSpacing: '-0.025em',
            margin: 0,
            color: 'var(--fg)',
          }}>
            <span style={{display:'block'}}>Black</span>
            <span style={{display:'block', fontStyle:'italic'}}>Iris</span>
          </h1>
          <p style={{
            marginTop: 36,
            fontFamily: 'var(--serif)',
            fontSize: 18,
            lineHeight: 1.5,
            color: 'var(--fg)',
            opacity: 0.78,
            maxWidth: 440,
            fontStyle: 'italic',
          }}>
            {t('tagline')}
          </p>
        </div>

        {/* Bottom: enter + meta */}
        <div style={{
          display:'flex', alignItems:'flex-end', justifyContent:'space-between', gap: 24,
        }}>
          <button onClick={onEnter} style={{
            all:'unset', cursor:'pointer',
            fontFamily: 'var(--mono)',
            fontSize: 12,
            letterSpacing: '0.32em',
            textTransform: 'uppercase',
            color: 'var(--fg)',
            padding: '18px 28px',
            border: '1px solid var(--fg)',
            transition: 'background 150ms ease, color 150ms ease',
          }}
          onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--fg)'; e.currentTarget.style.color = 'var(--bg)'; }}
          onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--fg)'; }}
          >
            {t('enter')}
          </button>
          <div style={{
            fontFamily: 'var(--mono)',
            fontSize: 10,
            letterSpacing: '0.18em',
            textTransform: 'uppercase',
            color: 'var(--muted)',
            textAlign:'right',
            lineHeight: 1.7,
          }}>
            {numWord(seriesCount, lang).replace(/^\w/, c => c.toUpperCase())} {seriesWord} · {worksCount} {t('works')}<br/>
            {t('containsNudity')}
          </div>
        </div>
      </div>

      {/* Right: cover photo, framed in its own portrait aspect */}
      <div style={{
        position:'relative',
        background: '#000',
        display:'flex',
        alignItems:'center',
        justifyContent:'center',
        padding: '48px 64px',
      }}
        onContextMenu={(e) => e.preventDefault()}
        onDragStart={(e) => e.preventDefault()}
      >
        <img
          src="drawings/cover.jpg"
          alt=""
          data-protected="true"
          style={{
            display:'block',
            maxWidth:'100%',
            maxHeight:'100%',
            width:'auto',
            height:'min(88vh, calc((100vh - 96px)))',
            aspectRatio: '4517 / 6126',
            objectFit:'contain',
          }}
        />
      </div>
    </div>
  );
}

// ----- About -----
function AboutView({ tone, onBack }) {
  const t = useT();
  return (
    <section style={{
      minHeight: '60vh',
      display:'flex', flexDirection:'column', justifyContent:'center',
      maxWidth: 720, margin: '0 auto',
      paddingTop: 32, paddingBottom: 64,
    }}>
      <div style={{
        fontFamily:'var(--mono)', fontSize: 10,
        letterSpacing:'0.22em', textTransform:'uppercase',
        color:'var(--muted)', marginBottom: 32,
      }}>
        {t('about')}
      </div>
      <p style={{
        margin: 0,
        fontFamily: 'var(--serif)',
        fontStyle: 'italic',
        fontSize: 'clamp(20px, 2vw, 28px)',
        lineHeight: 1.5,
        color:'var(--fg)',
      }}>
        {t('aboutText')}
      </p>
      <div style={{
        marginTop: 56, paddingTop: 24,
        borderTop:'1px solid var(--rule)',
        display:'flex', justifyContent:'space-between', alignItems:'baseline',
        fontFamily:'var(--mono)', fontSize: 10,
        letterSpacing:'0.22em', textTransform:'uppercase',
        color:'var(--muted)',
      }}>
        <span>contact@blackirisgallery.com</span>
        <button onClick={onBack} style={{
          all:'unset', cursor:'pointer', color:'var(--fg)', opacity: 0.7,
          padding:'8px 14px', border:'1px solid var(--rule)',
        }}
        onMouseEnter={(e) => { e.currentTarget.style.opacity = 1; }}
        onMouseLeave={(e) => { e.currentTarget.style.opacity = 0.7; }}
        >{t('backToSeries')}</button>
      </div>
    </section>
  );
}

// ----- Legal notice -----
function LegalView({ tone, onBack }) {
  const t = useT();
  return (
    <section style={{
      minHeight: '60vh',
      maxWidth: 720, margin: '0 auto',
      paddingTop: 32, paddingBottom: 64,
    }}>
      <div style={{
        fontFamily:'var(--mono)', fontSize: 10,
        letterSpacing:'0.22em', textTransform:'uppercase',
        color:'var(--muted)', marginBottom: 32,
      }}>
        {t('legal')}
      </div>
      <div style={{
        fontFamily:'var(--serif)', fontSize: 16, lineHeight: 1.7,
        color:'var(--fg)',
      }}>
        <p style={{marginTop: 0}}>{t('legalP1')}</p>
        <p>{t('legalP2')}</p>
        <p style={{marginBottom: 0, color:'var(--muted)', fontSize: 14}}>{t('legalP3')}</p>
      </div>
      <div style={{
        marginTop: 56, paddingTop: 24,
        borderTop:'1px solid var(--rule)',
        display:'flex', justifyContent:'flex-end',
        fontFamily:'var(--mono)', fontSize: 10,
        letterSpacing:'0.22em', textTransform:'uppercase',
      }}>
        <button onClick={onBack} style={{
          all:'unset', cursor:'pointer', color:'var(--fg)', opacity: 0.7,
          padding:'8px 14px', border:'1px solid var(--rule)',
        }}
        onMouseEnter={(e) => { e.currentTarget.style.opacity = 1; }}
        onMouseLeave={(e) => { e.currentTarget.style.opacity = 0.7; }}
        >{t('backToSeries')}</button>
      </div>
    </section>
  );
}

// ----- Index (home) -----
function IndexView({ tone, onOpen, blurNudes, onToggleBlur }) {
  const t = useT();
  const hasNude = SERIES.some(hasNudePhoto);
  return (
    <section style={{paddingTop: 24}}>
      {hasNude && <ContentNotice blurNudes={blurNudes} onToggle={onToggleBlur}/>}
      <div style={{
        display:'grid',
        gridTemplateColumns: 'repeat(3, 1fr)',
        rowGap: 80,
        columnGap: 48,
      }}>
        {SERIES.map(s => {
          const cover = s.photos[s.coverIndex || 0];
          const blur = !!blurNudes;
          return (
            <button key={s.id} onClick={() => onOpen(s.id)} style={{
              all:'unset', cursor:'pointer', display:'flex', flexDirection:'column',
              gap: 14,
            }} className="series-card">
              <div style={{aspectRatio: '3/4', width:'100%', overflow:'hidden'}}>
                <Photo photo={cover} blur={blur} slotAr="3/4" thumb/>
              </div>
              <div style={{display:'flex', justifyContent:'space-between', alignItems:'baseline', gap:16}}>
                <h3 className="series-card-title" style={{
                  margin:0,
                  fontFamily: tone.titleFamily === 'serif' ? 'var(--serif)' : 'var(--sans)',
                  fontWeight: tone.titleWeight,
                  fontStyle: tone.titleStyle,
                  letterSpacing: tone.titleTracking,
                  fontSize: 'clamp(28px, 3vw, 44px)',
                  lineHeight: 1,
                }}>{t(`series.${s.id}`)}</h3>
                <span style={{
                  fontFamily: 'var(--mono)', fontSize: 11, color:'var(--muted)',
                  letterSpacing: '0.12em',
                }}>{String(s.count).padStart(2,'0')}</span>
              </div>
              <span className="series-card-rule" aria-hidden="true"></span>
              <div className="series-card-meta" style={{
                fontFamily:'var(--mono)', fontSize: 10,
                letterSpacing:'0.22em', textTransform:'uppercase',
                color: 'var(--fg)',
                display:'flex', justifyContent:'space-between',
              }}>
                <span>{s.count} {t('works')}</span>
                <span>{t('open')}</span>
              </div>
            </button>
          );
        })}
      </div>
    </section>
  );
}

// ----- View cursor (custom cursor over series photos) -----
function ViewCursor() {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let visible = false;
    let raf = null;
    let nx = 0, ny = 0;

    const update = () => {
      el.style.left = nx + 'px';
      el.style.top = ny + 'px';
      raf = null;
    };
    const onMove = (e) => {
      nx = e.clientX; ny = e.clientY;
      const over = e.target && e.target.closest && e.target.closest('.photo-zoom');
      if (over && !visible) { el.classList.add('visible'); visible = true; }
      else if (!over && visible) { el.classList.remove('visible'); visible = false; }
      if (raf == null) raf = requestAnimationFrame(update);
    };
    const onLeave = () => {
      if (visible) { el.classList.remove('visible'); visible = false; }
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseleave', onLeave);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseleave', onLeave);
      if (raf) cancelAnimationFrame(raf);
    };
  }, []);
  return <div ref={ref} className="view-cursor" aria-hidden="true">View</div>;
}

// ----- Series view -----
function SeriesView({ series, layout, tone, onOpen, blurNudes, onToggleBlur, onBack }) {
  const t = useT();
  const Comp = LAYOUT_COMPS[layout] || EditorialLayout;
  const blur = !!blurNudes;
  const seriesHasNude = hasNudePhoto(series);
  return (
    <section style={{paddingTop: 8}}>
      {seriesHasNude && <ContentNotice blurNudes={blurNudes} onToggle={onToggleBlur} compact/>}
      <header style={{
        display:'flex', justifyContent:'space-between', alignItems:'baseline',
        paddingBottom: 32, marginBottom: 40,
        borderBottom: '1px solid var(--rule)',
      }}>
        <h2 style={{
          margin:0,
          fontFamily: tone.titleFamily === 'serif' ? 'var(--serif)' : 'var(--sans)',
          fontWeight: tone.titleWeight,
          fontStyle: tone.titleStyle,
          letterSpacing: tone.titleTracking,
          fontSize: 'clamp(56px, 9vw, 132px)',
          lineHeight: 0.9,
        }}>{t(`series.${series.id}`)}</h2>
        <div style={{
          fontFamily:'var(--mono)', fontSize: 11, color:'var(--muted)',
          letterSpacing:'0.14em', textTransform:'uppercase',
          textAlign:'right', display:'flex', flexDirection:'column', gap:4,
        }}>
          <span>{String(series.count).padStart(2,'0')} {t('frames')}</span>
        </div>
      </header>
      <Comp photos={series.photos} onOpen={onOpen} blur={blur}/>
      <div style={{
        marginTop: 80, paddingTop: 28,
        borderTop:'1px solid var(--rule)',
        display:'flex', justifyContent:'center',
      }}>
        <button onClick={onBack} className="series-back" style={{
          all:'unset', cursor:'pointer',
          fontFamily:'var(--mono)', fontSize: 11,
          letterSpacing:'0.22em', textTransform:'uppercase',
          color:'var(--fg)', opacity: 0.7,
          padding:'12px 20px',
          border:'1px solid var(--rule)',
          transition:'opacity 200ms ease, background 200ms ease',
        }}
        onMouseEnter={(e) => { e.currentTarget.style.opacity = 1; }}
        onMouseLeave={(e) => { e.currentTarget.style.opacity = 0.7; }}
        >{t('backToSeries')}</button>
      </div>
    </section>
  );
}

// ----- Lightbox -----
function Lightbox({ photos, index, onClose, onNav, blur }) {
  const t = useT();
  const { lang } = useLang();
  const [reveal, setReveal] = useState(false);
  // Reset per-image reveal whenever the image changes or the global blur changes.
  useEffect(() => { setReveal(false); }, [index, blur]);
  useEffect(() => {
    const fn = e => {
      if (e.key === 'Escape') onClose();
      else if (e.key === 'ArrowLeft') onNav(-1);
      else if (e.key === 'ArrowRight') onNav(1);
    };
    window.addEventListener('keydown', fn);
    return () => window.removeEventListener('keydown', fn);
  }, [onClose, onNav]);

  const photo = photos[index];
  return (
    <div onClick={onClose} style={{
      position:'fixed', inset:0, background:'rgba(8,8,8,0.97)',
      zIndex: 10000, display:'flex', flexDirection:'column',
    }}>
      <header style={{
        display:'flex', justifyContent:'space-between', alignItems:'center',
        padding: '20px 28px', color:'#bdb9b1',
        fontFamily:'var(--mono)', fontSize: 11, letterSpacing:'0.14em', textTransform:'uppercase',
      }}>
        <span style={{display:'flex', alignItems:'baseline', gap: 18}}>
          <span>{String(index+1).padStart(2,'0')} / {String(photos.length).padStart(2,'0')}</span>
          {photo.date && <span style={{opacity: 0.7}}>{formatDate(photo.date, lang)}</span>}
        </span>
        <button onClick={(e) => {e.stopPropagation(); onClose();}} style={{
          all:'unset', cursor:'pointer', padding:'6px 12px',
          border:'1px solid #333',
        }}>{t('close')}</button>
      </header>

      <div style={{flex:1, display:'flex', alignItems:'center', justifyContent:'center', padding:'0 24px 40px', gap: 32, position: 'relative'}}
        onClick={(e) => e.stopPropagation()}>
        {index > 0 && (
          <button onClick={(e) => {e.stopPropagation(); onNav(-1);}} aria-label="Previous"
            className="lb-arrow lb-arrow-left"
            style={{
              all:'unset', cursor:'pointer', padding: '14px 18px',
              display:'flex', alignItems:'center', gap: 12,
              color:'#bdb9b1', flex:'0 0 auto',
              fontFamily:'var(--mono)', fontSize: 11,
              letterSpacing:'0.22em', textTransform:'uppercase',
            }}>
            <span className="lb-arrow-glyph">←</span>
            <span className="lb-arrow-label">{t('previous')}</span>
          </button>
        )}
        {photo.placeholder
          ? <div style={{
              width: '40vw', maxWidth: 600, aspectRatio: `${photo.w}/${photo.h}`,
              background:'repeating-linear-gradient(135deg, #1a1a1a 0 1px, transparent 1px 8px)',
              border:'1px solid #2a2a2a',
              display:'flex', alignItems:'center', justifyContent:'center',
              color:'#666', fontFamily:'var(--mono)', fontSize:11, letterSpacing:'0.14em',
              textTransform:'uppercase',
            }}>{photo.label}</div>
          : (() => {
              const apply = blur && photo.nude && !reveal;
              return (
                <div style={{position:'relative'}}
                  onContextMenu={(e) => e.preventDefault()}
                  onDragStart={(e) => e.preventDefault()}
                  onClick={(e) => {
                  if (apply) { e.stopPropagation(); setReveal(true); }
                }}>
                  <img src={photo.src} data-protected="true" style={{
                    maxHeight:'88vh', maxWidth:'88vw', objectFit:'contain', display:'block',
                    filter: apply ? 'blur(48px)' : 'none',
                    transform: apply ? 'scale(1.04)' : 'none',
                    transition: 'filter 320ms ease, transform 320ms ease',
                    cursor: apply ? 'pointer' : 'default',
                  }}/>
                  {apply && (
                    <div style={{
                      position:'absolute', inset:0,
                      display:'flex', alignItems:'center', justifyContent:'center',
                      pointerEvents: 'none',
                    }}>
                      <span style={{
                        padding: '10px 18px',
                        border: '1px solid #444', color:'#ddd',
                        fontFamily:'var(--mono)', fontSize: 11,
                        letterSpacing:'0.18em', textTransform:'uppercase',
                        background: 'rgba(8,8,8,0.6)', backdropFilter: 'blur(4px)',
                      }}>{t('clickToReveal')}</span>
                    </div>
                  )}
                </div>
              );
            })()
        }
        {index < photos.length - 1 && (
          <button onClick={(e) => {e.stopPropagation(); onNav(1);}} aria-label="Next"
            className="lb-arrow lb-arrow-right"
            style={{
              all:'unset', cursor:'pointer', padding: '14px 18px',
              display:'flex', alignItems:'center', gap: 12,
              color:'#bdb9b1', flex:'0 0 auto',
              fontFamily:'var(--mono)', fontSize: 11,
              letterSpacing:'0.22em', textTransform:'uppercase',
            }}>
            <span className="lb-arrow-label">{t('next')}</span>
            <span className="lb-arrow-glyph">→</span>
          </button>
        )}
      </div>
    </div>
  );
}

// ----- Chrome -----
function TopNav({ activeId, onNavigate, tone, onHome, onAbout }) {
  const { lang, setLang } = useLang();
  const t = useT();
  const navStyle = {
    fontFamily: tone.navFamily === 'serif' ? 'var(--serif)' : tone.navFamily === 'mono' ? 'var(--mono)' : 'var(--sans)',
    fontWeight: tone.navWeight,
    fontSize: tone.navSize,
    letterSpacing: tone.navTracking,
    textTransform: tone.navUpper ? 'uppercase' : 'none',
  };
  return (
    <nav style={{
      display:'flex', justifyContent:'space-between', alignItems:'center',
      padding: '28px 0',
      borderBottom: '1px solid var(--rule)',
      marginBottom: 48,
    }}>
      <button onClick={() => onNavigate(null)} style={{
        all:'unset', cursor:'pointer', display:'flex', alignItems:'center', gap:12,
      }}>
        <Lemniscate/>
        <span style={{
          ...navStyle,
          fontSize: tone === TONES.editorial ? 22 : 13,
        }}>Black Iris Gallery</span>
      </button>
      <ul style={{
        listStyle:'none', margin:0, padding:0, display:'flex', gap: 28,
      }}>
        {SERIES.map(s => (
          <li key={s.id}>
            <button onClick={() => onNavigate(s.id)} style={{
              all:'unset', cursor:'pointer', ...navStyle,
              opacity: activeId === s.id ? 1 : 0.55,
              borderBottom: activeId === s.id ? '1px solid var(--fg)' : '1px solid transparent',
              paddingBottom: 2,
            }}>{t(`series.${s.id}`)}</button>
          </li>
        ))}
      </ul>
      <div style={{display:'flex', alignItems:'center', gap: 10}}>
        <button onClick={onAbout} style={{
          all:'unset', cursor:'pointer', ...navStyle,
          opacity: activeId === 'about' ? 1 : 0.7,
          padding:'6px 10px', border:'1px solid var(--rule)',
          background: activeId === 'about' ? 'var(--rule)' : 'transparent',
          transition:'opacity 150ms ease, background 150ms ease',
        }}
        onMouseEnter={(e) => { e.currentTarget.style.opacity = 1; }}
        onMouseLeave={(e) => { if (activeId !== 'about') e.currentTarget.style.opacity = 0.7; }}
        >{t('about')}</button>
        <button onClick={onHome} style={{
          all:'unset', cursor:'pointer', ...navStyle, opacity: 0.7,
          padding:'6px 10px', border:'1px solid var(--rule)',
          transition:'opacity 150ms ease, background 150ms ease',
        }}
        onMouseEnter={(e) => { e.currentTarget.style.opacity = 1; }}
        onMouseLeave={(e) => { e.currentTarget.style.opacity = 0.7; }}
        >{t('cover')}</button>
        <div style={{
          display:'flex', alignItems:'center',
          border:'1px solid var(--rule)',
          ...navStyle,
        }}>
          <button onClick={() => setLang('en')} style={{
            all:'unset', cursor:'pointer', padding:'6px 10px',
            opacity: lang === 'en' ? 1 : 0.4,
            background: lang === 'en' ? 'var(--rule)' : 'transparent',
            transition:'opacity 150ms ease, background 150ms ease',
          }}>EN</button>
          <button onClick={() => setLang('fr')} style={{
            all:'unset', cursor:'pointer', padding:'6px 10px',
            opacity: lang === 'fr' ? 1 : 0.4,
            background: lang === 'fr' ? 'var(--rule)' : 'transparent',
            transition:'opacity 150ms ease, background 150ms ease',
          }}>FR</button>
        </div>
      </div>
    </nav>
  );
}

function Lemniscate() {
  return (
    <svg width="22" height="22" viewBox="0 0 22 22" fill="none">
      <circle cx="11" cy="11" r="10" stroke="currentColor" strokeWidth="1" fill="none"/>
      <circle cx="11" cy="11" r="5" fill="currentColor"/>
    </svg>
  );
}

// ----- Tweaks panel -----
function Tweaks({ t, setTweak }) {
  return (
    <TweaksPanel title="Tweaks">
      <TweakSection label="Tone">
        <TweakRadio
          label=""
          value={t.tone}
          onChange={v => setTweak('tone', v)}
          options={[
            {value:'gallery',   label:'Gallery'},
            {value:'editorial', label:'Editorial'},
            {value:'brutalist', label:'Brutalist'},
          ]}
        />
      </TweakSection>
      <TweakSection label="Series layout">
        <TweakSelect
          label=""
          value={t.layout}
          onChange={v => setTweak('layout', v)}
          options={Object.entries(LAYOUTS).map(([k,v]) => ({value:k, label:v.label}))}
        />
      </TweakSection>
      <TweakSection label="Top navigation">
        <TweakToggle
          label="Show series in nav"
          value={t.showIndex}
          onChange={v => setTweak('showIndex', v)}
        />
      </TweakSection>
    </TweaksPanel>
  );
}

// ----- Dot cursor -----
function DotCursor() {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (!window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
      el.style.display = 'none';
      return;
    }
    let raf = null;
    let nx = 0, ny = 0;
    let hover = false;
    const update = () => {
      el.style.transform = `translate(${nx}px, ${ny}px) translate(-50%, -50%)`;
      raf = null;
    };
    const interactiveSel = 'a, button, [role="button"], .photo-zoom, .series-card, input, select, textarea, label';
    const onMove = (e) => {
      nx = e.clientX; ny = e.clientY;
      el.classList.remove('is-hidden');
      const over = e.target && e.target.closest && e.target.closest(interactiveSel);
      if (over && !hover) { el.classList.add('is-hover'); hover = true; }
      else if (!over && hover) { el.classList.remove('is-hover'); hover = false; }
      if (raf == null) raf = requestAnimationFrame(update);
    };
    const onLeave = () => { el.classList.add('is-hidden'); };
    const onEnter = () => { el.classList.remove('is-hidden'); };
    window.addEventListener('mousemove', onMove);
    document.addEventListener('mouseleave', onLeave);
    document.addEventListener('mouseenter', onEnter);
    return () => {
      window.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseleave', onLeave);
      document.removeEventListener('mouseenter', onEnter);
      if (raf) cancelAnimationFrame(raf);
    };
  }, []);
  return <div ref={ref} className="dot-cursor is-hidden" aria-hidden="true">
    <svg viewBox="0 0 22 22" fill="none">
      <circle cx="11" cy="11" r="5" fill="currentColor"/>
    </svg>
  </div>;
}

// ----- App -----
function App() {
  const [tweaks, setTweak] = useTweaks(DEFAULTS);
  const [activeId, setActiveId] = useState(null); // null = home/index
  const [lightboxIdx, setLightboxIdx] = useState(null);
  const [entered, setEntered] = useState(false);
  // Language: 'en' (default) or 'fr'. Persists via localStorage.
  const [lang, setLangState] = useState(() => {
    try { return localStorage.getItem('blackiris.lang') || 'en'; } catch (e) { return 'en'; }
  });
  const setLang = useCallback((next) => {
    setLangState(next);
    try { localStorage.setItem('blackiris.lang', next); } catch (e) {}
  }, []);
  const langCtx = useMemo(() => ({ lang, setLang }), [lang, setLang]);
  const t = useCallback((key) => (I18N[lang] && I18N[lang][key]) || I18N.en[key] || key, [lang]);

  const tone = TONES[tweaks.tone] || TONES.gallery;
  const paper = PAPERS[tweaks.paper] || PAPERS.bone;
  const activeSeries = SERIES.find(s => s.id === activeId);
  const photos = activeSeries ? activeSeries.photos : [];

  // Reset lightbox when navigating
  useEffect(() => { setLightboxIdx(null); }, [activeId]);
  // Scroll to top on view change
  useEffect(() => { window.scrollTo({top:0, behavior:'instant'}); }, [activeId]);

  const navigate = useCallback((id) => setActiveId(id), []);
  const openLightbox = useCallback((i) => setLightboxIdx(i), []);
  const navLightbox = useCallback((dir) => {
    setLightboxIdx(i => (i + dir + photos.length) % photos.length);
  }, [photos.length]);

  const cssVars = {
    '--bg': paper.bg,
    '--fg': paper.fg,
    '--muted': paper.muted,
    '--rule': paper.rule,
    '--accent': paper.accent,
    '--serif': tone.serif,
    '--sans': tone.sans,
    '--mono': tone.mono,
    '--gap': tweaks.tone === 'brutalist' ? '4px' : tweaks.tone === 'editorial' ? '20px' : '12px',
  };

  return (
    <LangContext.Provider value={langCtx}>
    <div data-screen-label="Black Iris Gallery" style={{
      ...cssVars,
      minHeight:'100vh',
      background:'var(--bg)',
      color:'var(--fg)',
      fontFamily: 'var(--sans)',
      transition: 'background 200ms ease, color 200ms ease',
    }}>
      <DotCursor/>
      {!entered ? (
        <Landing tone={tone} onEnter={() => setEntered(true)}/>
      ) : (
      <div style={{maxWidth: 1480, margin:'0 auto', padding:'0 48px 96px'}}>
        {tweaks.showIndex && <TopNav activeId={activeId} onNavigate={navigate} tone={tone} onHome={() => { setActiveId(null); setEntered(false); }} onAbout={() => setActiveId('about')}/>}
        {activeId === 'about'
          ? <AboutView tone={tone} onBack={() => setActiveId(null)}/>
          : activeId === 'legal'
          ? <LegalView tone={tone} onBack={() => setActiveId(null)}/>
          : !activeSeries
          ? <IndexView tone={tone} onOpen={navigate} blurNudes={tweaks.blurNudes} onToggleBlur={() => setTweak('blurNudes', !tweaks.blurNudes)}/>
          : <SeriesView series={activeSeries} layout={tweaks.layout} tone={tone} onOpen={openLightbox} blurNudes={tweaks.blurNudes} onToggleBlur={() => setTweak('blurNudes', !tweaks.blurNudes)} onBack={() => setActiveId(null)}/>
        }
        <footer style={{
          marginTop: 96, paddingTop: 24, borderTop:'1px solid var(--rule)',
          display:'flex', justifyContent:'space-between',
          fontFamily:'var(--mono)', fontSize:11, color:'var(--muted)',
          letterSpacing:'0.14em', textTransform:'uppercase',
        }}>
          <span>{t('copyright')}</span>
          <span>{SERIES.length.toString().padStart(2,'0')} {lang === 'fr' ? 'séries' : 'series'} · {SERIES.reduce((s,x)=>s+x.count,0)} {t('works')}</span>
          <span>contact@blackirisgallery.com</span>
          <button onClick={() => setActiveId('legal')} style={{
            all:'unset', cursor:'pointer',
            fontFamily:'var(--mono)', fontSize:11, color:'var(--muted)',
            letterSpacing:'0.14em', textTransform:'uppercase',
            opacity: 0.85,
          }}
          onMouseEnter={(e) => { e.currentTarget.style.opacity = 1; }}
          onMouseLeave={(e) => { e.currentTarget.style.opacity = 0.85; }}
          >{t('legal')}</button>
        </footer>
      </div>
      )}

      {lightboxIdx !== null && activeSeries && (
        <Lightbox
          photos={photos}
          index={lightboxIdx}
          onClose={() => setLightboxIdx(null)}
          onNav={navLightbox}
          blur={!!tweaks.blurNudes}
        />
      )}

      <Tweaks t={tweaks} setTweak={setTweak}/>
    </div>
    </LangContext.Provider>
  );
}

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