// site/components.jsx — shared chrome and primitives for the portfolio.
// Every visual token comes from colors_and_type.css. This file composes.

// ─── PAPER GRAIN — SVG noise turbulence overlay ─────────────────
// Subtle, decorative. Sits on the page surface at low opacity.
function Grain({ opacity = 0.045 }) {
  return (
    <svg
      aria-hidden="true"
      style={{
        position: 'fixed', inset: 0, width: '100%', height: '100%',
        pointerEvents: 'none', zIndex: 0, opacity, mixBlendMode: 'soft-light'
      }}>
      
      <filter id="kt-grain">
        <feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" stitchTiles="stitch" />
        <feColorMatrix values="0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.9 0" />
      </filter>
      <rect width="100%" height="100%" filter="url(#kt-grain)" />
    </svg>);

}

// ─── LOGO (wordmark) ─────────────────────────────────────────────
// Renders the right SVG for the current mode + accent.
function Wordmark({ height = 22, variant }) {
  // variant: 'reversed' | 'indigo' | 'moss' | 'black'
  const src = variant ? `assets/logo-${variant}.svg` : 'assets/logo.svg';
  return (
    <img src={src} alt="Katy Towell"
    style={{ ...{ height, display: 'block', width: "94.6641px" }, width: "94.6641px", height: "28px" }} />);

}
function MarkIcon({ size = 28, variant = 'reversed' }) {
  return (
    <img src={`assets/logo-icon-${variant}.svg`} alt=""
    style={{ width: size, height: size, display: 'block' }} />);

}

// ─── ROMAN NUMERAL — display-sized ───────────────────────────────
function Rune({ numeral, size = 'h2', color }) {
  const sizes = {
    h1: { font: '"Ohno Blazeface 72", var(--font-display)', fontSize: 84, lineHeight: 0.9 },
    h2: { font: '"Ohno Blazeface 60", var(--font-display)', fontSize: 64, lineHeight: 0.9 },
    h3: { font: '"Ohno Blazeface 48", var(--font-display)', fontSize: 48, lineHeight: 0.9 },
    h4: { font: '"Ohno Blazeface 36", var(--font-display)', fontSize: 36, lineHeight: 0.9 },
    h5: { font: '"Ohno Blazeface 24", var(--font-display)', fontSize: 24, lineHeight: 0.9 }
  };
  const s = sizes[size] || sizes.h2;
  return (
    <span style={{
      fontFamily: s.font, fontWeight: 400, fontSize: s.fontSize, lineHeight: s.lineHeight,
      letterSpacing: '0.02em', textTransform: 'uppercase',
      color: color || 'var(--fg-1)', display: 'inline-block'
    }}>{numeral}</span>);

}

// ─── ACCENT DOT — the eye-mark distilled to a moss pip ─────────
function AccentDot({ size = 8, color }) {
  return (
    <span style={{
      width: size, height: size, borderRadius: '50%',
      background: color || 'var(--moss)', display: 'inline-block', flexShrink: 0
    }} />);

}

// ─── BUTTON ─────────────────────────────────────────────────────
const btnStyles = {
  base: {
    fontFamily: 'var(--font-body)', fontWeight: 600, letterSpacing: '0.04em',
    border: 0, cursor: 'pointer', lineHeight: 1, borderRadius: 'var(--r-sm)',
    display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 10,
    transition: 'background var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out)',
    textDecoration: 'none'
  },
  lg: { padding: '16px 24px', fontSize: 15 },
  md: { padding: '12px 18px', fontSize: 14 },
  sm: { padding: '8px 13px', fontSize: 13, borderRadius: 'var(--r-xs)' },
  primary: { background: 'var(--action-primary-bg)', color: 'var(--action-primary-fg)' },
  secondary: { background: 'var(--btn-secondary-bg)', color: 'var(--btn-secondary-fg)', border: '1px solid var(--btn-secondary-border)' },
  ghost: { background: 'transparent', color: 'var(--action-ghost-fg)' }
};
function Btn({ variant = 'primary', size = 'md', href, onClick, children, style, trailing }) {
  const merged = { ...btnStyles.base, ...btnStyles[size], ...btnStyles[variant], ...style };
  const inner =
  <>
      {children}
      {trailing &&
    <span style={{ marginLeft: 4, fontFamily: '"Ohno Blazeface 18", var(--font-display)', fontWeight: 400, letterSpacing: '0.06em' }}>
          {trailing}
        </span>
    }
    </>;

  if (href) return <a href={href} onClick={onClick} className="kt-btn" data-variant={variant} style={merged}>{inner}</a>;
  return <button onClick={onClick} className="kt-btn" data-variant={variant} style={merged}>{inner}</button>;
}

