// Shared utilities + data for all three directions.
// Each direction renders inside a fixed-width DCArtboard, so layouts are pinned to a known canvas.

const SITE_DATA = {
  name: 'Julianna Bair, DMD',
  shortName: 'Dr. Bair',
  tagline: 'Dual board-specialized in Endodontics and Pediatric Dentistry',
  bioShort:
    'A dual-specialist clinician practicing across endodontics and pediatric dentistry in New York. Focused on conservative, biology-first care for developing dentitions and complex pulpal cases.',
  bioLong: [
    'Dr. Bair is one of a small cohort of clinicians in the United States board-eligible in both endodontics and pediatric dentistry. Her practice centers on the developing dentition — vital pulp therapy, traumatic dental injuries, autotransplantation, and the management of deep caries in young patients.',
    'She lectures nationally on the intersection of these two specialties and consults with referring practices on complex multidisciplinary cases.',
  ],
  locations: [
    {
      name: 'NYC Endodontics',
      role: 'Endodontic practice',
      address: '30 East 60th Street, 18th Floor',
      city: 'New York, NY 10022',
      hours: 'Mon – Thu, by referral',
    },
    {
      name: 'Smile Starters Pediatric Dentistry',
      role: 'Pediatric practice',
      address: 'Floral Park',
      city: 'New York',
      hours: 'Select Fridays & Saturdays',
    },
  ],
  specialties: [
    {
      title: 'Endodontics',
      copy: 'Microsurgical and non-surgical root canal therapy, retreatment, and pulpal regeneration in adult and adolescent patients.',
      points: ['Microscope-assisted treatment', 'Apical microsurgery', 'Regenerative endodontics', 'CBCT-guided diagnosis'],
    },
    {
      title: 'Pediatric Dentistry',
      copy: 'Comprehensive care for infants, children, adolescents, and patients with special healthcare needs — with a focus on prevention and behavior guidance.',
      points: ['Behavior management', 'Interceptive orthodontics', 'Trauma response', 'Special healthcare needs'],
    },
  ],
  talks: [
    { topic: 'Vital Pulp Therapy in the Developing Dentition', venue: 'AAE Annual Session', year: 2025, type: 'Lecture', tags: ['Endodontics', 'Pediatric'] },
    { topic: 'Pediatric Behavior Management in the Endodontic Setting', venue: 'AAPD Annual Session', year: 2025, type: 'Lecture', tags: ['Pediatric'] },
    { topic: 'Autotransplantation: A Biological Alternative', venue: 'Greater NY Dental Meeting', year: 2024, type: 'Course', tags: ['Endodontics', 'Surgical'] },
    { topic: 'Dental Trauma in Children & Adolescents', venue: 'Columbia CDE', year: 2024, type: 'Lecture', tags: ['Pediatric', 'Trauma'] },
    { topic: 'Management of Deep Caries in the Developing Dentition', venue: 'NYU CDE', year: 2024, type: 'Lecture', tags: ['Pediatric', 'Endodontics'] },
    { topic: 'Building a Dual-Specialty Practice', venue: 'Seattle Study Club', year: 2023, type: 'Panel', tags: ['Practice'] },
  ],
  cases: [
    { title: 'Vital pulp therapy · #19', detail: 'Class II caries excavation and partial pulpotomy. 18-mo recall.', tag: 'VPT' },
    { title: 'Apexogenesis · #8', detail: 'Traumatized immature incisor, MTA pulpotomy, root completion.', tag: 'Trauma' },
    { title: 'Autotransplant · #30 → #19', detail: 'Third molar transplant in adolescent. 24-mo follow-up.', tag: 'Surgical' },
    { title: 'Regenerative endo · #9', detail: 'Necrotic immature tooth, REP protocol, apical closure.', tag: 'Regen' },
    { title: 'Stainless steel crown · #B', detail: 'Pulpotomy and SSC, primary second molar.', tag: 'Pediatric' },
    { title: 'Apical microsurgery · #14', detail: 'Persistent periapical lesion s/p retreatment.', tag: 'Surgical' },
  ],
  press: [
    { outlet: 'Journal of Endodontics', title: 'Pulpal outcomes after partial pulpotomy in young permanent molars', year: '2024' },
    { outlet: 'Pediatric Dentistry', title: 'A protocol for behavior guidance during root canal therapy in children', year: '2023' },
  ],
};

