// shelf2.jsx — the library shelf: eleven Roman-numeral spines, staggered
// arrival, hover reveal with the progressive garden mark, and the book modal.
// Book data lives in books2.jsx. Nothing here invents content.
const { useState: useSState, useEffect: useSEffect, useRef: useSRef } = React;

const SHELF_STAGGER = 40;   // ms between spines
const SHELF_SETTLE = 700;   // ms — stagger is complete, Book I settles once

// ── Modal ────────────────────────────────────────────────────
function MRow({ label, children, style = {} }) {
  return (
    <div style={{ padding: '14px 0', borderTop: '1px solid ' + T.color.border, ...style }}>
      <div style={{ fontFamily: T.font.head, fontWeight: 800, fontSize: 10, letterSpacing: 2.2, textTransform: 'uppercase', color: T.color.textFaint, marginBottom: 6 }}>{label}</div>
      {children}
    </div>
  );
}

function BookModal({ book, onClose, onFeedback }) {
  const panelRef = useSRef(null);
  const soundRef = useSRef(null);
  const startY = useSRef(null);
  const [playing, setPlaying] = useSState(false);
  const [drag, setDrag] = useSState(0);
  const [dragging, setDragging] = useSState(false);

  // Body scroll lock — position-fixed so the swipe gesture has no page scroll
  // to fight. Scroll position is restored exactly on close.
  useSEffect(() => {
    const y = window.scrollY;
    const b = document.body;
    const prev = { position: b.style.position, top: b.style.top, width: b.style.width, overflow: b.style.overflow };
    b.style.position = 'fixed'; b.style.top = -y + 'px'; b.style.width = '100%'; b.style.overflow = 'hidden';
    return () => {
      b.style.position = prev.position; b.style.top = prev.top;
      b.style.width = prev.width; b.style.overflow = prev.overflow;
      window.scrollTo(0, y);
    };
  }, []);

  // Focus into the panel, trap Tab, close on Escape.
  useSEffect(() => {
    const p = panelRef.current;
    if (p) {
      const first = p.querySelector('[data-autofocus]');
      (first || p).focus();
    }
    const onKey = (e) => {
      if (e.key === 'Escape') { e.preventDefault(); onClose(); return; }
      if (e.key !== 'Tab' || !p) return;
      const all = p.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
      const list = Array.prototype.filter.call(all, (el) => !el.disabled && el.offsetParent !== null);
      if (!list.length) return;
      const first2 = list[0], last = list[list.length - 1];
      if (e.shiftKey && document.activeElement === first2) { e.preventDefault(); last.focus(); }
      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first2.focus(); }
    };
    document.addEventListener('keydown', onKey, true);
    return () => document.removeEventListener('keydown', onKey, true);
  }, [onClose]);

  // Dispose every audio node when the modal goes away.
  useSEffect(() => () => {
    if (soundRef.current) { soundRef.current.stop(); soundRef.current = null; }
  }, []);

  const hear = () => {
    if (playing || !AUDIO_OK) return;
    const h = playBookSound(book.n);
    if (!h) return;
    soundRef.current = h;
    setPlaying(true);
    setTimeout(() => {
      if (soundRef.current === h) { h.stop(); soundRef.current = null; }
      setPlaying(false);
    }, h.ms + 140);
  };

  const titleId = 'bookmodal-title';
  const canPlay = book.play && AUDIO_OK;

  return (
    <div onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}
      style={{
        position: 'fixed', inset: 0, zIndex: 200, display: 'flex', alignItems: 'center', justifyContent: 'center',
        padding: 'clamp(14px, 4vw, 40px)', background: 'rgba(26,19,10,0.44)', backdropFilter: 'blur(2px)',
        overflowY: 'auto', overscrollBehavior: 'contain',
      }}>
      <div ref={panelRef} role="dialog" aria-modal="true" aria-labelledby={titleId} tabIndex={-1}
        onTouchStart={(e) => { startY.current = e.touches[0].clientY; setDragging(true); }}
        onTouchMove={(e) => {
          if (startY.current == null) return;
          const d = e.touches[0].clientY - startY.current;
          if (d > 0) setDrag(d);
        }}
        onTouchEnd={() => {
          setDragging(false);
          if (drag > 90) { onClose(); return; }
          setDrag(0); startY.current = null;
        }}
        style={{
          width: 'min(430px, 100%)', background: T.color.card, border: '1px solid var(--acc)',
          borderRadius: 18, padding: 'clamp(26px, 4vw, 38px)', position: 'relative', outline: 'none',
          boxShadow: '0 30px 70px rgba(26,19,10,0.34)',
          transform: 'translateY(' + drag + 'px)',
          transition: dragging ? 'none' : 'transform .3s cubic-bezier(.2,.8,.2,1)',
        }}>
        <button onClick={onClose} aria-label="Close"
          style={{
            position: 'absolute', top: 10, right: 10, width: 44, height: 44, borderRadius: 100,
            background: 'none', border: 'none', cursor: 'pointer', color: T.color.textFaint,
            fontSize: 19, lineHeight: 1, fontFamily: T.font.body,
          }}>×</button>

        <div style={{ textAlign: 'center', marginBottom: 4 }}>
          <span aria-hidden="true" style={{ color: T.color.accent, fontSize: 11, letterSpacing: 6 }}>◆</span>
        </div>

        <div style={{ textAlign: 'center' }}>
          <div style={{ fontFamily: T.font.head, fontWeight: 800, fontSize: 10, letterSpacing: 2.2, textTransform: 'uppercase', color: T.color.textFaint, marginTop: 12 }}>Volume</div>
          <div style={{ fontFamily: T.font.display, fontWeight: 600, fontSize: 20, letterSpacing: 3, color: T.color.accent, marginTop: 4 }}>{book.n}</div>
          <h2 id={titleId} style={{ fontFamily: T.font.display, fontWeight: 600, fontSize: 'clamp(26px, 4.6vw, 33px)', lineHeight: 1.16, color: T.color.text, margin: '12px 0 20px', textWrap: 'balance' }}>
            {book.title}
          </h2>
        </div>

        <MRow label="Theme">
          <div style={{ fontFamily: T.font.body, fontSize: 15, lineHeight: 1.55, color: T.color.text }}>{book.theme}</div>
        </MRow>

        <MRow label="Status">
          <div style={{ fontFamily: T.font.body, fontSize: 15, lineHeight: 1.55, color: book.released ? T.color.text : T.color.textSoft }}>{book.status}</div>
        </MRow>

        {/* Books I and V carry no sound field — the row is omitted entirely. */}
        {book.sound && (
          <MRow label="Signature sound">
            <div style={{ fontFamily: T.font.display, fontWeight: 600, fontSize: 21, letterSpacing: 2.4, color: T.color.accent, lineHeight: 1.35 }}>{book.sound}</div>
            {canPlay && (
              <button onClick={hear} disabled={playing}
                style={{
                  marginTop: 14, minHeight: 44, padding: '10px 18px', borderRadius: 100, cursor: playing ? 'default' : 'pointer',
                  background: 'transparent', border: '1px solid ' + T.color.accent, color: T.color.accentDeep,
                  fontFamily: T.font.head, fontWeight: 800, fontSize: 12, letterSpacing: 1.4, textTransform: 'uppercase',
                  opacity: playing ? 0.55 : 1, transition: 'opacity .2s',
                }}>
                <span aria-hidden="true" style={{ marginRight: 8 }}>◆</span>{playing ? 'Playing…' : 'Hear this sound'}
              </button>
            )}
          </MRow>
        )}

        {book.released && (
          <div style={{ marginTop: 24 }}>
            <Btn full data-autofocus onClick={onFeedback}>Read it and tell us what you think <span className="p-arr">→</span></Btn>
          </div>
        )}
      </div>
    </div>
  );
}