// ─── EYEBROW (tracked, uppercase, all-caps body font) ────────────
function Eyebrow({ children, color, style }) {
  return (
    <div className="oma-eyebrow" style={{
      fontSize: 11, color: color || 'var(--fg-3)', ...style
    }}>{children}</div>);

}

// ─── TAG ─────────────────────────────────────────────────────────
function Tag({ children, tone = 'neutral', suit, style }) {
  const tones = {
    neutral: { bg: 'rgba(191,174,146,0.10)', fg: 'var(--fg-2)', line: 'rgba(191,174,146,0.20)' },
    indigo: { bg: 'rgba(45,78,166,0.18)', fg: 'var(--indigo-300)', line: 'rgba(45,78,166,0.35)' },
    moss: { bg: 'rgba(150,165,96,0.16)', fg: 'var(--moss)', line: 'rgba(150,165,96,0.32)' },
    plum: { bg: 'rgba(118,72,131,0.22)', fg: '#C49ACF', line: 'rgba(118,72,131,0.42)' },
    amber: { bg: 'rgba(244,175,89,0.14)', fg: 'var(--amber)', line: 'rgba(244,175,89,0.30)' }
  };
  const t = tones[tone] || tones.neutral;
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      padding: '5px 10px', borderRadius: 999,
      background: t.bg, color: t.fg, border: '1px solid ' + t.line,
      fontFamily: 'var(--font-body)', fontWeight: 600,
      fontSize: 11, letterSpacing: '0.10em', textTransform: 'uppercase',
      lineHeight: 1, ...style
    }}>
      {children}
    </span>);

}

// ─── HAIRLINE RULE ───────────────────────────────────────────────
function Rule({ tone = 'soft', style }) {
  const c = { soft: 'var(--line-soft)', medium: 'var(--line-medium)', strong: 'var(--line-strong)' }[tone];
  return <div style={{ height: 1, background: c, ...style }} />;
}

// ─── SECTION HEAD ───────────────────────────────────────────────
function SectionHead({ numeral, eyebrow, title, sub, action }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between',
      gap: 24, paddingBottom: 24, borderBottom: '1px solid var(--line-medium)',
      marginBottom: 32
    }}>
      <div style={{ display: 'flex', gap: 20, alignItems: 'flex-end', minWidth: 0 }}>
        {numeral && <Rune numeral={numeral} size="h3" color="var(--fg-3)" />}
        <div style={{ minWidth: 0 }}>
          {eyebrow && <Eyebrow style={{ marginBottom: 8 }}>{eyebrow}</Eyebrow>}
          <h2 className="oma-h2" style={{ margin: 0, fontSize: 'clamp(28px, 4.5vw, 44px)' }}>
            {title}
          </h2>
          {sub &&
          <div className="oma-small" style={{ marginTop: 8, color: 'var(--fg-2)', maxWidth: 520 }}>
              {sub}
            </div>
          }
        </div>
      </div>
      {action && <div style={{ flexShrink: 0 }}>{action}</div>}
    </div>);

}

// ─── PAGE TRANSITION WRAPPER ────────────────────────────────────
// Uses a JS-driven opacity transition (not CSS keyframes) so it works
// even when the host iframe throttles animation clocks.
function PageFade({ routeKey, children }) {
  // Just a stable key wrapper. Subtle hover transitions live on cards;
  // page transitions kept off so screenshots and direct edits stay readable.
  return <div key={routeKey} className="kt-page-enter">{children}</div>;
}

// ─── NAV LINK (hash router) ─────────────────────────────────────
function navTo(hash) {
  if (window.location.hash === hash) return;
  window.location.hash = hash;
}

function NavLink({ to, children, style, onClick }) {
  return (
    <a href={'#' + to}
    onClick={(e) => {onClick && onClick(e);}}
    style={{
      textDecoration: 'none', color: 'inherit', ...style
    }}>
      {children}
    </a>);

}