// A tasteful striped placeholder with a small monospace caption, used in lieu of real imagery.
// If `src` is provided, render the actual image instead. Auto-resolves portraits to the headshot
// and other labels to curated stock imagery.
const STOCK_IMAGES = {
  // Curated Unsplash stock — dental, family, medical, hands, smiles
  smile1: 'https://images.unsplash.com/photo-1581585099522-f6ac2efe5b5d?w=1200&q=80',
  smile2: 'https://images.unsplash.com/photo-1606811971618-4486d14f3f99?w=1200&q=80',
  child1: 'https://images.unsplash.com/photo-1503454537195-1dcabb73ffb9?w=1200&q=80',
  child2: 'https://images.unsplash.com/photo-1517256673644-36ad11246d21?w=1200&q=80',
  child3: 'https://images.unsplash.com/photo-1587116908077-a3b51e2dd7c2?w=1200&q=80',
  family1: 'https://images.unsplash.com/photo-1591779051696-1c3fa1469a79?w=1200&q=80',
  family2: 'https://images.unsplash.com/photo-1559839734-2b71ea197ec2?w=1200&q=80',
  hands1: 'https://images.unsplash.com/photo-1606811841689-23dfddce3e95?w=1200&q=80',
  hands2: 'https://images.unsplash.com/photo-1588776814546-1ffcf47267a5?w=1200&q=80',
  micro: 'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=1200&q=80',
  operatory: 'https://images.unsplash.com/photo-1629909613654-28e377c37b09?w=1200&q=80',
  tools: 'https://images.unsplash.com/photo-1606265752439-1f18756aa5fc?w=1200&q=80',
  brush: 'https://images.unsplash.com/photo-1588454896327-13f8a26f4f3a?w=1200&q=80',
  xray: 'https://images.unsplash.com/photo-1609840114035-3c981b782dfe?w=1200&q=80',
  laughing: 'https://images.unsplash.com/photo-1545048702-79362596cdc9?w=1200&q=80',
  parent: 'https://images.unsplash.com/photo-1602002418082-dd4a8f7d6f5e?w=1200&q=80',
  toddler: 'https://images.unsplash.com/photo-1519689680058-324335c77eba?w=1200&q=80',
  nyc: 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=1200&q=80',
  suburb: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200&q=80',
  texture: 'https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=1200&q=80',
};

function resolveImage(label, srcOverride) {
  if (srcOverride) return srcOverride;
  if (!label) return null;
  const l = label.toLowerCase();

  // Real photos of Dr. Bair, in priority order.
  if (/headshot|portrait/.test(l)) return 'assets/headshot.png';
  if (/microscope|micro|hands?\s*[·.]?\s*operatory|chair/.test(l)) return 'assets/microscope.png';
  if (/practice\s*[·.]?\s*operatory|operatory|in[- ]?practice|standing|scrubs/.test(l)) return 'assets/operatory.png';

  // Case images — rotate through the three real photos so the casebook still feels
  // populated until real case photography arrives.
  if (/vpt|trauma|regen|surgical|pediatric|pulp|case|fig|tag|smile|laugh|child|kid|toddler|patient|young|family|parent|tool|instrument|brush|prevention|hygiene|xray|x-ray|radiograph|cbct/.test(l)) {
    const pool = ['assets/microscope.png', 'assets/operatory.png', 'assets/headshot.png'];
    let h = 0;
    for (let i = 0; i < label.length; i++) h = (h * 31 + label.charCodeAt(i)) >>> 0;
    return pool[h % pool.length];
  }
  // Maps / locations stay as the patterned placeholder.
  return null;
}