// ── Spine ────────────────────────────────────────────────────
function ShelfSpine({ book, index, shown, arrived, settling, onOpen }) {
  // "Frame with padding only" — the padded button is the ≥44px tap target.
  // Frames may overlap via negative margin so the visible cloth packs tight;
  // the 44px hit area itself is never reduced.
  const pad = Math.max(5, Math.ceil((44 - book.w) / 2));
  return (
    <button className="p-spine" data-shown={shown ? '1' : '0'} data-lead={book.released ? '1' : '0'}
      onClick={(e) => onOpen(index, e.currentTarget)}
      aria-label={book.title + ' — Volume ' + book.n}
      style={{ padding: '0 ' + pad + 'px', minWidth: 44, '--sop': book.released ? 1 : 0.62 }}>
      <span className="p-spine-label" aria-hidden="true"
        style={{ background: T.color.card, border: '1px solid ' + T.color.border, borderRadius: 9, padding: '8px 14px', boxShadow: T.shadow.card, display: 'block' }}>
        <span style={{ display: 'block', fontFamily: T.font.display, fontWeight: 600, fontVariant: 'small-caps', fontSize: 15, letterSpacing: 0.7, color: T.color.text, lineHeight: 1.2 }}>{book.title}</span>
      </span>
      <span className={'p-spine-cloth' + (settling ? ' p-settle' : '')}
        style={{
          display: 'block', width: book.w, height: book.h,
          // Spine curve: darker at both edges, lighter through the middle.
          // Hinge, head/tail gold rules and head highlight are pseudo-elements
          // and insets in the stylesheet — kept off the inline layer stack so
          // twenty-two spines stay cheap to paint.
          background: 'linear-gradient(90deg, rgba(0,0,0,0.20), rgba(0,0,0,0) 26%, rgba(255,252,244,0.11) 50%, rgba(0,0,0,0) 76%, rgba(0,0,0,0.24)), ' + book.color,
          transitionDelay: arrived ? '0ms' : (index * SHELF_STAGGER) + 'ms',
        }}>
        <span style={{ position: 'absolute', inset: '14% 0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <span style={{ writingMode: 'vertical-rl', fontFamily: T.font.display, fontWeight: 600, fontSize: 13, letterSpacing: 2.4, color: book.ink, whiteSpace: 'nowrap' }}>{book.n}</span>
        </span>
        {/* Progressive garden: persistent on every device, at the foot of the
           cloth so it travels with the spine. Inert — a released book with a
           canon-ruled colour blooms, nothing else. */}
        <span className="p-spine-bloom" aria-hidden="true">
          {book.released && book.flowerHex && <GardenMark hex={book.flowerHex} name={book.flowerName} />}
        </span>
      </span>
      <span className="p-spine-motes" aria-hidden="true">
        <span className="p-spine-halo"></span>
        {[0, 1, 2, 3, 4, 5].map((k) => (
          (k < 5 || book.released) &&
          <span key={k} className="p-smote" style={{
            left: [20, 44, 62, 78, 34, 54][k] + '%',
            animationDelay: [0, 1.3, 2.4, 3.2, 4.1, 2.8][k] + 's',
            animationDuration: [3.6, 4.2, 3.9, 4.5, 4.0, 3.7][k] + 's',
            '--mx': [5, -6, 7, -4, 3, -5][k] + 'px',
          }}></span>
        ))}
      </span>
    </button>
  );
}

// One shelf unit: a row of spines packed edge to edge over its own board.
// `from` is the absolute index of books[0] in BOOKS, so arrival delays stay
// continuous however the row is split.
function ShelfRow({ books, from, shown, arrived, settling, onOpen }) {
  return (
    <React.Fragment>
      <div className="p-shelf-row">
        {books.map((b, i) => (
          <ShelfSpine key={b.n} book={b} index={from + i} shown={shown} arrived={arrived}
            settling={settling && b.released} onOpen={onOpen} />
        ))}
      </div>
      <div className="p-shelf-board" aria-hidden="true"></div>
    </React.Fragment>
  );
}

// ── Shelf ────────────────────────────────────────────────────
function ShelfSection({ onNav }) {
  const rowRef = useSRef(null);
  const openerRef = useSRef(null);
  const [shown, setShown] = useSState(false);
  const [arrived, setArrived] = useSState(false);
  const [settling, setSettling] = useSState(false);
  const [open, setOpen] = useSState(null);

  // ONE observer for the whole shelf; each spine gets an index-based delay.
  useSEffect(() => {
    const el = rowRef.current;
    if (!el) return;
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      setShown(true); setArrived(true); return;
    }
    // Already in view on mount — arrive without waiting for a callback.
    const r = el.getBoundingClientRect();
    if (r.top < window.innerHeight && r.bottom > 0) { setShown(true); return; }
    let io = null;
    // Safety net: some embedding contexts never deliver IO callbacks. The
    // shelf must never be left invisible because of it.
    const fallback = setTimeout(() => { setShown(true); if (io) io.disconnect(); }, 800);
    io = new IntersectionObserver(([e]) => {
      if (e.isIntersecting) { setShown(true); clearTimeout(fallback); io.disconnect(); }
    }, { threshold: 0.2 });
    io.observe(el);
    return () => { clearTimeout(fallback); io.disconnect(); };
  }, []);

  useSEffect(() => {
    if (!shown || arrived) return;
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
    const a = setTimeout(() => { setArrived(true); setSettling(true); }, SHELF_SETTLE);
    const b = setTimeout(() => setSettling(false), SHELF_SETTLE + 620);
    return () => { clearTimeout(a); clearTimeout(b); };
  }, [shown, arrived]);

  const openBook = (i, el) => { openerRef.current = el; setOpen(i); };

  const close = () => {
    setOpen(null);
    const el = openerRef.current;
    if (el) setTimeout(() => el.focus(), 0);
  };

  return (
    <section id="library" data-screen-label="Library" style={{ padding: '64px 0 8px' }}>
      <div style={{ textAlign: 'center', marginBottom: 30 }}>
        <div style={{ fontFamily: T.font.head, fontWeight: 800, fontSize: 11.5, letterSpacing: 3, textTransform: 'uppercase', color: T.color.textFaint }}>The Library</div>
        <div style={{ width: 32, height: 1, background: T.color.accent, opacity: 0.7, margin: '12px auto 0' }}></div>
      </div>
      <div ref={rowRef} style={{ maxWidth: 620, margin: '0 auto' }}>
        {/* Desktop: one shelf, all eleven. Unchanged. */}
        <div className="p-shelf-one">
          <ShelfRow books={BOOKS} from={0} shown={shown} arrived={arrived} settling={settling} onOpen={openBook} />
        </div>
        {/* ≤640px: two shelves — I–VI above, VII–XI below. The stagger index
            stays absolute, so the sequence reads straight through the break. */}
        <div className="p-shelf-two">
          <div className="p-shelf-unit">
            <ShelfRow books={BOOKS.slice(0, 6)} from={0} shown={shown} arrived={arrived} settling={settling} onOpen={openBook} />
          </div>
          <div className="p-shelf-unit">
            <ShelfRow books={BOOKS.slice(6)} from={6} shown={shown} arrived={arrived} settling={settling} onOpen={openBook} />
          </div>
        </div>
      </div>
      <p style={{ textAlign: 'center', fontFamily: T.font.body, fontSize: 13.5, color: T.color.textFaint, margin: '34px auto 0', maxWidth: 420 }}>
        Book One is open for readers. The garden grows as the cycle publishes.
      </p>
      <p className="p-tap-hint" style={{ textAlign: 'center', fontFamily: T.font.body, fontSize: 13.5, color: T.color.textFaint, margin: '6px auto 0', maxWidth: 420 }}>
        Tap a spine to open a book.
      </p>
      {open !== null && (
        <BookModal book={BOOKS[open]} onClose={close}
          onFeedback={() => { setOpen(null); onNav('feedback'); }} />
      )}
    </section>
  );
}

Object.assign(window, { ShelfSection, ShelfRow, BookModal, ShelfSpine });