// ─── TOP BAR ────────────────────────────────────────────────────
function TopBar({ route, mode, onModeToggle, accent, onMobileMenu }) {
  const wordmarkVariant =
  mode === 'light' ? accent === 'moss' ? 'moss' : 'indigo' :
  'reversed';
  return (
    <header className="kt-topbar" style={{
      position: 'sticky', top: 0, zIndex: 50,
      background: 'color-mix(in srgb, var(--bg-page) 92%, transparent)',
      backdropFilter: 'saturate(140%) blur(6px)',
      WebkitBackdropFilter: 'saturate(140%) blur(6px)',
      borderBottom: '1px solid var(--line-soft)',
      padding: '0 clamp(20px, 4vw, 28px)',
    }}>
      <div className="kt-topbar-inner">
        <NavLink to="/">
          <Wordmark height={28} variant={wordmarkVariant} />
        </NavLink>

        {/* desktop nav */}
        <nav className="kt-nav-desktop" style={{
          display: 'flex', gap: 4, justifyContent: 'center',
          fontFamily: 'var(--font-body)', fontWeight: 600,
          fontSize: 12, letterSpacing: '0.14em', textTransform: 'uppercase'
        }}>
          {KT.nav.map((item) => {
            const active = route.id === item.id || item.id === 'work' && route.id === 'working';
            return (
              <NavLink key={item.id} to={'/' + (item.id === 'altar' ? '' : item.id)}
              style={{
                padding: '10px 14px',
                color: active ? 'var(--moss)' : 'var(--fg-2)',
                borderRadius: 'var(--r-xs)',
                background: active ? 'var(--chrome-active-bg)' : 'transparent',
                display: 'inline-flex', alignItems: 'center', gap: 8,
                transition: 'color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out)'
              }}>
                {item.label}
              </NavLink>);

          })}
        </nav>

        <div style={{ display: 'flex', alignItems: 'center', gap: 10, justifyContent: "flex-end", fontFamily: "\"New Kansas\"" }}>
          <button onClick={onModeToggle}
          aria-label={mode === 'light' ? 'Switch to dark' : 'Switch to light'}
          style={{
            width: 38, height: 38, borderRadius: 'var(--r-md)',
            border: '1px solid var(--line-soft)', background: 'transparent',
            color: 'var(--fg-2)', cursor: 'pointer',
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center'
          }}>
            {mode === 'light' ?
            <SunGlyph size={16} /> :
            <MoonGlyph size={16} />}
          </button>
          <button className="kt-mobile-menu-btn" onClick={onMobileMenu}
          aria-label="Menu"
          style={{
            width: 38, height: 38, borderRadius: 'var(--r-md)',
            border: '1px solid var(--line-soft)', background: 'transparent',
            color: 'var(--fg-2)', cursor: 'pointer',
            display: 'none', alignItems: 'center', justifyContent: 'center', textAlign: "center"
          }}>
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
              <line x1="4" y1="7" x2="20" y2="7" />
              <line x1="6" y1="12" x2="18" y2="12" />
              <line x1="4" y1="17" x2="20" y2="17" />
            </svg>
          </button>
        </div>
      </div>
    </header>);

}

// ─── MOBILE MENU ────────────────────────────────────────────────
function MobileMenu({ open, onClose, route }) {
  if (!open) return null;
  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 100,
      background: 'var(--bg-overlay)'
    }}
    onClick={onClose}>
      <div onClick={(e) => e.stopPropagation()}
      style={{
        position: 'absolute', top: 0, right: 0, bottom: 0,
        width: 'min(88vw, 360px)',
        background: 'var(--bg-surface)',
        borderLeft: '1px solid var(--line-medium)',
        padding: '24px 24px 40px',
        display: 'flex', flexDirection: 'column', gap: 4,
        animation: 'kt-slide-in 320ms var(--ease-out)'
      }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
          <Eyebrow>Menu</Eyebrow>
          <button onClick={onClose} aria-label="Close"
          style={{
            width: 36, height: 36, borderRadius: 'var(--r-md)',
            border: '1px solid var(--line-soft)', background: 'transparent',
            color: 'var(--fg-2)', cursor: 'pointer'
          }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round">
              <line x1="6" y1="6" x2="18" y2="18" /><line x1="18" y1="6" x2="6" y2="18" />
            </svg>
          </button>
        </div>
        {KT.nav.map((item) => {
          const active = route.id === item.id;
          return (
            <a key={item.id} href={'#/' + (item.id === 'altar' ? '' : item.id)}
            onClick={onClose}
            style={{
              display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
              padding: '18px 4px', borderBottom: '1px solid var(--line-soft)',
              textDecoration: 'none', color: active ? 'var(--moss)' : 'var(--fg-1)',
              fontFamily: '"Ohno Blazeface 24", var(--font-display)',
              fontSize: 24, textTransform: 'uppercase', letterSpacing: '0.02em'
            }}>
              <span>{item.label}</span>
              <span style={{
                fontFamily: 'var(--font-body)', fontWeight: 600, fontSize: 11,
                letterSpacing: '0.16em', color: 'var(--fg-3)'
              }}>{item.numeral}</span>
            </a>);

        })}
      </div>
    </div>);

}