function Placeholder({ label = 'image', ratio = '4 / 5', tone = 'warm', radius = 0, style = {}, captionStyle = {}, src, position }) {
  const img = resolveImage(label, src);
  if (img) {
    // Smart default: keep faces in frame across the three real photos.
    let pos = position;
    if (!pos) {
      if (/headshot\.png$/.test(img)) pos = 'center 18%';
      else if (/operatory\.png$/.test(img)) pos = 'center 22%';
      else if (/microscope\.png$/.test(img)) pos = 'center 28%';
      else pos = 'center';
    }
    return (
      <div
        style={{
          aspectRatio: ratio,
          backgroundImage: `url(${img})`,
          backgroundSize: 'cover',
          backgroundPosition: pos,
          borderRadius: radius,
          ...style,
        }}
      />
    );
  }
  const palettes = {
    warm: { bg: '#ece6dc', stripe: 'rgba(60,40,20,.06)', text: 'rgba(60,40,20,.55)' },
    cool: { bg: '#e3e8ed', stripe: 'rgba(20,30,50,.06)', text: 'rgba(20,30,50,.55)' },
    sage: { bg: '#dde4dc', stripe: 'rgba(30,50,30,.06)', text: 'rgba(30,50,30,.55)' },
    ink: { bg: '#1f2024', stripe: 'rgba(255,250,240,.04)', text: 'rgba(255,250,240,.55)' },
    blush: { bg: '#ecdcd8', stripe: 'rgba(80,40,30,.05)', text: 'rgba(80,40,30,.55)' },
  };
  const p = palettes[tone] || palettes.warm;
  return (
    <div
      style={{
        aspectRatio: ratio,
        background: `repeating-linear-gradient(45deg, ${p.bg} 0 14px, ${p.stripe} 14px 15px)`,
        color: p.text,
        borderRadius: radius,
        display: 'flex',
        alignItems: 'flex-end',
        padding: 14,
        fontFamily: '"Berkeley Mono", ui-monospace, "SF Mono", Menlo, monospace',
        fontSize: 10,
        letterSpacing: '0.08em',
        textTransform: 'uppercase',
        ...style,
      }}
    >
      <span style={{ ...captionStyle }}>{label}</span>
    </div>
  );
}

// Find the nearest scrollable ancestor — the IntersectionObserver root.
// Each direction is mounted inside an artboard with its own internal scroll, so
// the global viewport observer never re-fires per-section. Walking up to the
// scroller fixes that.
function findScrollParent(el) {
  let n = el && el.parentElement;
  while (n) {
    const s = getComputedStyle(n);
    if (/(auto|scroll|overlay)/.test(s.overflowY) && n.scrollHeight > n.clientHeight + 1) return n;
    n = n.parentElement;
  }
  return null;
}

// Reveal on scroll — uses IntersectionObserver against the nearest scrollable
// ancestor so it works inside artboards. Variants: "rise" (default fade+up),
// "fade", "left", "right", "scale", "blur". Pass `once={false}` to re-trigger
// when the element scrolls back into view.
function Reveal({
  children,
  delay = 0,
  y = 14,
  x = 32,
  variant = 'rise',
  duration = 900,
  threshold = 0.12,
  once = true,
  style = {},
}) {
  const ref = React.useRef(null);
  const [shown, setShown] = React.useState(false);
  React.useEffect(() => {
    if (!ref.current) return;
    const root = findScrollParent(ref.current);
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            setShown(true);
            if (once) io.disconnect();
          } else if (!once) {
            setShown(false);
          }
        });
      },
      { root, threshold, rootMargin: '0px 0px -8% 0px' }
    );
    io.observe(ref.current);
    return () => io.disconnect();
  }, [once, threshold]);

  const hidden = (() => {
    switch (variant) {
      case 'fade':  return { opacity: 0, transform: 'none' };
      case 'left':  return { opacity: 0, transform: `translateX(-${x}px)` };
      case 'right': return { opacity: 0, transform: `translateX(${x}px)` };
      case 'scale': return { opacity: 0, transform: 'scale(.96)' };
      case 'blur':  return { opacity: 0, filter: 'blur(8px)', transform: `translateY(${y}px)` };
      case 'rise':
      default:      return { opacity: 0, transform: `translateY(${y}px)` };
    }
  })();
  const visible = { opacity: 1, transform: 'translate(0,0) scale(1)', filter: 'blur(0)' };
  const ease = 'cubic-bezier(.2,.7,.2,1)';
  const transition = [
    `opacity ${duration}ms ${ease} ${delay}ms`,
    `transform ${duration}ms ${ease} ${delay}ms`,
    `filter ${duration}ms ${ease} ${delay}ms`,
  ].join(', ');

  return (
    <div
      ref={ref}
      style={{ ...(shown ? visible : hidden), transition, willChange: 'transform, opacity, filter', ...style }}
    >
      {children}
    </div>
  );
}