// ─── FOOTER ─────────────────────────────────────────────────────
// ─── ROMAN NUMERAL HELPER ────────────────────────────────────────
function toRoman(n) {
  const vals = [1000,900,500,400,100,90,50,40,10,9,5,4,1];
  const syms = ['M','CM','D','CD','C','XC','L','XL','X','IX','V','IV','I'];
  let result = '';
  for (let i = 0; i < vals.length; i++) {
    while (n >= vals[i]) { result += syms[i]; n -= vals[i]; }
  }
  return result;
}

const KT_UX_START_YEAR = 2005; // MMV — update this if the start year ever changes

function Footer({ mode, accent }) {
  const currentYear  = new Date().getFullYear();
  const yearsExp     = currentYear - KT_UX_START_YEAR;
  const romanStart   = toRoman(KT_UX_START_YEAR);   // MMV
  const romanCurrent = toRoman(currentYear);          // MMXXVI, MMXXVII, …
  const romanYears   = toRoman(yearsExp);             // XXI, XXII, …
  return (
    <footer style={{
      marginTop: 96, padding: '56px 28px 40px',
      borderTop: '1px solid var(--line-medium)',
      background: 'var(--bg-page)'
    }}>
      <div style={{ maxWidth: 1240, margin: '0 auto' }}>
        <div className="kt-footer-grid" style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
          gap: 32, alignItems: 'flex-start'
        }}>
          <div>
            <Wordmark height={20} variant={mode === 'light' ? accent === 'moss' ? 'moss' : 'indigo' : 'reversed'} />
            <div className="oma-small" style={{ marginTop: 16, color: 'var(--fg-2)', maxWidth: 280 }}>
              {KT.profile.craft}
            </div>
          </div>
          <div>
            <Eyebrow style={{ marginBottom: 12 }}>The Channels Are Open</Eyebrow>
            <a href={'mailto:' + KT.contact.primary} style={{
              color: 'var(--fg-1)', textDecoration: 'none',
              fontFamily: 'var(--font-mono)', fontSize: 13
            }}>{KT.contact.primary}</a>
          </div>
          <div>
            <Eyebrow style={{ marginBottom: 12 }}>Elsewhere</Eyebrow>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              {KT.contact.social.map((s) =>
              <a key={s.label} href={s.href} style={{
                color: 'var(--fg-2)', textDecoration: 'none',
                fontFamily: 'var(--font-body)', fontSize: 14
              }}>{s.label}</a>
              )}
            </div>
          </div>
          <div className="kt-footer-last-col" style={{ textAlign: 'right' }}>
            <Eyebrow style={{ marginBottom: 12 }}>At the Cauldron</Eyebrow>
            <div style={{
              fontFamily: '"Ohno Blazeface 18", var(--font-display)',
              fontSize: 18, textTransform: 'uppercase', letterSpacing: '0.02em',
              color: 'var(--fg-1)', lineHeight: 1.1
            }}>{romanStart} — {romanCurrent}
            </div>
            <div className="oma-small" style={{ color: 'var(--fg-3)', marginTop: 6 }}>
              {romanYears} years of product experience
            </div>
          </div>
        </div>
        <div className="kt-footer-bottom" style={{
          marginTop: 56, paddingTop: 24,
          borderTop: '1px solid var(--line-soft)',
          display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 16,
          fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-3)'
        }}>
          <span>© {romanCurrent} · Katy Towell · all rites reserved</span>
          <span>Built by Claude. Designed by a human.</span>
        </div>
      </div>
    </footer>);

}

// ─── SUIT GLYPHS ────────────────────────────────────────────────
// Tiny tarot-suit marks for accenting category tags.
//   cups · swords · pentacles · wands
function SuitGlyph({ suit = 'pentacles', size = 11, color = 'currentColor', style }) {
  const stroke = 1.6;
  const paths = {
    // Cup: chalice — small bowl on a stem with a base
    cups:
    <g fill="none" stroke={color} strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round">
        <path d="M5 4 L19 4 L17 11 a5 5 0 0 1 -10 0 Z" />
        <line x1="12" y1="14" x2="12" y2="19" />
        <line x1="7" y1="20" x2="17" y2="20" />
      </g>,

    // Sword: vertical blade with crossguard
    swords:
    <g fill="none" stroke={color} strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round">
        <line x1="12" y1="3" x2="12" y2="17" />
        <line x1="7" y1="8" x2="17" y2="8" />
        <circle cx="12" cy="20" r="1.3" fill={color} stroke="none" />
      </g>,

    // Pentacle: five-pointed star inside a circle
    pentacles:
    <g fill="none" stroke={color} strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round">
        <circle cx="12" cy="12" r="8" />
        <path d="M12 5 L14.2 10.4 L20 10.6 L15.3 14 L17 19.6 L12 16.2 L7 19.6 L8.7 14 L4 10.6 L9.8 10.4 Z" />
      </g>,

    // Wand: budding rod — vertical with two leaf strokes at the top
    wands:
    <g fill="none" stroke={color} strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round">
        <line x1="12" y1="6" x2="12" y2="20" />
        <path d="M12 6 C 9 5 8 3 8 3" />
        <path d="M12 6 C 15 5 16 3 16 3" />
        <circle cx="12" cy="6" r="0.9" fill={color} stroke="none" />
      </g>

  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24"
    aria-label={suit}
    style={{ display: 'inline-block', verticalAlign: 'middle', flexShrink: 0, ...style }}>
      {paths[suit] || paths.pentacles}
    </svg>);

}

// ─── CELESTIAL GLYPHS ───────────────────────────────────────────
// A small ornamental family used as section marks, hover indicators,
// and inline flourishes. All share a 24px box and currentColor stroke.
function StarGlyph({ size = 14, color = 'currentColor', style }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24"
    style={{ display: 'inline-block', flexShrink: 0, ...style }}>
      <g fill="none" stroke={color} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
        <path d="M12 3 L13.6 10.4 L21 12 L13.6 13.6 L12 21 L10.4 13.6 L3 12 L10.4 10.4 Z" />
      </g>
    </svg>);
}
function EyeGlyph({ size = 16, color = 'currentColor', style }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24"
    style={{ display: 'inline-block', flexShrink: 0, ...style }}>
      <g fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
        <path d="M2.5 12 Q 12 4 21.5 12 Q 12 20 2.5 12 Z" />
        <circle cx="12" cy="12" r="2.6" fill={color} stroke="none" />
      </g>
    </svg>);
}
function KeyGlyph({ size = 16, color = 'currentColor', style }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24"
    style={{ display: 'inline-block', flexShrink: 0, ...style }}>
      <g fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
        <circle cx="7" cy="12" r="3.6" />
        <line x1="10.6" y1="12" x2="20" y2="12" />
        <line x1="16" y1="12" x2="16" y2="15.5" />
        <line x1="19" y1="12" x2="19" y2="14.5" />
      </g>
    </svg>);
}
function HandGlyph({ size = 16, color = 'currentColor', style }) {
  // open palm with an eye in the center — a hamsa-style mark
  return (
    <svg width={size} height={size} viewBox="0 0 24 24"
    style={{ display: 'inline-block', flexShrink: 0, ...style }}>
      <g fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
        <path d="M7 21 L7 14 Q5 13 5 10 L5 7 Q5 5 6.5 5 Q8 5 8 7 L8 11 M11 11 L11 4 Q11 2.5 12.5 2.5 Q14 2.5 14 4 L14 11 M17 11 L17 6 Q17 4.5 18.5 4.5 Q20 4.5 20 6 L20 13 Q20 17 17 19 L17 21" />
        <circle cx="12.5" cy="15" r="1.6" fill={color} stroke="none" />
      </g>
    </svg>);
}
function SaturnGlyph({ size = 16, color = 'currentColor', style }) {
  // Saturn drawn in three layers using SVG masks:
  //  1. Ring behind planet — upper half only, hidden where it overlaps the planet disk
  //  2. Planet circle
  //  3. Ring in front — lower half only, overlaid on top
  //
  // mask: white = show, black = hide
  return (
    <svg width={size} height={size} viewBox="0 0 24 24"
      style={{ display: 'inline-block', flexShrink: 0, ...style }}>
      <defs>
        {/* Back ring: show upper half (y < 11), hide the planet disk */}
        <mask id="kt-saturn-back">
          <rect x="0" y="0" width="24" height="11" fill="white" />
          <circle cx="12" cy="11" r="4.7" fill="black" />
        </mask>
        {/* Front ring: show lower half only (y ≥ 11) */}
        <mask id="kt-saturn-front">
          <rect x="0" y="11" width="24" height="13" fill="white" />
        </mask>
      </defs>

      {/* Ring going behind the planet — faint, upper half visible */}
      <ellipse cx="12" cy="12.5" rx="9" ry="3"
        transform="rotate(-15 12 12.5)"
        fill="none" stroke={color} strokeWidth="1.5"
        strokeLinecap="round" strokeLinejoin="round"
        mask="url(#kt-saturn-back)"
        opacity="0.4"
      />

      {/* Planet body */}
      <circle cx="12" cy="11" r="4.5"
        fill="none" stroke={color} strokeWidth="1.5"
        strokeLinecap="round" strokeLinejoin="round"
      />

      {/* Ring passing in front of the planet — full opacity, lower half */}
      <ellipse cx="12" cy="12.5" rx="9" ry="3"
        transform="rotate(-15 12 12.5)"
        fill="none" stroke={color} strokeWidth="1.5"
        strokeLinecap="round" strokeLinejoin="round"
        mask="url(#kt-saturn-front)"
      />
    </svg>);
}
function SparkGlyph({ size = 14, color = 'currentColor', style }) {
  // the ✦ shape rendered cleanly as SVG, for inline use
  return (
    <svg width={size} height={size} viewBox="0 0 24 24"
    style={{ display: 'inline-block', verticalAlign: 'middle', flexShrink: 0, ...style }}>
      <g fill={color} stroke="none">
        <path d="M12 2 L13 10 L21 12 L13 14 L12 22 L11 14 L3 12 L11 10 Z" />
      </g>
    </svg>);
}