// Sticky-ish band that fades + lifts as the section enters. Use as a wrapper
// on entire <section> elements to give each section a clear arrival moment.
function SectionTransition({ children, variant = 'rise', delay = 0, y = 28, style = {}, ...rest }) {
  return (
    <Reveal variant={variant} delay={delay} y={y} duration={1100} threshold={0.06} style={style} {...rest}>
      {children}
    </Reveal>
  );
}

// Smooth in-page anchor scroll.
function smoothTo(id, container) {
  const el = (container || document).querySelector(`[data-anchor="${id}"]`);
  if (!el) return;
  const root = container || document.scrollingElement;
  const top = el.getBoundingClientRect().top + (container ? container.scrollTop : window.scrollY) - 24;
  if (container) container.scrollTo({ top, behavior: 'smooth' });
  else window.scrollTo({ top, behavior: 'smooth' });
}

// Tiny icon set — line glyphs only, no whimsy.
const Icon = {
  arrow: (p = {}) => (
    <svg viewBox="0 0 24 24" width={p.size || 14} height={p.size || 14} fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={p.style}>
      <path d="M5 12h14M13 6l6 6-6 6" />
    </svg>
  ),
  pin: (p = {}) => (
    <svg viewBox="0 0 24 24" width={p.size || 14} height={p.size || 14} fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={p.style}>
      <path d="M12 21s7-7.5 7-12a7 7 0 1 0-14 0c0 4.5 7 12 7 12Z" />
      <circle cx="12" cy="9" r="2.5" />
    </svg>
  ),
  cal: (p = {}) => (
    <svg viewBox="0 0 24 24" width={p.size || 14} height={p.size || 14} fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={p.style}>
      <rect x="3.5" y="5" width="17" height="15" rx="1.5" />
      <path d="M3.5 10h17M8 3v4M16 3v4" />
    </svg>
  ),
  mail: (p = {}) => (
    <svg viewBox="0 0 24 24" width={p.size || 14} height={p.size || 14} fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={p.style}>
      <rect x="3" y="5" width="18" height="14" rx="1.5" />
      <path d="M3.5 6.5 12 13l8.5-6.5" />
    </svg>
  ),
};

// Section-level entrance — animates the entire section as it enters the scroll
// container. Use as a drop-in replacement for <section>: it forwards data-anchor,
// style, etc. and wraps the contents in an IntersectionObserver-driven transition.
function AnimSection({ children, variant = 'rise', delay = 0, duration = 1100, threshold = 0.08, style = {}, ...rest }) {
  const ref = React.useRef(null);
  const [shown, setShown] = React.useState(false);
  React.useEffect(() => {
    if (!ref.current) return;
    const root = findScrollParent(ref.current);
    const io = new IntersectionObserver(
      (entries) => entries.forEach((e) => { if (e.isIntersecting) { setShown(true); io.disconnect(); } }),
      { root, threshold, rootMargin: '0px 0px -10% 0px' }
    );
    io.observe(ref.current);
    return () => io.disconnect();
  }, [threshold]);

  const hidden = (() => {
    switch (variant) {
      case 'fade':  return { opacity: 0 };
      case 'left':  return { opacity: 0, transform: 'translateX(-32px)' };
      case 'right': return { opacity: 0, transform: 'translateX(32px)' };
      case 'scale': return { opacity: 0, transform: 'scale(.985)' };
      case 'blur':  return { opacity: 0, filter: 'blur(10px)', transform: 'translateY(24px)' };
      case 'rise':
      default:      return { opacity: 0, transform: 'translateY(36px)' };
    }
  })();
  const visible = { opacity: 1, transform: 'translate(0,0) scale(1)', filter: 'blur(0)' };
  const ease = 'cubic-bezier(.2,.7,.2,1)';
  const transition = [
    `opacity ${duration}ms ${ease} ${delay}ms`,
    `transform ${duration}ms ${ease} ${delay}ms`,
    `filter ${duration}ms ${ease} ${delay}ms`,
  ].join(', ');

  return (
    <section
      ref={ref}
      style={{
        ...style,
        ...(shown ? visible : hidden),
        transition,
        willChange: 'transform, opacity, filter',
      }}
      {...rest}
    >
      {children}
    </section>
  );
}

Object.assign(window, { SITE_DATA, Placeholder, Reveal, SectionTransition, AnimSection, smoothTo, Icon });