// ─── SMALL GLYPHS ──────────────────────────────────────────────
function SunGlyph({ size = 16 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
      <circle cx="12" cy="12" r="3.5" />
      <line x1="12" y1="2.5" x2="12" y2="5" /><line x1="12" y1="19" x2="12" y2="21.5" />
      <line x1="2.5" y1="12" x2="5" y2="12" /><line x1="19" y1="12" x2="21.5" y2="12" />
      <line x1="5.2" y1="5.2" x2="7" y2="7" /><line x1="17" y1="17" x2="18.8" y2="18.8" />
      <line x1="5.2" y1="18.8" x2="7" y2="17" /><line x1="17" y1="7" x2="18.8" y2="5.2" />
    </svg>);

}
function MoonGlyph({ size = 16 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
      <path d="M19 14 a8 8 0 1 1 -9 -9 a6 6 0 0 0 9 9 Z" />
      <path d="M6 7 L7 5.5 L7.5 7 L9 7.5 L7.5 8 L7 9.5 L6.5 8 L5 7.5 Z" fill="currentColor" stroke="none" />
    </svg>);

}

// ─── MOON PHASE (for journal entries) ───────────────────────────
function PhaseGlyph({ phase = 'waning-gibbous', size = 16, color = 'var(--moss)' }) {
  const r = size / 2;
  let face;
  switch (phase) {
    case 'new':face = <circle cx={r} cy={r} r={r - 0.8} fill="none" stroke={color} strokeWidth="1.2" />;break;
    case 'full':face = <circle cx={r} cy={r} r={r - 0.8} fill={color} />;break;
    case 'first-quarter':face = <><circle cx={r} cy={r} r={r - 0.8} fill="none" stroke={color} strokeWidth="1.2" /><path d={`M${r} 0.8 A ${r - 0.8} ${r - 0.8} 0 0 1 ${r} ${size - 0.8} Z`} fill={color} /></>;break;
    case 'last-quarter':face = <><circle cx={r} cy={r} r={r - 0.8} fill="none" stroke={color} strokeWidth="1.2" /><path d={`M${r} 0.8 A ${r - 0.8} ${r - 0.8} 0 0 0 ${r} ${size - 0.8} Z`} fill={color} /></>;break;
    case 'waxing-gibbous':face = <><circle cx={r} cy={r} r={r - 0.8} fill={color} /><path d={`M${r - 2} 0.8 A ${r - 0.8} ${r - 0.8} 0 0 0 ${r - 2} ${size - 0.8} A ${r - 4} ${r - 0.8} 0 0 0 ${r - 2} 0.8 Z`} fill="var(--bg-page)" /></>;break;
    case 'waning-gibbous':face = <><circle cx={r} cy={r} r={r - 0.8} fill={color} /><path d={`M${r + 2} 0.8 A ${r - 0.8} ${r - 0.8} 0 0 1 ${r + 2} ${size - 0.8} A ${r - 4} ${r - 0.8} 0 0 1 ${r + 2} 0.8 Z`} fill="var(--bg-page)" /></>;break;
    default:face = <circle cx={r} cy={r} r={r - 0.8} fill={color} />;
  }
  return <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>{face}</svg>;
}

// ─── LIGHTBOX ────────────────────────────────────────────────────
// Full-screen image viewer with keyboard nav, zoom, and optional device frames.
var ZOOM_STEPS = [1, 1.5, 2, 3];

function Lightbox({ images, startIndex = 0, onClose }) {
  const [idx, setIdx]         = React.useState(startIndex);
  const [zoomIdx, setZoomIdx] = React.useState(0);
  const img   = images[idx];
  const total = images.length;
  const zoom  = ZOOM_STEPS[zoomIdx];

  // Detect mobile viewport — no device frames on mobile
  const isMobile = window.innerWidth < 768;

  // Reset zoom when image changes
  React.useEffect(() => { setZoomIdx(0); }, [idx]);

  function cycleZoom(e) {
    e.stopPropagation();
    setZoomIdx(i => (i + 1) % ZOOM_STEPS.length);
  }

  // keyboard nav
  React.useEffect(() => {
    function onKey(e) {
      if (e.key === 'Escape')      onClose();
      if (e.key === 'ArrowLeft')   setIdx(i => Math.max(0, i - 1));
      if (e.key === 'ArrowRight')  setIdx(i => Math.min(total - 1, i + 1));
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [total, onClose]);

  // lock scroll on body (lightbox itself scrolls)
  React.useEffect(() => {
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = ''; };
  }, []);

  // Touch swipe (only when not zoomed)
  const touchStartX = React.useRef(null);
  function onTouchStart(e) { touchStartX.current = e.touches[0].clientX; }
  function onTouchEnd(e) {
    if (touchStartX.current === null || zoom !== 1) return;
    const dx = e.changedTouches[0].clientX - touchStartX.current;
    if (Math.abs(dx) > 40) {
      if (dx < 0) setIdx(i => Math.min(total - 1, i + 1));
      else        setIdx(i => Math.max(0, i - 1));
    }
    touchStartX.current = null;
  }

  // Device frame wrapper — suppressed entirely on mobile viewports
  function wrapDevice(imgEl, deviceType) {
    if (!isMobile && deviceType === 'desktop') {
      return (
        <div className="kt-device-desktop">
          <div className="kt-device-screen">{imgEl}</div>
        </div>
      );
    }
    if (!isMobile && deviceType === 'phone') {
      return (
        <div className="kt-device-phone">
          <div className="kt-device-screen">{imgEl}</div>
        </div>
      );
    }
    return imgEl;
  }

  const isDevice = !isMobile && (img.deviceType === 'phone' || img.deviceType === 'desktop');

  const imgEl = isDevice ? (
    <img src={img.url} alt={img.alt || ''} style={{ width: '100%', height: 'auto', display: 'block' }} />
  ) : (
    <img src={img.url} alt={img.alt || ''} style={{ maxWidth: '100%', maxHeight: '78vh', objectFit: 'contain', display: 'block', borderRadius: 'var(--r-xs)' }} />
  );

  return ReactDOM.createPortal(
    <div
      className="kt-lightbox"
      onClick={onClose}
      onTouchStart={onTouchStart}
      onTouchEnd={onTouchEnd}
      role="dialog" aria-modal="true" aria-label="Image viewer"
    >
      {/* toolbar: zoom + close */}
      <div onClick={e => e.stopPropagation()} style={{
        display: 'flex', justifyContent: 'flex-end', alignItems: 'center',
        gap: 10, padding: '14px 16px', flexShrink: 0,
      }}>
        {/* zoom — pill shape with icon + label */}
        <button
          onClick={cycleZoom}
          aria-label={`Zoom ${zoom}×`}
          style={{
            display: 'flex', alignItems: 'center', gap: 6,
            height: 36, padding: '0 14px', borderRadius: 999,
            border: '1.5px solid rgba(150,165,96,0.45)',
            background: 'rgba(20,14,8,0.72)',
            color: '#96A560', fontSize: 13, fontFamily: 'var(--font-body)',
            fontWeight: 600, letterSpacing: '0.04em',
            cursor: 'pointer', flexShrink: 0, lineHeight: 1,
          }}
        >
          <span style={{ fontSize: 16 }}>⊕</span>
          <span>{zoom}×</span>
        </button>

        {/* close — rounder pill */}
        <button
          onClick={onClose}
          aria-label="Close"
          style={{
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            height: 36, width: 36, borderRadius: 999,
            border: '1.5px solid rgba(150,165,96,0.45)',
            background: 'rgba(20,14,8,0.72)',
            color: '#96A560', fontSize: 18, lineHeight: 1,
            cursor: 'pointer', flexShrink: 0,
          }}
        >
          ×
        </button>
      </div>

      {/* centred content column — clicking dark area closes */}
      <div className="kt-lightbox-body" onClick={onClose}>
        {/* image wrap with zoom applied — stop propagation so clicking image doesn't close */}
        <div className="kt-lightbox-img-wrap" style={{ zoom }} onClick={e => e.stopPropagation()}>
          {idx > 0 && (
            <button className="kt-lightbox-nav prev" onClick={() => setIdx(i => i - 1)} aria-label="Previous image">‹</button>
          )}

          {wrapDevice(imgEl, img.deviceType)}

          {idx < total - 1 && (
            <button className="kt-lightbox-nav next" onClick={() => setIdx(i => i + 1)} aria-label="Next image">›</button>
          )}
        </div>

        {/* caption + counter */}
        {img.caption && (
          <div className="kt-lightbox-caption">{img.caption}</div>
        )}
        {total > 1 && (
          <div className="kt-lightbox-counter">{idx + 1} / {total}</div>
        )}
      </div>
    </div>,
    document.body
  );
}

// ─── IMAGE GALLERY ────────────────────────────────────────────────
// Thumbnail grid. Clicking opens the Lightbox.
function ImageGallery({ images, label }) {
  const [lightboxIdx, setLightboxIdx] = React.useState(null);

  if (!images || images.length === 0) return null;

  return (
    <div>
      {label && (
        <Eyebrow style={{ marginBottom: 16 }}>{label}</Eyebrow>
      )}
      <div className="kt-gallery-grid">
        {images.map((img, i) => (
          <button
            key={i}
            className="kt-gallery-thumb"
            onClick={() => setLightboxIdx(i)}
            aria-label={img.alt || img.caption || ('Image ' + (i + 1))}
            style={{ border: 'none', padding: 0, cursor: 'zoom-in', background: 'var(--ink-200)' }}
          >
            <img
              src={img.url}
              alt={img.alt || ''}
              loading="lazy"
            />
          </button>
        ))}
      </div>
      {lightboxIdx !== null && (
        <Lightbox
          images={images}
          startIndex={lightboxIdx}
          onClose={() => setLightboxIdx(null)}
        />
      )}
    </div>
  );
}

// ─── SPARKLE EFFECT ──────────────────────────────────────────────
var SPARKLE_COLORS = ['#96A560', '#B8A86E', '#F0E4C0', '#96A560', '#C8B878'];

function useSparkles() {
  const [sparkles, setSparkles] = React.useState([]);
  const lastFire = React.useRef(0);

  function fire(e) {
    const now = Date.now();
    if (now - lastFire.current < 110) return;
    lastFire.current = now;
    const rect = e.currentTarget.getBoundingClientRect();
    const cx = e.clientX - rect.left;
    const cy = e.clientY - rect.top;
    const batch = Array.from({ length: 3 }, () => ({
      id: Math.random().toString(36).slice(2),
      x: cx + (Math.random() - 0.5) * 52,
      y: cy + (Math.random() - 0.5) * 52,
      size: Math.random() * 9 + 7,
      color: SPARKLE_COLORS[Math.floor(Math.random() * SPARKLE_COLORS.length)],
    }));
    setSparkles(s => [...s, ...batch]);
    setTimeout(() => {
      const ids = new Set(batch.map(b => b.id));
      setSparkles(s => s.filter(sp => !ids.has(sp.id)));
    }, 720);
  }

  function clear() { setSparkles([]); }

  return { sparkles, fire, clear };
}

function SparkleLayer({ sparkles }) {
  return sparkles.map(sp => (
    <div key={sp.id} className="kt-sparkle" style={{ left: sp.x, top: sp.y }}>
      <svg width={sp.size} height={sp.size} viewBox="0 0 24 24" overflow="visible">
        <path fill={sp.color}
          d="M12 0 L13.4 10.6 L24 12 L13.4 13.4 L12 24 L10.6 13.4 L0 12 L10.6 10.6 Z" />
      </svg>
    </div>
  ));
}

// expose
Object.assign(window, {
  Grain, Wordmark, MarkIcon, Rune, AccentDot,
  Btn, Eyebrow, Tag, Rule, SectionHead,
  PageFade, NavLink, navTo,
  TopBar, MobileMenu, Footer,
  SunGlyph, MoonGlyph, PhaseGlyph,
  SuitGlyph, StarGlyph, EyeGlyph, KeyGlyph, HandGlyph, SparkGlyph, SaturnGlyph,
  Lightbox, ImageGallery,
  useSparkles, SparkleLayer,
});