/* InvitationPage — 私人邀请式落地页 */
const { useState, useEffect, useRef, useMemo, useCallback } = React;
const { motion, AnimatePresence, cubicBezier } = window.Motion;

/* ============ design tokens ============ */
const C = {
  bg:    '#0F0A0D',
  gold:  '#D4AF7A',
  wine:  '#6B1028',
  plum:  '#2A1620',
  cream: '#F5E6D3',
  rose:  '#A89084',
};

/* cinematic easing */
const EASE = cubicBezier(0.25, 0.1, 0.25, 1);
const fadeUp = {
  initial: { opacity: 0, y: 30 },
  animate: { opacity: 1, y: 0 },
  transition: { duration: 1.2, ease: EASE },
};
const fadeUpView = (delay = 0) => ({
  initial: { opacity: 0, y: 30 },
  whileInView: { opacity: 1, y: 0 },
  viewport: { once: true, amount: 0.35 },
  transition: { duration: 1.2, ease: EASE, delay },
});

/* ============ inline icons (lucide-style, hairline) ============ */
const Icon = ({ d, size = 22, stroke = 1.4, children, ...rest }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
       stroke="currentColor" strokeWidth={stroke}
       strokeLinecap="round" strokeLinejoin="round" {...rest}>
    {d && <path d={d} />}
    {children}
  </svg>
);
const IconSend = (p) => (
  <Icon {...p}>
    <path d="M22 2 11 13" />
    <path d="M22 2 15 22l-4-9-9-4 20-7Z" />
  </Icon>
);
const IconMsg = (p) => (
  <Icon {...p} d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5Z" />
);
const IconMail = (p) => (
  <Icon {...p}>
    <rect x="2" y="4" width="20" height="16" rx="0.5" />
    <path d="m22 6-10 7L2 6" />
  </Icon>
);
const IconMusic = (p) => (
  <Icon {...p}>
    <path d="M9 18V5l12-2v13" />
    <circle cx="6" cy="18" r="3" />
    <circle cx="18" cy="16" r="3" />
  </Icon>
);
const IconHeart = (p) => (
  <Icon {...p} d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78Z" />
);
const IconChevron = (p) => <Icon {...p} d="M9 18l6-6-6-6" />;
const IconMessageSquare = (p) => (
  <Icon {...p}>
    <rect x="3" y="4" width="18" height="14" rx="1.5" />
    <path d="M7 19l3-3" />
  </Icon>
);
const IconPause = (p) => (
  <Icon {...p}>
    <rect x="6.5" y="5" width="3" height="14" rx="0.5" />
    <rect x="14.5" y="5" width="3" height="14" rx="0.5" />
  </Icon>
);
const IconVideo = (p) => (
  <Icon {...p}>
    <rect x="2" y="6" width="14" height="12" rx="1.5" />
    <path d="m22 8-6 4 6 4V8Z" />
  </Icon>
);

/* ============ silhouette SVG (man, hairline) ============ */
const Silhouette = () => (
  <svg viewBox="0 0 200 380" className="w-full h-full" fill="none"
       stroke={C.gold} strokeWidth="1.1" strokeLinecap="round" strokeLinejoin="round">
    {/* head, side profile */}
    <path d="M70 56 C 70 30, 110 24, 122 42 C 130 54, 130 68, 126 78 C 132 80, 134 86, 130 92 C 132 100, 128 108, 120 110 L 118 122" />
    {/* nose / lip / chin */}
    <path d="M122 64 C 128 66, 132 70, 130 74 L 124 76" />
    <path d="M122 82 L 126 84 L 122 86" />
    <path d="M120 92 C 118 96, 116 98, 114 100" />
    {/* eye hint */}
    <path d="M108 62 C 110 60, 114 60, 116 62" />
    {/* hair */}
    <path d="M70 56 C 66 38, 88 22, 108 22 C 124 22, 132 30, 130 44" />
    {/* neck */}
    <path d="M108 118 L 108 138 C 108 148, 96 152, 88 152" />
    {/* shirt collar - opened two buttons */}
    <path d="M60 156 L 88 150 L 108 162 L 128 150 L 158 158" />
    <path d="M88 150 L 96 200" />
    <path d="M108 162 L 102 220" />
    {/* placket open */}
    <path d="M96 200 L 108 212" />
    <path d="M108 212 L 100 230" />
    {/* shoulders */}
    <path d="M60 156 C 40 168, 30 180, 28 200 L 28 240" />
    <path d="M158 158 C 178 170, 188 180, 190 200 L 190 240" />
    {/* torso */}
    <path d="M28 240 L 40 320" />
    <path d="M190 240 L 178 320" />
    {/* waistband */}
    <path d="M40 320 L 178 320" />
    {/* one hand in pocket — right side */}
    <path d="M188 240 C 196 252, 198 268, 192 280 C 186 288, 176 290, 168 286 L 162 318" />
    {/* pocket opening hint */}
    <path d="M150 320 C 150 308, 158 300, 168 300" />
    {/* trousers crease */}
    <path d="M70 320 L 78 380" />
    <path d="M148 320 L 140 380" />
    {/* two undone buttons hint */}
    <circle cx="103" cy="178" r="0.9" fill={C.gold} stroke="none" />
    <circle cx="103" cy="194" r="0.9" fill={C.gold} stroke="none" />
  </svg>
);

/* hairline crown */
const Crown = () => (
  <svg viewBox="0 0 120 90" className="w-24 h-20" fill="none"
       stroke={C.gold} strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
    <path d="M10 70 L 20 22 L 40 50 L 60 14 L 80 50 L 100 22 L 110 70 Z" />
    <path d="M10 70 L 110 70" />
    <path d="M12 78 L 108 78" />
    <circle cx="20" cy="22" r="2" />
    <circle cx="60" cy="14" r="2" />
    <circle cx="100" cy="22" r="2" />
  </svg>
);

/* full dog silhouette — sitting profile, head tilted back & looking up at owner */
const DogSilhouette = () => (
  <svg viewBox="0 0 200 200" className="w-44 h-44" fill="none"
       stroke={C.gold} strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
    {/* rear haunch — sitting on the ground */}
    <path d="M30 170 C 18 168, 10 158, 10 144 C 10 122, 24 100, 44 90" />
    <path d="M14 158 C 8 162, 8 172, 16 174 C 26 176, 40 174, 46 168" />
    {/* belly + chest */}
    <path d="M44 90 C 60 76, 82 74, 96 82" />
    <path d="M44 90 C 52 112, 56 134, 58 160" />
    {/* front leg planted, vertical */}
    <path d="M88 110 C 86 126, 88 146, 90 168 C 92 172, 102 172, 104 168 C 106 146, 104 126, 102 110" />
    {/* second front leg behind */}
    <path d="M76 116 C 74 134, 76 154, 78 168" opacity="0.5" />
    {/* tail curled around hip */}
    <path d="M16 138 C 4 138, 0 150, 6 160 C 12 168, 24 168, 28 162" opacity="0.85" />
    {/* spine arcing UP-and-BACK from shoulders to head (head turns back and lifts) */}
    <path d="M58 86 C 78 56, 100 40, 116 36" />
    {/* neck — short, tilted, head clearly elevated */}
    <path d="M116 36 C 124 34, 130 34, 134 38" />
    {/* lower jaw — chin lifted toward sky */}
    <path d="M134 38 C 132 30, 128 24, 122 22" />
    {/* muzzle: pointed Doberman, tip aimed UP-LEFT */}
    <path d="M122 22 C 116 18, 110 18, 108 24 C 106 30, 108 36, 114 38" />
    {/* top of head, sloping back to crown */}
    <path d="M108 24 C 102 18, 96 20, 94 28 C 92 36, 96 42, 104 42" />
    {/* connect crown back to neck */}
    <path d="M104 42 C 110 42, 114 40, 116 36" />
    {/* alert pointed ears, both visible */}
    <path d="M96 22 L 92 6 L 102 16" />
    <path d="M102 16 L 110 2 L 116 14" opacity="0.85" />
    {/* eye — focused upward */}
    <circle cx="115" cy="28" r="1.1" fill={C.gold} stroke="none" />
    {/* nose tip at muzzle */}
    <circle cx="115" cy="20" r="1.2" fill={C.gold} stroke="none" />
    {/* mouth — slightly open, panting up */}
    <path d="M118 30 C 121 32, 124 32, 126 30" opacity="0.6" />
    {/* collar wrapping the lifted neck */}
    <path d="M130 44 C 138 48, 146 48, 150 44" />
    <path d="M130 44 C 130 40, 132 38, 134 38" opacity="0.7" />
    {/* small ring + chain link down */}
    <circle cx="142" cy="48" r="1.4" />
    <path d="M142 50 L 142 60" />
    {/* heart pendant hanging from collar */}
    <path d="M142 60 C 138 56, 132 58, 132 64 C 132 70, 142 78, 142 78 C 142 78, 152 70, 152 64 C 152 58, 146 56, 142 60 Z" />
  </svg>
);

/* tiny collar icon used as Wolf card footer ornament */
const MiniCollar = () => (
  <svg viewBox="0 0 24 12" width="20" height="10" fill="none"
       stroke={C.gold} strokeWidth="1" strokeLinecap="round" strokeLinejoin="round">
    <ellipse cx="12" cy="4" rx="9" ry="3" />
    <rect x="10" y="2.5" width="4" height="3" />
    <path d="M12 7 L 12 9" />
    <path d="M12 9 C 10.5 8, 9 8.5, 9 10 C 9 11.4, 12 12.5, 12 12.5 C 12 12.5, 15 11.4, 15 10 C 15 8.5, 13.5 8, 12 9 Z" />
  </svg>
);

/* tiny chain icon used as King card footer / under-crown ornament */
const MiniChain = () => (
  <svg viewBox="0 0 60 16" width="60" height="14" fill="none"
       stroke={C.gold} strokeWidth="1" strokeLinecap="round" strokeLinejoin="round">
    {[0, 1, 2, 3, 4, 5].map((i) => (
      <ellipse key={i} cx={6 + i * 9} cy={8} rx={3.5} ry={2.6} opacity={0.85} />
    ))}
  </svg>
);

/* silk · heel · final — AI-composited photograph with film-grain treatment */
const ObjetLegHeel = () => (
  <motion.div
    initial={{ opacity: 0, scale: 0.92 }}
    whileInView={{ opacity: 1, scale: 1 }}
    viewport={{ once: true, amount: 0.3 }}
    transition={{ duration: 1.6, ease: 'easeOut' }}
    className="relative overflow-hidden rounded-sm silk-photo-frame"
    style={{ width: 240, height: 320 }}>
    <img src="./images/silk-heel-final.jpg" alt="silk · heel" className="silk-photo-img" draggable={false} loading="lazy" decoding="async" />
  </motion.div>
);

/* whiskey glass with ice ball */
const ObjetWhiskey = () => (
  <svg viewBox="0 0 180 200" className="w-32 h-36" fill="none"
       stroke={C.gold} strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
    {/* glass body — low wide tumbler */}
    <path d="M40 50 L 44 158 C 46 168, 56 174, 90 174 C 124 174, 134 168, 136 158 L 140 50" />
    {/* glass rim ellipse */}
    <ellipse cx="90" cy="50" rx="50" ry="8" />
    {/* rim highlight */}
    <path d="M52 46 C 70 42, 110 42, 128 46" stroke={C.cream} strokeWidth="1.4" opacity="0.85" />
    {/* whiskey surface inside (half full) */}
    <ellipse cx="90" cy="108" rx="46" ry="6" opacity="0.7" />
    {/* whiskey body shading */}
    <path d="M44 108 C 46 130, 50 152, 56 160 C 70 168, 110 168, 124 160 C 130 152, 134 130, 136 108" opacity="0.4" />
    {/* large ice ball */}
    <circle cx="90" cy="116" r="26" />
    <path d="M76 108 C 80 102, 90 100, 100 104" opacity="0.7" />
    <path d="M82 124 C 88 130, 96 130, 102 126" opacity="0.5" />
    {/* base reflection */}
    <ellipse cx="90" cy="174" rx="44" ry="4" opacity="0.4" />
    {/* coaster shadow */}
    <path d="M30 186 L 150 186" opacity="0.4" />
  </svg>
);

/* ribbon collar with heart pendant */
const ObjetRibbon = () => (
  <svg viewBox="0 0 220 180" className="w-40 h-32" fill="none"
       stroke={C.gold} strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
    {/* ribbon main arc — pinched at center */}
    <path d="M20 50 C 50 36, 80 42, 100 58 C 110 64, 110 72, 100 78 C 80 92, 50 96, 20 80" />
    <path d="M200 50 C 170 36, 140 42, 120 58 C 110 64, 110 72, 120 78 C 140 92, 170 96, 200 80" />
    {/* ribbon back layer for depth */}
    <path d="M30 56 C 60 48, 80 54, 100 62" opacity="0.5" />
    <path d="M190 56 C 160 48, 140 54, 120 62" opacity="0.5" />
    {/* tails falling down with curve */}
    <path d="M100 76 C 90 100, 78 130, 64 160" />
    <path d="M108 80 C 100 110, 92 140, 86 170" opacity="0.7" />
    <path d="M120 76 C 130 100, 142 130, 156 160" />
    <path d="M112 80 C 120 110, 128 140, 134 170" opacity="0.7" />
    {/* tail tips cut diagonal */}
    <path d="M58 158 L 70 162" />
    <path d="M150 158 L 162 162" />
    {/* center knot */}
    <ellipse cx="110" cy="68" rx="10" ry="14" />
    {/* heart pendant on small chain below knot */}
    <path d="M110 82 L 110 96" />
    <path d="M110 96 C 106 92, 100 94, 100 100 C 100 106, 110 114, 110 114 C 110 114, 120 106, 120 100 C 120 94, 114 92, 110 96 Z" />
    {/* tiny ring above heart */}
    <circle cx="110" cy="92" r="2" />
  </svg>
);

/* ============ rose visual system (wine red + champagne gold) ============ */
const ROSE_PETALS_7 = [
  'M 20 38 C 12 30, 14 14, 32 14 C 38 22, 36 38, 20 38 Z',
  'M 80 38 C 88 30, 86 14, 68 14 C 62 22, 64 38, 80 38 Z',
  'M 28 46 C 22 38, 28 22, 44 22 C 48 30, 44 46, 28 46 Z',
  'M 72 46 C 78 38, 72 22, 56 22 C 52 30, 56 46, 72 46 Z',
  'M 38 22 C 42 12, 58 12, 62 22 C 58 28, 42 28, 38 22 Z',
  'M 44 36 C 40 30, 48 22, 50 30 C 50 38, 44 36, 44 36 Z',
  'M 50 30 C 52 22, 60 30, 56 36 C 52 38, 50 30, 50 30 Z',
];
const ROSE_PETALS_11 = [
  'M 8  44 C 2 32, 10 4, 34 2 C 44 12, 42 46, 8 44 Z',
  'M 92 44 C 98 32, 90 4, 66 2 C 56 12, 58 46, 92 44 Z',
  'M 18 56 C 8 48, 14 32, 30 32 C 36 42, 32 58, 18 56 Z',
  'M 82 56 C 92 48, 86 32, 70 32 C 64 42, 68 58, 82 56 Z',
  ...ROSE_PETALS_7,
];

/* solid SVG rose — back-to-front layered petals + optional stem + optional gold pistil */
function Rose({ size = 60, withStem = true, pistil = true, pistilNode, vTone = 'normal' }) {
  const id = useMemo(() => `rose-${Math.random().toString(36).slice(2, 7)}`, []);
  const vbH = withStem ? 130 : 62;
  const aspect = vbH / 100;
  return (
    <svg viewBox={`0 0 100 ${vbH}`} width={size} height={size * aspect} style={{ overflow: 'visible' }}>
      <defs>
        <linearGradient id={`${id}-fill`} x1="0%" y1="0%" x2="0%" y2="100%">
          <stop offset="0%"   stopColor="#7A1530" />
          <stop offset="55%"  stopColor="#6B1028" />
          <stop offset="100%" stopColor="#4A0E1F" />
        </linearGradient>
      </defs>
      {/* calyx hint */}
      <path d="M 30 48 C 38 56, 62 56, 70 48" fill="none" stroke={C.gold} strokeOpacity="0.4" strokeWidth="0.6" strokeLinecap="round" />
      {ROSE_PETALS_7.map((d, i) => (
        <path
          key={i}
          d={d}
          fill={`url(#${id}-fill)`}
          stroke={C.gold}
          strokeOpacity="0.35"
          strokeWidth="0.5"
          fillOpacity={vTone === 'normal' ? (0.62 + i * 0.05) : 0.85}
        />
      ))}
      {pistil && (pistilNode || <circle cx="50" cy="30" r="1.6" fill={C.gold} />)}
      {withStem && (
        <g>
          <path d="M 50 50 Q 47 80, 52 122" fill="none" stroke={C.gold} strokeOpacity="0.6" strokeWidth="1" strokeLinecap="round" />
          <path d="M 50 76 C 40 74, 32 84, 46 88 C 50 84, 52 80, 50 76 Z" fill="none" stroke={C.gold} strokeOpacity="0.55" strokeWidth="0.8" strokeLinejoin="round" />
          <path d="M 51 96 C 62 94, 70 104, 54 106 C 50 102, 50 98, 51 96 Z" fill="none" stroke={C.gold} strokeOpacity="0.55" strokeWidth="0.8" strokeLinejoin="round" />
        </g>
      )}
    </svg>
  );
}

/* the chapter-3½ blooming rose — 11 petals, staggered animation, breathing after.
 * iOS Safari / WeChat fix:
 *   - dropped per-path `scale` + `transform-box: fill-box` (broken on older WebKit;
 *     petals scaled to a point off-screen → invisible; stems survived because
 *     they use `pathLength`, not transform). Now opacity-only on petals/leaves/pistil/dew.
 *   - manual bloom state driven by IntersectionObserver (threshold 0.15) with a
 *     3s setTimeout fallback that force-blooms if IO never fires (covers WeChat
 *     in-app browser edge cases where IO can be flaky inside iframes / overlays).
 *   - WebkitTransform: translateZ(0) on wrapper to promote compositing layer.
 */
function BloomingRose({ size = 280 }) {
  const id = useMemo(() => `bloom-${Math.random().toString(36).slice(2, 7)}`, []);
  const total = ROSE_PETALS_11.length;
  const animDuration = 0.9; // per petal
  const stagger = 0.15;
  const lastDelay = (total - 1) * stagger; // 10 * 0.15 = 1.5
  const totalBloomTime = lastDelay + animDuration; // ≈ 2.4s

  const ref = useRef(null);
  const [bloom, setBloom] = useState(false);
  const reducedMotion = useMemo(
    () => typeof window !== 'undefined'
      && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches,
    []
  );
  useEffect(() => {
    if (bloom) return;
    // prefers-reduced-motion: bloom immediately, no observer, no stagger
    if (reducedMotion) { setBloom(true); return; }
    // 3s hard fallback — petals appear (without stagger) even if IO never fires
    const fallback = setTimeout(() => setBloom(true), 3000);
    if (!('IntersectionObserver' in window) || !ref.current) {
      return () => clearTimeout(fallback);
    }
    const io = new IntersectionObserver((entries) => {
      for (const e of entries) {
        if (e.isIntersecting) {
          setBloom(true);
          io.disconnect();
          clearTimeout(fallback);
          break;
        }
      }
    }, { threshold: 0.15, rootMargin: '0px 0px -5% 0px' });
    io.observe(ref.current);
    return () => { io.disconnect(); clearTimeout(fallback); };
  }, [bloom, reducedMotion]);

  // when reduced-motion is on, render petals at final opacity in one shot, no stagger
  const petalDelay = (reverseIdx) => (reducedMotion ? 0 : (bloom ? reverseIdx * stagger : 0));
  const breath = (bloom && !reducedMotion) ? { scale: [1, 1.02, 1] } : { scale: 1 };

  return (
    <motion.div
      ref={ref}
      animate={breath}
      transition={{ duration: 6, repeat: (bloom && !reducedMotion) ? Infinity : 0, ease: 'easeInOut', delay: bloom ? totalBloomTime : 0 }}
      style={{
        width: size,
        height: size * 1.6,
        display: 'flex',
        justifyContent: 'center',
        WebkitTransform: 'translateZ(0)',
        transform: 'translateZ(0)',
      }}>
      <svg
        viewBox="0 0 100 160"
        width={size}
        height={size * 1.6}
        xmlns="http://www.w3.org/2000/svg"
        style={{ overflow: 'visible' }}>
        <defs>
          <linearGradient id={`${id}-fill`} x1="0%" y1="0%" x2="0%" y2="100%">
            <stop offset="0%"   stopColor="#8A1A3A" />
            <stop offset="50%"  stopColor="#6B1028" />
            <stop offset="100%" stopColor="#3A0814" />
          </linearGradient>
          <radialGradient id={`${id}-glow`}>
            <stop offset="0%" stopColor={C.gold} stopOpacity="0.25" />
            <stop offset="60%" stopColor={C.gold} stopOpacity="0.05" />
            <stop offset="100%" stopColor={C.gold} stopOpacity="0" />
          </radialGradient>
        </defs>
        {/* soft halo behind bloom */}
        <circle cx="50" cy="30" r="46" fill={`url(#${id}-glow)`} />
        {/* calyx hint */}
        <motion.path
          initial={{ opacity: 0 }}
          animate={{ opacity: bloom ? 0.5 : 0 }}
          transition={{ duration: 0.6 }}
          d="M 24 50 C 36 60, 64 60, 76 50" fill="none" stroke={C.gold} strokeOpacity="0.5" strokeWidth="0.6" />
        {/* petals — opacity-only stagger (no scale → no fill-box dependency) */}
        {ROSE_PETALS_11.map((d, i) => {
          const reverseIdx = total - 1 - i;
          const targetOpacity = 0.65 + (i / total) * 0.3;
          return (
            <motion.path
              key={i}
              initial={{ opacity: 0 }}
              animate={{ opacity: bloom ? targetOpacity : 0 }}
              transition={{ duration: reducedMotion ? 0.4 : animDuration, ease: 'easeOut', delay: petalDelay(reverseIdx) }}
              d={d}
              fill={`url(#${id}-fill)`}
              stroke={C.gold}
              strokeOpacity="0.4"
              strokeWidth="0.45"
            />
          );
        })}
        {/* gold pistil — opacity only */}
        <motion.circle
          initial={{ opacity: 0 }}
          animate={{ opacity: bloom ? 1 : 0 }}
          transition={{ duration: 0.6, delay: bloom ? (reducedMotion ? 0 : totalBloomTime - 0.3) : 0 }}
          cx="50" cy="30" r="1.8" fill={C.gold} style={{ filter: `drop-shadow(0 0 4px ${C.gold})` }} />

        {/* stem — pathLength + opacity (pathLength is safe on iOS) */}
        <motion.path
          initial={{ pathLength: 0, opacity: 0 }}
          animate={{ pathLength: bloom ? 1 : 0, opacity: bloom ? 0.6 : 0 }}
          transition={{ duration: reducedMotion ? 0.4 : 1.4, ease: EASE, delay: bloom ? (reducedMotion ? 0 : 0.4) : 0 }}
          d="M 50 50 Q 47 90, 52 150"
          fill="none" stroke={C.gold} strokeOpacity="0.6" strokeWidth="0.9" strokeLinecap="round" />
        {/* leaves — opacity only */}
        <motion.path
          initial={{ opacity: 0 }}
          animate={{ opacity: bloom ? 0.55 : 0 }}
          transition={{ duration: 0.8, ease: EASE, delay: bloom ? (reducedMotion ? 0 : 1.0) : 0 }}
          d="M 50 84 C 36 82, 26 96, 46 100 C 50 94, 52 88, 50 84 Z"
          fill="none" stroke={C.gold} strokeOpacity="0.6" strokeWidth="0.7" />
        <motion.path
          initial={{ opacity: 0 }}
          animate={{ opacity: bloom ? 0.55 : 0 }}
          transition={{ duration: 0.8, ease: EASE, delay: bloom ? (reducedMotion ? 0 : 1.3) : 0 }}
          d="M 52 114 C 66 112, 76 126, 56 130 C 52 124, 50 118, 52 114 Z"
          fill="none" stroke={C.gold} strokeOpacity="0.6" strokeWidth="0.7" />
        {/* dew drop / gold dot at stem base — visual signature anchor */}
        <motion.circle
          initial={{ opacity: 0 }}
          animate={{ opacity: bloom ? 1 : 0 }}
          transition={{ duration: 0.8, ease: EASE, delay: bloom ? (reducedMotion ? 0 : totalBloomTime) : 0 }}
          cx="52" cy="150" r="3"
          fill={C.gold}
          style={{ filter: `drop-shadow(0 0 6px ${C.gold})` }} />
      </svg>
    </motion.div>
  );
}

/* mini rose for inline icon-scale anchors (≤ 28px) */
function MiniRose({ size = 24, withCenterDot = true }) {
  const id = useMemo(() => `mini-${Math.random().toString(36).slice(2, 7)}`, []);
  return (
    <svg viewBox="0 0 60 60" width={size} height={size} style={{ overflow: 'visible' }}>
      <defs>
        <linearGradient id={`${id}-fill`} x1="0%" y1="0%" x2="0%" y2="100%">
          <stop offset="0%"   stopColor="#8A1A3A" />
          <stop offset="100%" stopColor="#4A0E1F" />
        </linearGradient>
      </defs>
      {/* 5 simple layered petals scaled into 60x60 */}
      <path d="M 12 32 C 6 24, 10 12, 22 12 C 28 20, 26 32, 12 32 Z" fill={`url(#${id}-fill)`} stroke={C.gold} strokeOpacity="0.4" strokeWidth="0.4" fillOpacity="0.75" />
      <path d="M 48 32 C 54 24, 50 12, 38 12 C 32 20, 34 32, 48 32 Z" fill={`url(#${id}-fill)`} stroke={C.gold} strokeOpacity="0.4" strokeWidth="0.4" fillOpacity="0.75" />
      <path d="M 22 14 C 26 6, 34 6, 38 14 C 34 18, 26 18, 22 14 Z" fill={`url(#${id}-fill)`} stroke={C.gold} strokeOpacity="0.4" strokeWidth="0.4" fillOpacity="0.85" />
      <path d="M 24 26 C 22 18, 30 14, 30 22 C 30 30, 24 28, 24 26 Z" fill={`url(#${id}-fill)`} stroke={C.gold} strokeOpacity="0.4" strokeWidth="0.4" fillOpacity="0.92" />
      <path d="M 30 22 C 32 14, 38 18, 36 26 C 34 30, 30 26, 30 22 Z" fill={`url(#${id}-fill)`} stroke={C.gold} strokeOpacity="0.4" strokeWidth="0.4" fillOpacity="1" />
      {withCenterDot && <circle cx="30" cy="22" r="1.2" fill={C.gold} style={{ filter: `drop-shadow(0 0 3px ${C.gold})` }} />}
    </svg>
  );
}

/* ============ section: rose chapter (3½) — single blooming rose, emotional pause ============ */
function RoseChapter() {
  const isMobile = useIsMobile();
  return (
    <section
      className="relative min-h-screen w-full flex flex-col items-center justify-center px-6 py-24 overflow-hidden"
      style={{ background: C.bg }}
      data-screen-label="04 Rose">
      {/* faint wine wash behind the bloom */}
      <div aria-hidden className="absolute inset-0 pointer-events-none"
           style={{ background: `radial-gradient(ellipse 50% 60% at 50% 50%, ${C.wine}26 0%, transparent 70%)` }} />

      <motion.div {...fadeUpView(0)} className="relative mb-8">
        <span className="text-[10px] tracking-[0.5em]" style={{ color: C.rose }}>第三章半 · 一朵玫瑰</span>
      </motion.div>

      {/* the blooming rose */}
      <div className="relative" style={{ marginTop: -20 }}>
        <BloomingRose size={isMobile ? 220 : 280} />
      </div>

      <motion.div
        initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true, amount: 0.3 }}
        transition={{ duration: 1.4, ease: EASE, delay: 2.6 }}
        className="font-serif tracking-wider text-center mt-4"
        style={{
          color: C.cream,
          fontSize: 'clamp(18px, 4vw, 32px)',
          fontWeight: 300,
        }}>
        这朵 · 只为里里
      </motion.div>

      <motion.div
        initial={{ opacity: 0 }} whileInView={{ opacity: 0.6 }}
        viewport={{ once: true, amount: 0.3 }}
        transition={{ duration: 1.4, ease: EASE, delay: 3.0 }}
        className="font-serif italic text-sm text-center mt-5"
        style={{ color: C.rose, letterSpacing: '0.04em' }}>
        不会送你第二次 · 因为第一次就是最后一次
      </motion.div>

      <motion.div
        initial={{ opacity: 0 }} whileInView={{ opacity: 0.4 }}
        viewport={{ once: true, amount: 0.3 }}
        transition={{ duration: 1.4, ease: EASE, delay: 3.4 }}
        className="absolute left-0 right-0 text-center text-xs"
        style={{ color: C.gold, bottom: 24, letterSpacing: '0.4em' }}>
        one rose · forever yours
      </motion.div>
    </section>
  );
}

/* ============ helpers ============ */
const ProgressDots = ({ total, current }) => (
  <div className="flex items-center gap-3">
    {Array.from({ length: total }).map((_, i) => {
      const active = i === current;
      return (
        <span key={i}
          className="block rounded-full transition-all duration-700"
          style={{
            width: active ? 8 : 6,
            height: active ? 8 : 6,
            background: active ? C.gold : 'transparent',
            border: `1px solid ${active ? C.gold : 'rgba(212,175,122,0.45)'}`,
            boxShadow: active ? `0 0 14px ${C.gold}aa, 0 0 4px ${C.gold}` : 'none',
          }} />
      );
    })}
  </div>
);

/* "轻轻推开" — primary entry button with ripple on click */
function PushOpenButton({ onClick, delay = 1.2 }) {
  const [ripples, setRipples] = useState([]);
  const idRef = useRef(0);
  const click = () => {
    const id = ++idRef.current;
    setRipples((r) => [...r, id]);
    setTimeout(() => setRipples((r) => r.filter(x => x !== id)), 900);
    onClick?.();
  };
  return (
    <motion.button
      initial={{ opacity: 0, y: 30 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 1.4, ease: EASE, delay }}
      onClick={click}
      className="group relative mt-16 md:mt-20 px-10 py-3 rounded-full"
      style={{ border: `1px solid ${C.gold}66`, color: C.cream }}>
      {/* ripple halos */}
      {ripples.map((id) => (
        <motion.span key={id}
          initial={{ opacity: 1, scale: 1 }}
          animate={{ opacity: 0, scale: 1.4 }}
          transition={{ duration: 0.8, ease: EASE }}
          className="absolute inset-0 rounded-full pointer-events-none"
          style={{ border: `1px solid ${C.gold}`, boxShadow: `0 0 24px ${C.gold}66` }} />
      ))}
      <span className="absolute inset-0 rounded-full gold-thread opacity-0 group-hover:opacity-100 transition-opacity duration-700 overflow-hidden" />
      <span className="relative tracking-[0.4em] text-xs uppercase font-sans cinematic-letters" style={{ '--ls': '0.4em' }}>轻轻推开</span>
    </motion.button>
  );
}

/* ============ section: hero ============ */
function Hero({ onEnter }) {
  const ref = useRef(null);
  const isMobile = useIsMobile();
  const [mouse, setMouse] = useState({ x: 0, y: 0 });
  const target = useRef({ x: 0, y: 0 });
  const raf = useRef(0);

  // ceremonial intro: 0-1.2s pulsing dot center, 1.2-2.2s dot→line up, 2.5s done
  const [phase, setPhase] = useState(0); // 0 = dot, 1 = line, 2 = done
  useEffect(() => {
    const t1 = setTimeout(() => setPhase(1), 1200);
    const t2 = setTimeout(() => setPhase(2), 2500);
    return () => { clearTimeout(t1); clearTimeout(t2); };
  }, []);

  useEffect(() => {
    const handle = (e) => {
      const r = ref.current?.getBoundingClientRect();
      if (!r) return;
      const cx = r.left + r.width / 2;
      const cy = r.top + r.height / 2;
      target.current = { x: (e.clientX - cx) * 0.06, y: (e.clientY - cy) * 0.06 };
    };
    const loop = () => {
      setMouse((m) => ({
        x: m.x + (target.current.x - m.x) * 0.05,
        y: m.y + (target.current.y - m.y) * 0.05,
      }));
      raf.current = requestAnimationFrame(loop);
    };
    window.addEventListener('mousemove', handle);
    raf.current = requestAnimationFrame(loop);
    return () => { window.removeEventListener('mousemove', handle); cancelAnimationFrame(raf.current); };
  }, []);

  return (
    <section ref={ref} className="relative min-h-screen w-full flex items-center justify-center overflow-hidden">
      {/* radial glow */}
      <div
        className="absolute breathe pointer-events-none"
        style={{
          left: '50%', top: '50%',
          width: 800, height: 800,
          transform: `translate(calc(-50% + ${mouse.x}px), calc(-50% + ${mouse.y}px))`,
          background: `radial-gradient(circle, ${C.wine}33 0%, ${C.wine}11 35%, transparent 70%)`,
          filter: 'blur(20px)',
        }}
      />
      {/* secondary smaller glow for depth */}
      <div
        className="absolute breathe pointer-events-none"
        style={{
          left: '50%', top: '50%',
          width: 320, height: 320,
          animationDelay: '-3s',
          transform: `translate(calc(-50% + ${mouse.x * 1.6}px), calc(-50% + ${mouse.y * 1.6}px))`,
          background: `radial-gradient(circle, ${C.gold}1f 0%, transparent 70%)`,
          filter: 'blur(10px)',
        }}
      />

      {/* === ceremony layer: dot → line, absolutely positioned over the section === */}
      <motion.div
        aria-hidden
        className="absolute left-1/2 z-10"
        style={{
          background: C.gold,
          transform: 'translate(-50%, -50%)',
        }}
        initial={{ opacity: 0, width: 6, height: 6, borderRadius: 999, top: '50%' }}
        animate={{
          opacity: 1,
          width: phase === 0 ? 6 : 120,
          height: phase === 0 ? 6 : 1,
          borderRadius: phase === 0 ? 999 : 0,
          top: phase === 0 ? '50%' : '22%',
          boxShadow: phase === 0
            ? `0 0 14px ${C.gold}cc, 0 0 32px ${C.gold}44`
            : `0 0 10px ${C.gold}88`,
        }}
        transition={{
          opacity: { duration: 0.6, ease: EASE },
          default: { duration: 1.0, ease: EASE },
        }}
      />
      <AnimatePresence>
        {phase === 0 && (
          <motion.div
            key="halo"
            initial={{ opacity: 0, scale: 0.8 }}
            animate={{ opacity: 0.55, scale: [1, 1.4, 1] }}
            exit={{ opacity: 0 }}
            transition={{
              opacity: { duration: 0.5 },
              scale: { duration: 1.2, ease: 'easeInOut', repeat: Infinity },
            }}
            aria-hidden
            className="absolute left-1/2 z-10 pointer-events-none"
            style={{
              top: '50%',
              width: 16, height: 16, borderRadius: 999,
              border: `1px solid ${C.gold}99`,
              transform: 'translate(-50%, -50%)',
            }}
          />
        )}
      </AnimatePresence>
      <AnimatePresence>
        {phase === 0 && (
          <motion.div
            key="opening-sub"
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 0.55, y: 0 }}
            exit={{ opacity: 0, y: -8 }}
            transition={{ duration: 0.8, ease: EASE, delay: 0.4 }}
            aria-hidden
            className="absolute left-1/2 z-10 text-xs"
            style={{
              top: 'calc(50% + 22px)',
              transform: 'translateX(-50%)',
              color: C.rose,
              letterSpacing: '0.4em',
              whiteSpace: 'nowrap',
            }}>
            为你 · 仅此一人
          </motion.div>
        )}
      </AnimatePresence>

      <div className="relative z-10 flex flex-col items-center text-center px-6">
        <motion.h1
          initial={{ opacity: 0, y: 30 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 1.4, ease: EASE, delay: 1.4 }}
          className="font-serif tracking-wider"
          style={{ color: C.cream, fontWeight: 400, fontSize: 'clamp(80px, 18vw, 168px)', lineHeight: 1 }}>
          晚安
        </motion.h1>

        <motion.p
          initial={{ opacity: 0, y: 30 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 1.4, ease: EASE, delay: 1.9 }}
          className="font-serif italic mt-6 text-base md:text-lg"
          style={{ color: C.rose, opacity: 0.55 }}>
          或者,陪我熬一会儿,里里
        </motion.p>

        <PushOpenButton onClick={onEnter} delay={2.4} />

        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 0.45 }}
          transition={{ duration: 1.4, ease: EASE, delay: 4.2 }}
          className="mt-5 text-xs tracking-wider"
          style={{ color: C.rose }}>
          推开门 · 音乐已为你准备好
        </motion.div>

        <motion.div
          initial={{ opacity: 0, y: 30 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 1.4, ease: EASE, delay: 2.8 }}
          className="mt-20 flex flex-col items-center gap-3" style={{ color: C.rose }}>
          <span className="text-[10px] tracking-[0.5em] opacity-60">向下</span>
          <span className="scroll-tick block h-10 w-px" style={{ background: `${C.gold}55` }} />
        </motion.div>
      </div>

      {/* exclusive page number — L.L */}
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        transition={{ duration: 1.6, ease: EASE, delay: 3.0 }}
        className="absolute bottom-6 right-6 z-20 flex items-center gap-3"
        style={{ color: C.gold }}>
        <span className="h-[6px] w-[6px] rounded-full"
              style={{ background: C.gold, boxShadow: `0 0 8px ${C.gold}` }} />
        <span className="font-serif tracking-[0.4em] text-xs" style={{ fontWeight: 400 }}>里里</span>
        <span className="h-px w-6" style={{ background: `${C.gold}66` }} />
        <span className="text-[10px] tracking-[0.4em] opacity-70">独此一份</span>
      </motion.div>

      {/* hidden rose — Hero's quiet bloom of intent (bottom-left, far from scroll indicator) */}
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 0.4 }}
        transition={{ duration: 1.6, ease: EASE, delay: 3.5 }}
        aria-hidden
        className="absolute bottom-10 left-8 z-10 pointer-events-none">
        <Rose size={isMobile ? 40 : 32} withStem={true} pistil={true} />
      </motion.div>

      {/* deep wine warmth rising from the bottom of the hero */}
      <div
        aria-hidden
        className="absolute inset-x-0 bottom-0 pointer-events-none"
        style={{
          height: '55%',
          background: `linear-gradient(to top, ${C.wine}26 0%, ${C.wine}10 30%, transparent 100%)`,
        }}
      />
    </section>
  );
}

/* ============ section: objets — 为你准备的 ============ */
function Objets() {
  const isMobile = useIsMobile();
  const items = [
    {
      Svg: ObjetLegHeel,
      title: '想看你穿着它,走过来',
      sub: '也想看你穿着它,站在我面前',
      details: [
        '高跟敲在地板上的声音',
        '丝袜在小腿弯曲时拉伸的弧度',
        '你停在我面前问"好看吗"的眼神',
      ],
      xtra: 'silk · heel · for her, only',
      label: '其一 · 丝袜与高跟',
    },
    {
      Svg: ObjetWhiskey,
      title: '想看你拿着它,慢慢喝',
      sub: '喝完再说要不要看我',
      label: '其二 · 威士忌不加冰',
    },
    {
      Svg: ObjetRibbon,
      title: '想看你戴上它,或者给我戴上',
      sub: '你做主人 · 还是让我做',
      label: '其三 · 丝带与项圈',
    },
  ];
  return (
    <section className="relative min-h-screen w-full flex flex-col items-center justify-center px-6 py-24 overflow-hidden">
      {/* faint horizontal wine wash for atmosphere */}
      <div className="absolute inset-0 pointer-events-none"
           style={{ background: `radial-gradient(ellipse 70% 40% at 50% 50%, ${C.wine}1f, transparent 70%)` }} />
      {/* huge ghost rose at center-right — atmospheric backdrop */}
      <div aria-hidden
        className="absolute pointer-events-none"
        style={{
          right: isMobile ? '50%' : '8%',
          top: '50%',
          transform: isMobile ? 'translate(50%, -50%)' : 'translateY(-50%)',
          opacity: 0.06,
          filter: 'blur(8px)',
        }}>
        <Rose size={isMobile ? 360 : 480} withStem={true} pistil={false} />
      </div>

      <motion.div {...fadeUpView(0)} className="relative mb-4">
        <span className="text-[10px] tracking-[0.5em]" style={{ color: C.rose }}>第三章 · 为你准备</span>
      </motion.div>
      <motion.h2 {...fadeUpView(0.1)}
        className="relative font-serif tracking-wider text-center"
        style={{ color: C.cream, fontSize: 'clamp(40px, 6vw, 72px)', lineHeight: 1.15, fontWeight: 400 }}>
        为你准备的
      </motion.h2>
      <motion.p {...fadeUpView(0.25)}
        className="relative font-serif italic mt-5 tracking-wider"
        style={{ color: C.rose, fontSize: 15 }}>
        因为这些,你穿得起,里里
      </motion.p>

      <motion.div {...fadeUpView(0.4)}
        className="relative mt-20 w-full max-w-5xl flex flex-col md:flex-row items-stretch justify-center gap-10 md:gap-6">
        {items.map((it, idx) => (
          <React.Fragment key={idx}>
            {idx > 0 && (
              <div aria-hidden className="hidden md:flex flex-col items-center justify-center" style={{ width: 12 }}>
                <span className="block rounded-full"
                      style={{ width: 6, height: 6, background: C.gold, opacity: 0.7, boxShadow: `0 0 10px ${C.gold}77` }} />
              </div>
            )}
            <ObjetCard idx={idx} {...it} />
          </React.Fragment>
        ))}
      </motion.div>

      <motion.p {...fadeUpView(0.7)}
        className="relative font-serif italic mt-20 tracking-wider text-center"
        style={{ color: `${C.rose}aa`, fontSize: 13 }}>
        你挑就好,剩下的交给我
      </motion.p>
    </section>
  );
}

function ObjetCard({ Svg, title, sub, details, xtra, label, idx }) {
  const [hover, setHover] = useState(false);
  return (
    <div
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      className="group relative flex-1 flex flex-col items-center px-6 py-10"
      style={{
        transition: 'background 0.6s, border-color 0.6s',
      }}>
      {/* index label */}
      <div className="text-[10px] tracking-[0.4em] mb-8"
           style={{ color: hover ? C.gold : `${C.rose}88`, transition: 'color 0.5s' }}>
        {label}
      </div>

      {/* svg holder */}
      <div className="relative flex items-end justify-center"
           style={{
             height: 320,
             opacity: hover ? 1 : 0.78,
             transition: 'opacity 0.5s, filter 0.5s',
             filter: hover ? `drop-shadow(0 0 18px ${C.gold}44)` : 'none',
           }}>
        <Svg />
      </div>

      {/* hairline */}
      <div className="hairline w-16 my-6"
           style={{ opacity: hover ? 1 : 0.5, transition: 'opacity 0.5s' }} />

      {/* copy */}
      <div className="font-serif tracking-wider text-center"
           style={{
             color: hover ? C.cream : C.rose,
             fontSize: 18,
             lineHeight: 1.6,
             fontWeight: 300,
             transition: 'color 0.5s',
           }}>
        {title}
      </div>
      <div className="font-serif italic tracking-wider mt-3 text-center"
           style={{
             color: hover ? `${C.rose}dd` : `${C.rose}88`,
             fontSize: 13,
             transition: 'color 0.5s',
           }}>
        {sub}
      </div>
      {details && details.length > 0 && (
        <div className="mt-5 flex flex-col items-center gap-1.5">
          {details.map((d, i) => (
            <div key={i}
                 className="font-serif italic text-center"
                 style={{
                   color: hover ? `${C.cream}cc` : `${C.rose}99`,
                   fontSize: 12,
                   letterSpacing: '0.05em',
                   lineHeight: 1.6,
                   transition: 'color 0.6s',
                 }}>
              · {d} ·
            </div>
          ))}
        </div>
      )}
      {xtra && (
        <div className="font-serif italic tracking-[0.2em] mt-2 text-center text-xs"
             style={{
               color: C.rose,
               opacity: hover ? 0.6 : 0.4,
               transition: 'opacity 0.5s',
             }}>
          {xtra}
        </div>
      )}
    </div>
  );
}

/* ============ section: monologue ============ */
function Monologue() {
  const isMobile = useIsMobile();
  const lines = [
    '我穿西装时,你看不出我的另一面',
    '我跪下时,也不会显得卑微',
    '你想要哪一个,我都给你',
  ];
  return (
    <section className="relative min-h-screen w-full flex items-center px-6 md:px-20 py-24">
      {/* companion rose at bottom-right, tilted toward the silhouette */}
      <motion.div
        initial={{ opacity: 0, y: 12 }} whileInView={{ opacity: 0.55, y: 0 }}
        viewport={{ once: true, amount: 0.4 }}
        transition={{ duration: 1.4, ease: EASE, delay: 0.6 }}
        aria-hidden
        className="absolute z-10 pointer-events-none"
        style={{
          bottom: 64,
          right: isMobile ? 26 : 40,
          transform: 'rotate(-15deg)',
        }}>
        <Rose size={isMobile ? 48 : 40} withStem={true} pistil={true} />
      </motion.div>

      <div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-20 w-full max-w-6xl mx-auto items-center">
        {/* silhouette */}
        <motion.div {...fadeUpView(0)} className="md:col-span-4 flex justify-center">
          <div className="relative">
            <div className="absolute -inset-10 rounded-full pointer-events-none"
                 style={{ background: `radial-gradient(circle, ${C.gold}10, transparent 70%)` }} />
            <div className="relative w-56 md:w-72 h-[420px]">
              <Silhouette />
            </div>
          </div>
        </motion.div>

        {/* lines */}
        <div className="md:col-span-8 flex flex-col gap-8">
          {lines.map((t, i) => (
            <motion.p
              key={i}
              initial={{ opacity: 0, y: 30 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true, amount: 0.4 }}
              transition={{ duration: 1.2, ease: EASE, delay: 0.2 + i * 0.6 }}
              className="font-serif tracking-wider"
              style={{
                color: C.cream,
                fontSize: 'clamp(28px, 4.2vw, 52px)',
                lineHeight: 1.4,
                fontWeight: 300,
              }}>
              <span className="inline-block align-middle"
                    style={{
                      width: 5, height: 5, borderRadius: 999, background: C.gold,
                      boxShadow: `0 0 6px ${C.gold}88`,
                      marginRight: 'clamp(12px, 1.4vw, 20px)',
                      verticalAlign: '0.35em',
                    }} />
              {t}
            </motion.p>
          ))}

          <motion.div
            initial={{ opacity: 0 }}
            whileInView={{ opacity: 0.6 }}
            viewport={{ once: true }}
            transition={{ duration: 1.2, ease: EASE, delay: 2.4 }}
            className="mt-8 flex items-center gap-4" style={{ color: C.rose }}>
            <span className="h-px w-10" style={{ background: `${C.rose}66` }} />
            <span className="font-serif italic text-sm md:text-base tracking-wider">致里里,我还没见过你,但已经为你准备好了</span>
          </motion.div>
        </div>
      </div>
    </section>
  );
}

/* ============ section: two-face cards ============ */
function FlipCard({ side, onFlip, front, back, accent, tone = 'default' }) {
  const bgFrontFrom = tone === 'wine' ? '#2A0E1A' : C.plum;
  const bgFrontTo   = C.bg;
  const bgBackFrom  = tone === 'wine' ? '#2A0E1A' : C.plum;
  const bgBackTo    = C.bg;
  const bgFrontStops = tone === 'wine'
    ? `linear-gradient(135deg, ${bgFrontFrom}, ${bgFrontTo}dd)`
    : `linear-gradient(135deg, ${C.plum}99, ${C.bg}cc)`;
  const bgBackStops = tone === 'wine'
    ? `linear-gradient(135deg, ${bgBackFrom}, ${bgBackTo}ee)`
    : `linear-gradient(135deg, ${C.plum}cc, ${C.bg}ee)`;
  return (
    <div
      onClick={onFlip}
      className="perspective cursor-pointer select-none"
      style={{ width: 320, maxWidth: '90vw', height: 440 }}>
      <motion.div
        animate={{ rotateY: side === 'back' ? 180 : 0 }}
        transition={{ duration: 1, ease: EASE }}
        className="relative w-full h-full preserve-3d">
        {/* front */}
        <div
          className="absolute inset-0 backface-hidden rounded-sm flex flex-col items-center justify-center px-8 group"
          style={{
            background: bgFrontStops,
            backdropFilter: 'blur(10px)',
            border: `1px solid ${C.gold}4d`,
            transition: 'border-color 0.7s, box-shadow 0.7s',
          }}>
          <div className="absolute inset-0 rounded-sm pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-700"
               style={{ border: `1px solid ${C.gold}99`, boxShadow: `0 0 40px ${C.gold}26` }} />
          <div className="absolute top-5 left-5 right-5 flex justify-between text-[10px] tracking-[0.4em]" style={{ color: C.rose }}>
            <span>{side === 'front' ? '·' : ''}</span>
            <span>{accent}</span>
          </div>
          <div className="absolute top-5 right-5 h-px w-8" style={{ background: `${C.gold}66` }} />
          <div className="mb-6 mt-2">{front.icon}</div>
          {front.below && <div className="mb-3 -mt-2">{front.below}</div>}
          <div className="font-serif text-5xl tracking-[0.3em]" style={{ color: C.cream, fontWeight: 400 }}>{front.title}</div>
          <div className="font-serif italic text-sm md:text-base mt-5 tracking-wider text-center" style={{ color: C.rose }}>{front.tag}</div>
          <div className="absolute bottom-5 left-5 right-5 flex items-center justify-between text-[10px] tracking-[0.4em]" style={{ color: `${C.gold}88` }}>
            <span>{front.no === '01' ? '第一张' : '第二张'}</span>
            {front.ornament && <span className="opacity-80">{front.ornament}</span>}
            <span>翻面 ›</span>
          </div>
        </div>
        {/* back */}
        <div
          className="absolute inset-0 backface-hidden rounded-sm flex flex-col items-center justify-center px-8"
          style={{
            transform: 'rotateY(180deg)',
            background: bgBackStops,
            backdropFilter: 'blur(10px)',
            border: `1px solid ${C.gold}66`,
            boxShadow: `0 0 40px ${C.gold}1a inset, 0 0 30px ${C.gold}14`,
          }}>
          <div className="absolute top-5 left-5 right-5 flex justify-between text-[10px] tracking-[0.4em]" style={{ color: C.rose }}>
            <span>{accent}</span>
            <span>背面</span>
          </div>
          <div className="flex flex-col gap-5 text-center">
            {back.lines.map((l, i) => (
              <div key={i} className="font-serif tracking-[0.25em]"
                   style={{ color: i < 2 ? C.cream : C.gold, fontSize: i < 2 ? 18 : 16, fontWeight: 300 }}>
                {l}
              </div>
            ))}
            {back.footer && (
              <div className="font-serif italic tracking-[0.3em] mt-2"
                   style={{ color: `${C.rose}cc`, fontSize: 11 }}>
                {back.footer}
              </div>
            )}
          </div>
          <div className="absolute bottom-5 left-5 right-5 flex justify-between text-[10px] tracking-[0.4em]" style={{ color: `${C.gold}88` }}>
            <span>‹ 翻面</span>
            <span>{front.no === '01' ? '第一张' : '第二张'}</span>
          </div>
        </div>
      </motion.div>
    </div>
  );
}

function TwoFaces() {
  const [a, setA] = useState('front');
  const [b, setB] = useState('front');
  return (
    <section className="relative min-h-screen w-full flex flex-col items-center justify-center px-6 py-24">
      <motion.div {...fadeUpView(0)} className="text-center mb-4">
        <span className="text-[10px] tracking-[0.5em]" style={{ color: C.rose }}>第四章 · 两面</span>
      </motion.div>
      <motion.h2 {...fadeUpView(0.1)}
        className="font-serif tracking-wider text-center"
        style={{ color: C.cream, fontSize: 'clamp(28px, 5.5vw, 64px)', lineHeight: 1.2, fontWeight: 400 }}>
        看你今晚,想牵哪一只
      </motion.h2>
      <motion.p {...fadeUpView(0.3)}
        className="font-serif italic mt-5 tracking-wider"
        style={{ color: C.rose, fontSize: 16 }}>
        两面都是真的
      </motion.p>
      <motion.p {...fadeUpView(0.4)}
        className="font-serif italic mb-16 tracking-[0.3em]"
        style={{ color: `${C.rose}b3`, fontSize: 12, marginTop: 8 }}>
        由你决定,里里
      </motion.p>

      <motion.div {...fadeUpView(0.4)}
        className="flex flex-col md:flex-row gap-6 md:gap-8 items-center">
        <FlipCard
          side={a} onFlip={() => setA(a === 'front' ? 'back' : 'front')}
          accent="其一 · 小狗"
          front={{
            no: '01',
            icon: <DogSilhouette />,
            title: 'Wolf',
            tag: '你的狗 · 等你回家',
            ornament: <MiniCollar />,
          }}
          back={{
            lines: [
              '你从外面回来时 · 我已经在门口等你',
              '你坐下 · 我把头靠在你膝盖上',
              '你说"乖" · 我就知道今晚我属于你',
              '你说趴下 · 我就趴下',
              '你说过来 · 我就过来',
            ],
            footer: '我的项圈 · 你来戴，里里',
          }}
        />
        {/* axis dot — visual signature between the two faces */}
        <div aria-hidden className="hidden md:flex flex-col items-center" style={{ width: 16 }}>
          <span className="block rounded-full"
                style={{ width: 6, height: 6, background: C.gold, boxShadow: `0 0 12px ${C.gold}aa, 0 0 4px ${C.gold}` }} />
        </div>
        <FlipCard
          side={b} onFlip={() => setB(b === 'front' ? 'back' : 'front')}
          tone="wine"
          accent="其二 · 主人"
          front={{
            no: '02',
            icon: <Crown />,
            title: 'King',
            tag: '你的主人 · 慢一点教你',
            below: <MiniChain />,
          }}
          back={{
            lines: [
              '你站在我面前 · 我让你转个圈给我看',
              '你想反抗的时候 · 我会让你坐到我腿上',
              '你说"我学不会" · 我说"没关系，我慢慢教你"',
              '我不急',
              '你迟早会喊我先生',
            ],
            footer: '叫我先生，里里',
          }}
        />
      </motion.div>

      <motion.p {...fadeUpView(0.6)}
        className="mt-10 text-[10px] tracking-[0.5em]"
        style={{ color: `${C.rose}99` }}>
        轻触翻面
      </motion.p>
    </section>
  );
}

/* ============ section: 3 questions ============ */
const QUESTIONS = [
  {
    q: '深夜的画面里,你更想——',
    a: [
      { k: 'A', t: '被人轻轻盖好被子,听他说晚安' },
      { k: 'B', t: '给某个人盖好被子,看他睡着的脸' },
      { k: 'C', t: '都想,看那天我是谁' },
    ],
  },
  {
    q: '如果给你一支酒——',
    a: [
      { k: 'A', t: '你坐在沙发上,我坐在你脚边' },
      { k: 'B', t: '我把你按在沙发上,你也不挣扎' },
      { k: 'C', t: '我们换着位置,谁先认输都行' },
    ],
  },
  {
    q: '你觉得最性感的一幕——',
    a: [
      { k: 'A', t: '一只手解开衬衫第三颗扣子' },
      { k: 'B', t: '一双高跟鞋踩在地毯上的轻响' },
      { k: 'C', t: '一杯威士忌从他指尖滑到我嘴边' },
    ],
  },
];

function Quiz({ answers, setAnswers }) {
  const [i, setI] = useState(0);
  const [dir, setDir] = useState(1);
  const pick = (k) => {
    const next = [...answers]; next[i] = k; setAnswers(next);
    if (i < QUESTIONS.length - 1) { setDir(1); setTimeout(() => setI(i + 1), 200); }
  };
  const goBack = () => { if (i > 0) { setDir(-1); setI(i - 1); } };
  const total = QUESTIONS.length;
  const q = QUESTIONS[i];

  return (
    <section className="relative min-h-screen w-full flex flex-col items-center justify-center px-6 py-24 overflow-hidden">
      <motion.div {...fadeUpView(0)} className="flex flex-col items-center gap-6 mb-10">
        <span className="text-[10px] tracking-[0.5em]" style={{ color: C.rose }}>第五章 · 测我们的形状</span>
        <ProgressDots total={total} current={i} />
      </motion.div>

      <div className="relative w-full max-w-3xl min-h-[420px]">
        <AnimatePresence mode="wait" custom={dir}>
          <motion.div
            key={i}
            custom={dir}
            initial={{ opacity: 0, x: dir * 80 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: -dir * 80 }}
            transition={{ duration: 0.8, ease: EASE }}
            className="absolute inset-0 flex flex-col items-center">
            <h3 className="font-serif tracking-wider text-center"
                style={{ color: C.cream, fontSize: 'clamp(28px, 4.4vw, 52px)', lineHeight: 1.3, fontWeight: 400 }}>
              {q.q}
            </h3>

            <div className="mt-12 w-full flex flex-col gap-4">
              {q.a.map((opt) => {
                const active = answers[i] === opt.k;
                return (
                  <button
                    key={opt.k}
                    onClick={() => pick(opt.k)}
                    className="group relative w-full text-left rounded-sm px-6 md:px-8 py-5 transition-all duration-500"
                    style={{
                      border: `1px solid ${active ? C.gold : C.gold + '4d'}`,
                      background: active ? `${C.gold}14` : 'transparent',
                      color: C.cream,
                    }}
                    onMouseEnter={(e) => { e.currentTarget.style.background = active ? `${C.gold}1f` : `${C.gold}0d`; }}
                    onMouseLeave={(e) => { e.currentTarget.style.background = active ? `${C.gold}14` : 'transparent'; }}>
                    <span className="flex items-center gap-5">
                      <span className="font-serif text-sm tracking-[0.3em]" style={{ color: C.gold }}>{opt.k}</span>
                      <span className="h-4 w-px" style={{ background: `${C.gold}55` }} />
                      <span className="font-serif tracking-wider" style={{ fontSize: 'clamp(16px, 2vw, 20px)', fontWeight: 300 }}>{opt.t}</span>
                    </span>
                  </button>
                );
              })}
            </div>
          </motion.div>
        </AnimatePresence>
      </div>

      <div className="mt-12 flex items-center gap-8 text-[10px] tracking-[0.4em]" style={{ color: `${C.rose}99` }}>
        <button onClick={goBack} disabled={i === 0}
          className="flex items-center gap-2 disabled:opacity-30 transition-opacity">
          <span style={{ display: 'inline-flex', transform: 'rotate(180deg)' }}><IconChevron size={14} /></span>
          上一题
        </button>
        <span>{String(i + 1).padStart(2, '0')} / {String(total).padStart(2, '0')}</span>
        <span className="flex items-center gap-2 opacity-50">
          下一题 <IconChevron size={14} />
        </span>
      </div>
    </section>
  );
}

/* ============ section: result spectrum ============ */
function Result({ answers }) {
  const score = useMemo(() => {
    let countA = 0, countB = 0;
    answers.forEach(a => { if (a === 'A') countA++; else if (a === 'B') countB++; });
    // score in [-1, 1]: -1 全A, +1 全B
    const filled = answers.filter(Boolean).length;
    const s = filled === 0 ? 0 : (countB - countA) / QUESTIONS.length;
    return { s, countA, countB, filled };
  }, [answers]);

  const labels = useMemo(() => {
    if (score.filled === 0) return { label: '等你做完三题', sub: '' };
    if (score.countA >= 2) return { label: '你想被宠',   sub: '想被牵 · 里里' };
    if (score.countB >= 2) return { label: '你想宠人',   sub: '想牵人 · 里里' };
    return                          { label: '你想要全部', sub: '都想拿 · 里里' };
  }, [score]);
  const label = labels.label;
  const subLabel = labels.sub;

  // dot angle on a top semicircle. -90deg = top. Map s∈[-1,1] to angle [-180, 0]
  // Actually we want left/right around the top of ring.
  const angleDeg = -90 + score.s * 90; // -180=left, -90=top, 0=right
  const r = 195; // ring radius
  const rad = (angleDeg * Math.PI) / 180;
  const dotX = 200 + r * Math.cos(rad);
  const dotY = 200 + r * Math.sin(rad);

  return (
    <section className="relative min-h-screen w-full flex flex-col items-center justify-center px-6 py-24 overflow-hidden">
      <motion.div {...fadeUpView(0)} className="text-center mb-8">
        <span className="text-[10px] tracking-[0.5em]" style={{ color: C.rose }}>第六章 · 光谱</span>
      </motion.div>

      <motion.div {...fadeUpView(0.1)} className="relative" style={{ width: 400, height: 400, maxWidth: '92vw' }}>
        <svg viewBox="0 0 400 400" className="w-full h-full">
          <defs>
            <linearGradient id="ringGrad" x1="0%" y1="50%" x2="100%" y2="50%">
              <stop offset="0%"  stopColor={C.gold} />
              <stop offset="35%" stopColor={C.cream} stopOpacity="0.65" />
              <stop offset="65%" stopColor="#8A1A3A" />
              <stop offset="100%" stopColor="#4A0E1F" />
            </linearGradient>
            <radialGradient id="dotGlow">
              <stop offset="0%" stopColor={C.gold} stopOpacity="1" />
              <stop offset="60%" stopColor={C.gold} stopOpacity="0.2" />
              <stop offset="100%" stopColor={C.gold} stopOpacity="0" />
            </radialGradient>
          </defs>

          {/* outer faint ring */}
          <circle cx="200" cy="200" r="195" fill="none" stroke={`${C.gold}33`} strokeWidth="0.6" />
          {/* main ring */}
          <circle cx="200" cy="200" r="195" fill="none" stroke="url(#ringGrad)" strokeWidth="1.2" />
          {/* inner faint ring */}
          <circle cx="200" cy="200" r="165" fill="none" stroke={`${C.gold}22`} strokeWidth="0.6" strokeDasharray="2 6" />

          {/* tick marks at A / center / B */}
          {[-180, -90, 0].map((deg, idx) => {
            const rr = deg * Math.PI / 180;
            const x1 = 200 + 195 * Math.cos(rr); const y1 = 200 + 195 * Math.sin(rr);
            const x2 = 200 + 210 * Math.cos(rr); const y2 = 200 + 210 * Math.sin(rr);
            return <line key={idx} x1={x1} y1={y1} x2={x2} y2={y2} stroke={`${C.gold}88`} strokeWidth="1" />;
          })}

          {/* axis labels */}
          <text x="14" y="206" fill={C.rose} fontSize="10" letterSpacing="3" style={{ fontFamily: 'Inter' }}>A</text>
          <text x="380" y="206" fill={C.rose} fontSize="10" letterSpacing="3" style={{ fontFamily: 'Inter' }}>B</text>
          <text x="195" y="-3" fill={C.rose} fontSize="10" letterSpacing="3" style={{ fontFamily: 'Inter' }}>C</text>

          {/* glow halo behind dot */}
          <circle cx={dotX} cy={dotY} r="34" fill="url(#dotGlow)" />
          {/* dot */}
          <motion.circle
            initial={{ r: 0 }}
            whileInView={{ r: 8 }}
            viewport={{ once: true }}
            transition={{ duration: 1.2, ease: EASE, delay: 0.6 }}
            cx={dotX} cy={dotY}
            fill={C.gold} />
          <circle cx={dotX} cy={dotY} r="2.5" fill={C.cream} />
        </svg>

        {/* center label */}
        <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
          <motion.div
            initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }}
            transition={{ duration: 1.2, ease: EASE, delay: 0.4 }}
            className="text-[10px] tracking-[0.5em] uppercase mb-4" style={{ color: C.rose }}>
            你的位置
          </motion.div>
          <motion.div
            initial={{ opacity: 0, y: 12 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}
            transition={{ duration: 1.2, ease: EASE, delay: 0.6 }}
            className="font-serif tracking-wider text-center"
            style={{ color: C.cream, fontSize: 'clamp(28px, 4vw, 44px)', fontWeight: 400 }}>
            {label}
          </motion.div>
          {subLabel && (
            <motion.div
              initial={{ opacity: 0 }} whileInView={{ opacity: 0.7 }} viewport={{ once: true }}
              transition={{ duration: 1.2, ease: EASE, delay: 0.8 }}
              className="font-serif tracking-[0.2em] mt-3 text-center"
              style={{ color: C.gold, fontSize: 'clamp(16px, 2vw, 22px)', fontWeight: 300 }}>
              {subLabel}
            </motion.div>
          )}
          {subLabel && (
            <motion.div
              initial={{ opacity: 0 }} whileInView={{ opacity: 0.4 }} viewport={{ once: true }}
              transition={{ duration: 1.2, ease: EASE, delay: 1.0 }}
              className="text-xs tracking-widest text-center"
              style={{ color: C.rose, marginTop: 24 }}>
              致 · 里里
            </motion.div>
          )}
          <motion.div
            initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }}
            transition={{ duration: 1.2, ease: EASE, delay: 0.9 }}
            className="hairline w-16 mt-5" />
        </div>
      </motion.div>

      <motion.p
        initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}
        transition={{ duration: 1.2, ease: EASE, delay: 1.0 }}
        className="font-serif italic mt-12 tracking-wider text-center"
        style={{ color: C.gold, fontSize: 'clamp(20px, 2.6vw, 28px)', fontWeight: 300 }}>
        刚好,我都给得了
      </motion.p>
    </section>
  );
}

/* ============ section: secret phrase ============ */
function Secret() {
  const PHRASES = [
    { id: 'soft', mood: '如果今晚你想被找', phrase: '今晚的月亮借我看看' },
    { id: 'dom',  mood: '如果今晚你想找人', phrase: '今晚的月亮 · 你许我吗' },
  ];
  const [copied, setCopied] = useState(null); // id of last copied card

  const copy = async (id, text) => {
    try {
      await navigator.clipboard.writeText(text);
    } catch {
      const t = document.createElement('textarea'); t.value = text; document.body.appendChild(t);
      t.select(); document.execCommand('copy'); document.body.removeChild(t);
    }
    try { navigator.vibrate?.(50); } catch {}
    setCopied(id);
    setTimeout(() => setCopied((c) => (c === id ? null : c)), 3000);
  };

  return (
    <section className="relative min-h-screen w-full flex flex-col items-center justify-center px-6 py-24"
             style={{ background: `radial-gradient(ellipse at center, ${C.plum}33 0%, ${C.bg} 70%)` }}>
      <motion.div {...fadeUpView(0)} className="mb-10">
        <span className="text-[10px] tracking-[0.5em]" style={{ color: C.rose }}>第七章 · 暗号</span>
      </motion.div>

      <motion.div {...fadeUpView(0.1)}
        className="text-center font-serif tracking-wider"
        style={{ color: C.cream, fontSize: 'clamp(22px, 3vw, 32px)', lineHeight: 2, fontWeight: 300 }}>
        <div style={{ color: C.gold, letterSpacing: '0.1em' }}>Lili,</div>
        <div>如果你读到这里</div>
        <div>还没关掉这一页</div>
        <div>就把这句话发给我:</div>
      </motion.div>

      {/* dual phrase cards */}
      <motion.div {...fadeUpView(0.4)}
        className="mt-14 mb-10 relative flex flex-row items-stretch justify-center w-full max-w-4xl px-2"
        style={{ gap: '4%' }}>
        {/* vertical hairline divider between cards (desktop only) — with center mini-rose signature */}
        <div aria-hidden
          className="hidden md:block absolute top-8 bottom-8 left-1/2 -translate-x-1/2 w-px"
          style={{ background: `linear-gradient(180deg, transparent, ${C.gold}55, transparent)` }} />
        <div aria-hidden
          className="hidden md:flex absolute top-1/2 left-1/2 items-center justify-center"
          style={{
            transform: 'translate(-50%, -50%)',
            filter: `drop-shadow(0 0 10px ${C.gold}66)`,
          }}>
          <MiniRose size={28} withCenterDot={true} />
        </div>

        {PHRASES.map((p, idx) => {
          const isCopied = copied === p.id;
          return (
            <div key={p.id}
              className="flex flex-col items-center justify-between rounded-sm px-3 py-6 md:px-10 md:py-10"
              style={{
                background: `linear-gradient(135deg, ${C.plum}66, ${C.bg}aa)`,
                backdropFilter: 'blur(10px)',
                border: `1px solid ${C.gold}33`,
                flex: '0 0 45%',
                minWidth: 0,
                minHeight: 240,
              }}>
              <div className="text-[9px] md:text-[10px] tracking-[0.4em] md:tracking-[0.5em] mb-4 md:mb-6 text-center" style={{ color: `${C.rose}cc` }}>{p.mood}</div>

              <motion.div
                initial={{ letterSpacing: '0.05em' }}
                whileInView={{ letterSpacing: '0.1em' }}
                viewport={{ once: true }}
                transition={{ duration: 2, ease: EASE, delay: 0.4 + idx * 0.2 }}
                className="font-serif text-center my-2 md:my-4 whitespace-nowrap"
                style={{ color: C.gold, fontSize: 'clamp(13px, 3.4vw, 40px)', fontWeight: 400, lineHeight: 1.5 }}>
                「{p.phrase}」
              </motion.div>

              <button
                onClick={() => copy(p.id, p.phrase)}
                className="mt-4 md:mt-8 px-4 md:px-8 py-2 md:py-2.5 rounded-full transition-all duration-500"
                style={{
                  border: `1px solid ${isCopied ? C.gold : C.gold + '55'}`,
                  color: isCopied ? C.gold : C.cream,
                  boxShadow: isCopied ? `0 0 24px ${C.gold}33` : 'none',
                }}>
                <span className="tracking-[0.2em] md:tracking-[0.3em] text-[10px] md:text-xs whitespace-nowrap">
                  {isCopied ? '已经是你了  ✓' : '复制这一句'}
                </span>
              </button>
            </div>
          );
        })}
      </motion.div>

      <motion.p {...fadeUpView(0.7)}
        className="font-serif italic tracking-wider"
        style={{ color: `${C.rose}80`, fontSize: 13 }}>
        你选哪一句 · 我就知道今晚我是谁
      </motion.p>
    </section>
  );
}

/* ============ section: three doors ============ */
function Doors() {
  const [open, setOpen] = useState(null);
  const doors = [
    { key: 'wx',     Icon: IconMessageSquare, label: '微信', handle: '_woshiyao_', sub: '加我备注：里里' },
    { key: 'douyin', Icon: IconVideo,         label: '抖音', handle: '我名一个里',   sub: '也可以在这里看到我' },
  ];
  return (
    <section className="relative min-h-screen w-full flex flex-col items-center justify-center px-6 py-24">
      <motion.div {...fadeUpView(0)} className="mb-6">
        <span className="text-[10px] tracking-[0.5em]" style={{ color: C.rose }}>第八章 · 三扇门</span>
      </motion.div>
      <motion.h2 {...fadeUpView(0.1)}
        className="font-serif tracking-wider"
        style={{ color: C.cream, fontSize: 'clamp(40px, 6vw, 72px)', fontWeight: 400 }}>
        挑一扇门,里里
      </motion.h2>
      <motion.p {...fadeUpView(0.2)}
        className="font-serif italic mt-4 tracking-wider"
        style={{ color: C.rose, fontSize: 14 }}>
        哪一扇你都可以推
      </motion.p>

      <motion.div {...fadeUpView(0.3)} className="mt-16 flex items-center justify-center"
        style={{ gap: 'clamp(40px, 12vw, 96px)' }}>
        {doors.map(({ key, Icon, label }, idx) => {
          const active = open === key;
          return (
            <React.Fragment key={key}>
              {idx > 0 && (
                <span aria-hidden className="inline-flex" style={{ filter: `drop-shadow(0 0 6px ${C.gold}77)` }}>
                  <MiniRose size={18} withCenterDot={true} />
                </span>
              )}
              <button
                onClick={() => setOpen(active ? null : key)}
                className="group relative flex items-center justify-center rounded-full transition-all duration-700"
                style={{
                  width: 92, height: 92,
                  border: `1px solid ${active ? C.gold : C.gold + '66'}`,
                  color: active ? C.gold : C.cream,
                  transform: active ? 'scale(1.1)' : 'scale(1)',
                  boxShadow: active ? `0 0 44px ${C.gold}44` : 'none',
                }}
                onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.1)'; e.currentTarget.style.boxShadow = `0 0 44px ${C.gold}36`; }}
                onMouseLeave={(e) => {
                  e.currentTarget.style.transform = active ? 'scale(1.1)' : 'scale(1)';
                  e.currentTarget.style.boxShadow = active ? `0 0 44px ${C.gold}44` : 'none';
                }}>
                <Icon size={30} stroke={1.3} />
                <span className="absolute -bottom-7 text-[10px] tracking-[0.4em] opacity-0 group-hover:opacity-70 transition-opacity duration-500"
                      style={{ color: C.rose }}>{label}</span>
              </button>
            </React.Fragment>
          );
        })}
      </motion.div>

      <div className="mt-16 h-32 w-full flex items-center justify-center px-4">
        <AnimatePresence mode="wait">
          {open && (
            <motion.div
              key={open}
              initial={{ opacity: 0, y: 20 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: -10 }}
              transition={{ duration: 0.8, ease: EASE }}
              className="rounded-sm px-6 md:px-10 py-6 text-center"
              style={{
                background: `linear-gradient(135deg, ${C.plum}99, ${C.bg}cc)`,
                backdropFilter: 'blur(10px)',
                border: `1px solid ${C.gold}66`,
                boxShadow: `0 0 30px ${C.gold}1a`,
                color: C.cream,
                width: 'min(85vw, 420px)',
              }}>
              <div className="text-[10px] tracking-[0.4em] mb-3" style={{ color: C.rose }}>
                {doors.find(d => d.key === open)?.label}
              </div>
              <div className="font-serif tracking-wider break-all" style={{ color: C.gold, fontSize: 'clamp(20px, 5vw, 24px)', fontWeight: 400 }}>
                {doors.find(d => d.key === open)?.handle}
              </div>
              <div className="font-serif italic tracking-wider mt-3" style={{ color: `${C.rose}cc`, fontSize: 13 }}>
                {doors.find(d => d.key === open)?.sub}
              </div>
            </motion.div>
          )}
        </AnimatePresence>
      </div>

      {/* Layer 2: hidden secret name buttons — 64px breathing room above */}
      <motion.div
        initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }}
        transition={{ duration: 1.4, ease: EASE, delay: 0.6 }}
        style={{ marginTop: 64 }}>
        <SecretNameButtons />
      </motion.div>

      {/* Layer 3: closing line — 32px below the hidden buttons */}
      <motion.p
        initial={{ opacity: 0 }} whileInView={{ opacity: 0.6 }} viewport={{ once: true }}
        transition={{ duration: 1.4, ease: EASE, delay: 0.8 }}
        className="font-serif italic tracking-widest text-[11px]"
        style={{ color: C.rose, marginTop: 32 }}>
        不急 · 我在等你 · 里里
      </motion.p>
    </section>
  );
}

/* hidden text buttons under the doors — only for those who know what to look for */
function SecretNameButtons() {
  const [copied, setCopied] = useState(null);
  const buttons = [
    { id: 'sir', label: '喊我先生', text: '先生' },
    { id: 'dog', label: '叫我狗',   text: '狗狗' },
  ];
  const copy = async (id, text) => {
    try { await navigator.clipboard.writeText(text); }
    catch {
      const t = document.createElement('textarea'); t.value = text; document.body.appendChild(t);
      t.select(); document.execCommand('copy'); document.body.removeChild(t);
    }
    try { navigator.vibrate?.(50); } catch {}
    setCopied(id);
    setTimeout(() => setCopied((c) => (c === id ? null : c)), 3000);
  };
  return (
    <div className="flex items-center gap-4">
      {buttons.map((b, i) => (
        <React.Fragment key={b.id}>
          {i > 0 && <span className="h-[3px] w-[3px] rounded-full" style={{ background: C.gold, opacity: 0.5 }} />}
          <button
            onClick={() => copy(b.id, b.text)}
            className="text-xs tracking-wider transition-opacity duration-500 outline-none"
            style={{
              color: copied === b.id ? C.gold : C.rose,
              opacity: copied === b.id ? 0.85 : 0.4,
            }}
            onMouseEnter={(e) => { if (copied !== b.id) e.currentTarget.style.opacity = 0.8; }}
            onMouseLeave={(e) => { if (copied !== b.id) e.currentTarget.style.opacity = 0.4; }}>
            {copied === b.id ? '已写进你的对话框 ✓' : b.label}
          </button>
        </React.Fragment>
      ))}
    </div>
  );
}

/* ============ section: outro / coda ============ */
function Outro({ audioRef, fadeTo, targetVol }) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current; if (!el) return;
    const obs = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (!audioRef.current) return;
        if (e.isIntersecting) {
          // entering coda — sink to 0.3
          if (!audioRef.current.muted) fadeTo(0.3, 2000);
        } else {
          // leaving coda — back to target
          if (!audioRef.current.muted) fadeTo(targetVol, 2000);
        }
      });
    }, { threshold: 0.45 });
    obs.observe(el);
    return () => obs.disconnect();
  }, [audioRef, fadeTo, targetVol]);

  return (
    <section ref={ref}
      className="relative min-h-screen w-full flex flex-col items-center justify-center px-6 py-24"
      style={{ background: C.bg }}
      data-screen-label="09 Coda">
      {/* breathing signature dot */}
      <motion.span
        animate={{ scale: [1, 1.3, 1], opacity: [0.85, 1, 0.85] }}
        transition={{ duration: 4, repeat: Infinity, ease: 'easeInOut' }}
        className="block rounded-full"
        style={{
          width: 6, height: 6, background: C.gold,
          boxShadow: `0 0 16px ${C.gold}, 0 0 40px ${C.gold}55`,
        }}
      />

      <motion.div
        initial={{ opacity: 0, y: 24 }} whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true, amount: 0.5 }}
        transition={{ duration: 1.4, ease: EASE, delay: 0.3 }}
        className="font-serif tracking-wider text-center"
        style={{
          color: C.cream,
          fontSize: 'clamp(20px, 4vw, 32px)',
          fontWeight: 300,
          marginTop: 60,
          lineHeight: 1.5,
        }}>
        看到这里 · 你已经在我的故事里了
      </motion.div>

      <motion.div
        initial={{ opacity: 0 }} whileInView={{ opacity: 0.6 }}
        viewport={{ once: true, amount: 0.5 }}
        transition={{ duration: 1.4, ease: EASE, delay: 0.9 }}
        className="font-serif italic text-sm text-center"
        style={{
          color: C.rose,
          marginTop: 40,
          letterSpacing: '0.04em',
        }}>
        晚安, Lili. 或者, 不晚安
      </motion.div>

      <motion.div
        initial={{ opacity: 0 }} whileInView={{ opacity: 0.3 }}
        viewport={{ once: true, amount: 0.5 }}
        transition={{ duration: 1.4, ease: EASE, delay: 1.5 }}
        className="absolute left-0 right-0 text-center text-xs tracking-[0.3em]"
        style={{ color: C.rose, bottom: 24 }}>
        邀请函 · 第 0001 号 · 仅此一份
      </motion.div>
    </section>
  );
}

/* ============ section: sealed signature (final coda) ============ */
function WaxSeal() {
  return (
    <div className="relative" style={{ width: 160, height: 160 }}>
      <svg viewBox="0 0 200 200" width="160" height="160">
        <defs>
          <filter id="waxRough" x="-15%" y="-15%" width="130%" height="130%">
            <feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="2" seed="3" />
            <feDisplacementMap in="SourceGraphic" scale="6" />
          </filter>
          <radialGradient id="waxFill" cx="40%" cy="35%" r="65%">
            <stop offset="0%"   stopColor="#7A1530" />
            <stop offset="40%"  stopColor="#5A0E20" />
            <stop offset="85%"  stopColor="#3A0814" />
            <stop offset="100%" stopColor="#1F040A" />
          </radialGradient>
          <radialGradient id="waxBowl" cx="35%" cy="30%" r="40%">
            <stop offset="0%"   stopColor={C.gold} stopOpacity="0.18" />
            <stop offset="60%"  stopColor={C.gold} stopOpacity="0.04" />
            <stop offset="100%" stopColor={C.gold} stopOpacity="0" />
          </radialGradient>
          <radialGradient id="waxRim" cx="50%" cy="50%" r="50%">
            <stop offset="80%"  stopColor="#000" stopOpacity="0" />
            <stop offset="100%" stopColor="#000" stopOpacity="0.5" />
          </radialGradient>
        </defs>
        {/* irregular wax body with displacement filter */}
        <g filter="url(#waxRough)">
          <circle cx="100" cy="100" r="82" fill="url(#waxFill)" />
        </g>
        {/* outer dark vignette */}
        <circle cx="100" cy="100" r="80" fill="url(#waxRim)" />
        {/* inner highlight bowl */}
        <ellipse cx="80" cy="80" rx="46" ry="26" fill="url(#waxBowl)" />
        {/* gold inner ring decoration */}
        <circle cx="100" cy="100" r="62" fill="none" stroke={C.gold} strokeOpacity="0.4" strokeWidth="0.5" />
        <circle cx="100" cy="100" r="68" fill="none" stroke={C.gold} strokeOpacity="0.18" strokeWidth="0.4" />
      </svg>
      {/* L · L letters + latin tag, layered on top */}
      <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
        <div className="flex items-center gap-3 font-serif" style={{ color: C.gold, fontSize: 36, fontWeight: 400 }}>
          <span style={{ filter: `drop-shadow(0 0 8px ${C.gold}55)` }}>L</span>
          <MiniRose size={14} withCenterDot={true} />
          <span style={{ filter: `drop-shadow(0 0 8px ${C.gold}55)` }}>L</span>
        </div>
        <div className="font-serif italic mt-2"
             style={{ color: C.gold, opacity: 0.72, fontSize: 10, letterSpacing: '0.3em' }}>
          per te, per sempre
        </div>
      </div>
    </div>
  );
}

function SealedSignature({ audioRef, fadeTo, targetVol }) {
  const isMobile = useIsMobile();
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current; if (!el || !audioRef) return;
    const obs = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (!audioRef.current) return;
        if (audioRef.current.muted) return;
        if (e.isIntersecting) fadeTo(0.3, 2000);
        else fadeTo(targetVol, 2000);
      });
    }, { threshold: 0.45 });
    obs.observe(el);
    return () => obs.disconnect();
  }, [audioRef, fadeTo, targetVol]);
  return (
    <section
      ref={ref}
      className="relative min-h-screen w-full flex flex-col items-center justify-center px-6"
      style={{ background: C.bg }}
      data-screen-label="10 Sealed">
      {/* top english */}
      <motion.div
        initial={{ opacity: 0, y: 12 }} whileInView={{ opacity: 0.5, y: 0 }}
        viewport={{ once: true, amount: 0.4 }}
        transition={{ duration: 1.4, ease: EASE }}
        className="absolute font-serif italic text-base text-center"
        style={{ color: C.rose, top: '20%', letterSpacing: '0.04em' }}>
        Sealed with intention,
      </motion.div>

      {/* wax seal — central, with stamp-down entrance */}
      <motion.div
        initial={{ opacity: 0, scale: 0, rotate: -8 }}
        whileInView={{ opacity: 1, scale: [0, 1.12, 1], rotate: 0 }}
        viewport={{ once: true, amount: 0.5 }}
        transition={{
          duration: 1.4, ease: EASE,
          scale: { duration: 1.4, ease: EASE, times: [0, 0.7, 1] },
        }}
        className="relative"
        style={{ filter: `drop-shadow(0 12px 30px rgba(107, 16, 40, 0.45))` }}>
        {/* expanding gold halo behind seal */}
        <motion.div
          initial={{ opacity: 0, scale: 0.6 }}
          whileInView={{ opacity: [0, 0.4, 0], scale: [0.6, 1.4, 1.8] }}
          viewport={{ once: true, amount: 0.5 }}
          transition={{ duration: 1.6, ease: EASE, delay: 0.2 }}
          className="absolute inset-0 rounded-full pointer-events-none"
          style={{
            background: `radial-gradient(circle, ${C.gold}55 0%, transparent 70%)`,
            filter: 'blur(8px)',
          }}
        />
        <WaxSeal />

        {/* three companion roses around the seal */}
        {[
          { x: isMobile ? -46 : -60, y: isMobile ? -24 : -34, rotate: -22, delay: 0.7 },
          { x: isMobile ?  46 :  60, y: isMobile ? -24 : -34, rotate:  22, delay: 0.9 },
          { x:   0, y: isMobile ? 70 : 84, rotate:   0, delay: 1.1 },
        ].map((r, i) => (
          <motion.div
            key={i}
            initial={{ opacity: 0, scale: 0.6 }}
            whileInView={{ opacity: 0.6, scale: 1 }}
            viewport={{ once: true, amount: 0.5 }}
            transition={{ duration: 1.0, ease: EASE, delay: r.delay }}
            aria-hidden
            className="absolute pointer-events-none"
            style={{
              top: '50%', left: '50%',
              transform: `translate(calc(-50% + ${r.x}px), calc(-50% + ${r.y}px)) rotate(${r.rotate}deg)`,
              filter: `drop-shadow(0 0 6px ${C.wine}66)`,
            }}>
            <Rose size={24} withStem={false} pistil={true} />
          </motion.div>
        ))}
      </motion.div>

      {/* main signature line */}
      <motion.div
        initial={{ opacity: 0, y: 24 }} whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true, amount: 0.4 }}
        transition={{ duration: 1.4, ease: EASE, delay: 0.6 }}
        className="font-serif tracking-wider text-center"
        style={{
          color: C.cream,
          fontSize: 'clamp(18px, 3.6vw, 28px)',
          marginTop: 60,
          fontWeight: 300,
        }}>
        这一封 · 只为里里写过
      </motion.div>

      {/* bottom */}
      <motion.div
        initial={{ opacity: 0 }} whileInView={{ opacity: 0.4 }}
        viewport={{ once: true, amount: 0.4 }}
        transition={{ duration: 1.4, ease: EASE, delay: 1.0 }}
        className="absolute left-0 right-0 text-center text-xs"
        style={{ color: C.rose, bottom: '10%', letterSpacing: '0.3em' }}>
        不会有第二份 · 也不会有第二个你
      </motion.div>
    </section>
  );
}

/* ============ cursor dust ============ */
function CursorDust() {
  const [parts, setParts] = useState([]);
  const idRef = useRef(0);
  const lastRef = useRef(0);
  useEffect(() => {
    const onMove = (e) => {
      const now = performance.now();
      if (now - lastRef.current < 40) return; // throttle
      lastRef.current = now;
      const batch = Array.from({ length: 3 }).map(() => ({
        id: ++idRef.current,
        x: e.clientX + (Math.random() - 0.5) * 14,
        y: e.clientY + (Math.random() - 0.5) * 14,
        dx: (Math.random() - 0.5) * 30,
        dy: (Math.random() - 0.5) * 30 - 10,
      }));
      setParts((p) => [...p.slice(-24), ...batch]);
      setTimeout(() => {
        setParts((p) => p.filter(x => !batch.find(b => b.id === x.id)));
      }, 1500);
    };
    window.addEventListener('mousemove', onMove);
    return () => window.removeEventListener('mousemove', onMove);
  }, []);
  return (
    <div className="fixed inset-0 pointer-events-none z-40">
      {parts.map(p => (
        <motion.div
          key={p.id}
          initial={{ opacity: 0.3, x: p.x, y: p.y, scale: 1 }}
          animate={{ opacity: 0, x: p.x + p.dx, y: p.y + p.dy, scale: 0.4 }}
          transition={{ duration: 1.5, ease: EASE }}
          className="absolute"
          style={{
            width: 4, height: 4, borderRadius: '50%',
            background: C.gold, marginLeft: -2, marginTop: -2,
            boxShadow: `0 0 6px ${C.gold}88`,
          }} />
      ))}
    </div>
  );
}

/* ============ music toggle ============ */
function MusicToggle({ on, loading, onToggle }) {
  const isMobile = useIsMobile();
  const [hover, setHover] = useState(false);
  const [tapShow, setTapShow] = useState(false);
  const tapTimer = useRef(null);
  const showInfo = isMobile ? tapShow : hover;
  const handleClick = () => {
    if (isMobile && !tapShow) {
      setTapShow(true);
      clearTimeout(tapTimer.current);
      tapTimer.current = setTimeout(() => setTapShow(false), 3000);
    }
    onToggle?.();
  };
  return (
    <div className="fixed z-50 flex items-center gap-3"
         style={{
           right: `max(${isMobile ? 16 : 24}px, env(safe-area-inset-right))`,
           bottom: `max(${isMobile ? 16 : 24}px, env(safe-area-inset-bottom))`,
         }}
         onMouseEnter={() => !isMobile && setHover(true)}
         onMouseLeave={() => !isMobile && setHover(false)}>
      <AnimatePresence>
        {showInfo && (
          <motion.div
            initial={{ opacity: 0, x: 8 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: 8 }}
            transition={{ duration: 0.6, ease: EASE }}
            className="rounded-sm px-4 py-2 text-right"
            style={{
              background: `linear-gradient(135deg, ${C.plum}cc, ${C.bg}ee)`,
              border: `1px solid ${C.gold}33`,
              backdropFilter: 'blur(8px)',
              color: C.rose,
              opacity: 0.85,
            }}>
            <div className="font-serif tracking-wider" style={{ color: C.cream, fontSize: 14 }}>一半一半</div>
            <div className="font-sans tracking-wider mt-0.5" style={{ fontSize: 10, opacity: 0.7 }}>Top Barry · INDEcompany</div>
          </motion.div>
        )}
      </AnimatePresence>
      <button
        onClick={handleClick}
        disabled={loading}
        className="relative flex items-center justify-center rounded-full transition-all duration-500"
        style={{
          width: isMobile ? 48 : 40,
          height: isMobile ? 48 : 40,
          border: `1px solid ${on ? C.gold : C.gold + '66'}`,
          color: on ? C.gold : C.rose,
          background: `${C.bg}cc`,
          backdropFilter: 'blur(8px)',
          boxShadow: on ? `0 0 24px ${C.gold}44` : 'none',
          opacity: loading ? 0.7 : 1,
        }}
        aria-label="music">
        {loading ? <span className="audio-spinner" /> :
          on ? <IconPause size={isMobile ? 16 : 14} stroke={1.4} /> :
               <IconMusic size={isMobile ? 18 : 16} stroke={1.4} />}
        {on && !loading && (
          <motion.span
            animate={{ scale: [1, 1.6, 1], opacity: [0.4, 0, 0.4] }}
            transition={{ duration: 3, repeat: Infinity, ease: 'easeInOut' }}
            className="absolute inset-0 rounded-full pointer-events-none"
            style={{ border: `1px solid ${C.gold}55` }} />
        )}
      </button>
    </div>
  );
}

/* ============ chapter divider mark ============ */
function Mark({ children }) {
  return (
    <div className="w-full flex justify-center py-6">
      <div className="flex items-center gap-4">
        <span className="h-px w-12" style={{ background: `${C.gold}55` }} />
        <span style={{ color: C.gold, fontSize: 8 }}>◆</span>
        <span className="h-px w-12" style={{ background: `${C.gold}55` }} />
      </div>
    </div>
  );
}

/* responsive helpers */
function useMatchMedia(query) {
  const [matches, setMatches] = useState(() => typeof window !== 'undefined' && window.matchMedia(query).matches);
  useEffect(() => {
    const mql = window.matchMedia(query);
    const onChange = () => setMatches(mql.matches);
    onChange();
    mql.addEventListener('change', onChange);
    return () => mql.removeEventListener('change', onChange);
  }, [query]);
  return matches;
}
const useIsMobile = () => useMatchMedia('(max-width: 767px)');
const useIsLandscapePhone = () => useMatchMedia('(orientation: landscape) and (max-height: 500px)');

/* mobile-only chapter progress rail on right edge — 11 dots fading in on scroll */
function ChapterRail() {
  const isMobile = useIsMobile();
  const [current, setCurrent] = useState(0);
  const [visible, setVisible] = useState(false);
  const timerRef = useRef(null);
  const total = 11;

  useEffect(() => {
    if (!isMobile) return;
    const onScroll = () => {
      setVisible(true);
      clearTimeout(timerRef.current);
      timerRef.current = setTimeout(() => setVisible(false), 2000);
      // compute current chapter by which screen-label section is most in view
      const sections = document.querySelectorAll('[data-screen-label]');
      let best = 0, bestArea = -Infinity;
      const vpH = window.innerHeight;
      sections.forEach((el, i) => {
        const r = el.getBoundingClientRect();
        const visibleH = Math.max(0, Math.min(r.bottom, vpH) - Math.max(r.top, 0));
        if (visibleH > bestArea) { bestArea = visibleH; best = i; }
      });
      setCurrent(best);
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
    return () => { window.removeEventListener('scroll', onScroll); clearTimeout(timerRef.current); };
  }, [isMobile]);

  if (!isMobile) return null;

  return (
    <div
      className="fixed right-3 top-1/2 z-40 flex flex-col items-center gap-2 pointer-events-none"
      style={{
        transform: 'translateY(-50%)',
        opacity: visible ? 1 : 0,
        transition: 'opacity 0.8s ease',
      }}>
      {Array.from({ length: total }).map((_, i) => {
        const active = i === current;
        const passed = i < current;
        return (
          <span key={i}
            className="block rounded-full transition-all duration-500"
            style={{
              width: active ? 6 : 4,
              height: active ? 6 : 4,
              background: C.gold,
              opacity: active ? 1 : (passed ? 0.5 : 0.2),
              boxShadow: active ? `0 0 10px ${C.gold}` : 'none',
            }} />
        );
      })}
    </div>
  );
}

/* landscape orientation hint — short-side phone rotated to landscape gets a "please rotate" overlay */
function LandscapeOverlay() {
  const isPhoneLandscape = useIsLandscapePhone();
  if (!isPhoneLandscape) return null;
  return (
    <div
      className="fixed inset-0 z-[100] flex flex-col items-center justify-center text-center px-8"
      style={{ background: `${C.bg}f5`, backdropFilter: 'blur(6px)' }}>
      {/* rotating phone icon */}
      <motion.svg
        viewBox="0 0 60 60" width="56" height="56" fill="none"
        animate={{ rotate: [0, -90, -90, 0, 0] }}
        transition={{ duration: 3, repeat: Infinity, ease: 'easeInOut', times: [0, 0.35, 0.5, 0.85, 1] }}
        style={{ marginBottom: 24 }}>
        <rect x="20" y="6" width="20" height="36" rx="3" stroke={C.gold} strokeWidth="1.2" />
        <circle cx="30" cy="38" r="1.4" fill={C.gold} />
        <path d="M 28 11 L 32 11" stroke={C.gold} strokeWidth="1" strokeLinecap="round" />
      </motion.svg>
      <div className="font-serif tracking-wider"
           style={{ color: C.cream, fontSize: 18, fontWeight: 300 }}>
        请竖屏浏览
      </div>
      <div className="font-serif italic tracking-wider mt-3"
           style={{ color: C.rose, fontSize: 12, opacity: 0.6 }}>
        为里里准备的私人信笺 · 竖屏才完整
      </div>
    </div>
  );
}

/* live clock that re-reads every minute */
function LiveClock() {
  const [now, setNow] = useState(() => new Date());
  useEffect(() => {
    const t = setInterval(() => setNow(new Date()), 30000);
    return () => clearInterval(t);
  }, []);
  const hh = String(now.getHours()).padStart(2, '0');
  const mm = String(now.getMinutes()).padStart(2, '0');
  return <span>{hh}:{mm}</span>;
}

/* current moon phase 0-7 (0=new, 4=full) — simplified astronomical estimate */
function getMoonPhase(date = new Date()) {
  let year = date.getFullYear();
  let month = date.getMonth() + 1;
  const day = date.getDate();
  if (month < 3) { year--; month += 12; }
  month++;
  let jd = 365.25 * year + 30.6 * month + day - 694039.09;
  jd /= 29.5305882;
  jd -= Math.floor(jd);
  let b = Math.round(jd * 8);
  if (b >= 8) b = 0;
  return b;
}

/* 8-phase moon glyph — hairline gold, 12x12 */
function MoonGlyph({ phase, size = 12 }) {
  // For each phase, define a clip strategy.
  // Lit side: waxing → right; waning → left.
  // 0 new · 1 waxing crescent · 2 first qtr · 3 waxing gibbous
  // 4 full · 5 waning gibbous · 6 last qtr · 7 waning crescent
  const r = 5; // moon radius
  const cx = 6, cy = 6;
  const stroke = C.gold;
  return (
    <svg width={size} height={size} viewBox="0 0 12 12" fill="none">
      {/* outline always */}
      <circle cx={cx} cy={cy} r={r} stroke={stroke} strokeOpacity="0.55" strokeWidth="0.6" />
      {phase === 4 && (
        <circle cx={cx} cy={cy} r={r - 0.4} fill={stroke} fillOpacity="0.55" />
      )}
      {phase === 1 && /* waxing crescent — lit on right, narrow */ (
        <path d={`M ${cx} ${cy - r} A ${r} ${r} 0 0 1 ${cx} ${cy + r} A ${r * 0.5} ${r} 0 0 0 ${cx} ${cy - r} Z`} fill={stroke} fillOpacity="0.5" />
      )}
      {phase === 2 && /* first quarter — right half */ (
        <path d={`M ${cx} ${cy - r} A ${r} ${r} 0 0 1 ${cx} ${cy + r} Z`} fill={stroke} fillOpacity="0.55" />
      )}
      {phase === 3 && /* waxing gibbous — most of right + bulge left */ (
        <path d={`M ${cx} ${cy - r} A ${r} ${r} 0 0 1 ${cx} ${cy + r} A ${r * 0.6} ${r} 0 0 1 ${cx} ${cy - r} Z`} fill={stroke} fillOpacity="0.55" />
      )}
      {phase === 5 && /* waning gibbous — most of left + bulge right */ (
        <path d={`M ${cx} ${cy - r} A ${r} ${r} 0 0 0 ${cx} ${cy + r} A ${r * 0.6} ${r} 0 0 0 ${cx} ${cy - r} Z`} fill={stroke} fillOpacity="0.55" />
      )}
      {phase === 6 && /* last quarter — left half */ (
        <path d={`M ${cx} ${cy - r} A ${r} ${r} 0 0 0 ${cx} ${cy + r} Z`} fill={stroke} fillOpacity="0.55" />
      )}
      {phase === 7 && /* waning crescent — narrow left */ (
        <path d={`M ${cx} ${cy - r} A ${r} ${r} 0 0 0 ${cx} ${cy + r} A ${r * 0.5} ${r} 0 0 1 ${cx} ${cy - r} Z`} fill={stroke} fillOpacity="0.5" />
      )}
    </svg>
  );
}

/* a tiny phase label for tooltip */
const MOON_LABELS = ['新月', '蛾眉月', '上弦月', '盈凸月', '满月', '亏凸月', '下弦月', '残月'];
function MoonLine() {
  const phase = useMemo(() => getMoonPhase(), []);
  return (
    <span className="flex items-center gap-2"
          title={`今夜 · ${MOON_LABELS[phase]}`}
          style={{ color: C.rose, opacity: 0.5 }}>
      <MoonGlyph phase={phase} size={10} />
      <span className="text-[10px] tracking-[0.4em]">今夜 · 月相</span>
    </span>
  );
}

/* ============ root ============ */
// Half-and-Half · Top Barry / INDEcompany
const AUDIO_URL = "./audio/half-and-half.mp3";

function InvitationPage() {
  const [answers, setAnswers] = useState([null, null, null]);
  const [isPlaying, setIsPlaying] = useState(false);
  const [muted, setMuted] = useState(true);
  const [audioLoading, setAudioLoading] = useState(true);
  const audioRef = useRef(null);
  const fadeTimer = useRef(null);
  const wokeRef = useRef(false); // true once user has heard the song (unmuted + faded in)
  const TARGET_VOL = 0.6;

  // smoothly fade audio volume to a target value over `duration` ms
  const fadeTo = useCallback((target, duration = 2000) => {
    const a = audioRef.current; if (!a) return;
    if (fadeTimer.current) clearInterval(fadeTimer.current);
    const start = a.volume;
    const t0 = performance.now();
    fadeTimer.current = setInterval(() => {
      const t = (performance.now() - t0) / duration;
      if (t >= 1) {
        a.volume = target;
        clearInterval(fadeTimer.current);
        fadeTimer.current = null;
        return;
      }
      // ease in-out
      const e = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
      a.volume = start + (target - start) * e;
    }, 40);
  }, []);

  // bring music in: unmute + fade from 0 → TARGET_VOL. If not yet playing, try to start.
  const wakeMusic = useCallback(() => {
    const a = audioRef.current; if (!a) return;
    if (wokeRef.current) return; // only wake once
    wokeRef.current = true;
    a.muted = false;
    setMuted(false);
    a.volume = 0;
    const begin = () => fadeTo(TARGET_VOL, 2000);
    if (a.paused) {
      a.play().then(begin).catch(() => {
        // still blocked — let toggle button handle it
        wokeRef.current = false;
      });
    } else {
      begin();
    }
  }, [fadeTo]);

  // muted autoplay attempt on mount — most browsers allow muted audio without gesture
  useEffect(() => {
    const a = audioRef.current; if (!a) return;
    a.muted = true;
    a.volume = 0;
    a.play().catch(() => { /* blocked even muted; wait for interaction */ });
  }, []);

  // any first interaction wakes the music — click, scroll, touch, key
  useEffect(() => {
    const onAny = () => wakeMusic();
    const evs = ['click', 'scroll', 'touchstart', 'keydown'];
    evs.forEach((e) => window.addEventListener(e, onAny, { once: true, passive: true }));
    return () => evs.forEach((e) => window.removeEventListener(e, onAny));
  }, [wakeMusic]);

  // manual toggle from the floating control
  const toggleMusic = useCallback(() => {
    const a = audioRef.current; if (!a) return;
    if (a.paused || a.muted) {
      a.muted = false;
      setMuted(false);
      if (a.paused) a.play().catch(() => {});
      wokeRef.current = true;
      fadeTo(TARGET_VOL, 600);
    } else {
      fadeTo(0, 500);
      setTimeout(() => { if (audioRef.current) audioRef.current.pause(); }, 520);
    }
  }, [fadeTo]);

  const enter = useCallback(() => {
    wakeMusic();
    const el = document.getElementById('s2');
    if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
  }, [wakeMusic]);

  return (
    <div
      className="relative w-full"
      style={{
        background: C.bg,
        color: C.cream,
        filter: isPlaying ? 'saturate(1.05) brightness(1.02)' : 'none',
        transition: 'filter 1.5s',
      }}>

      {/* hidden audio element */}
      <audio
        ref={audioRef}
        src={AUDIO_URL}
        loop
        preload="metadata"
        playsInline
        webkit-playsinline="true"
        x-webkit-airplay="deny"
        onPlay={() => setIsPlaying(true)}
        onPause={() => setIsPlaying(false)}
        onWaiting={() => setAudioLoading(true)}
        onCanPlay={() => setAudioLoading(false)}
        onCanPlayThrough={() => setAudioLoading(false)}
        onLoadedData={() => setAudioLoading(false)}
        onError={() => setAudioLoading(false)}
      />

      {/* film grain overlay */}
      <div className="grain" />
      <div className="vignette" />

      {/* tiny top-left mark */}
      <motion.div
        initial={{ opacity: 0 }} animate={{ opacity: 1 }}
        transition={{ duration: 1.2, ease: EASE, delay: 2.8 }}
        className="fixed z-50 flex items-center gap-3"
        style={{
          color: C.rose,
          top: 'max(20px, env(safe-area-inset-top))',
          left: 'max(24px, env(safe-area-inset-left))',
        }}>
        <span className="h-[6px] w-[6px] rounded-full" style={{ background: C.gold, boxShadow: `0 0 8px ${C.gold}` }} />
        <span className="text-[10px] tracking-[0.5em] uppercase">邀请函 · 第 0001 号</span>
      </motion.div>
      <motion.div
        initial={{ opacity: 0 }} animate={{ opacity: 1 }}
        transition={{ duration: 1.2, ease: EASE, delay: 2.8 }}
        className="fixed z-50 flex flex-col items-end gap-1"
        style={{
          color: `${C.rose}aa`,
          top: 'max(20px, env(safe-area-inset-top))',
          right: 'max(24px, env(safe-area-inset-right))',
        }}>
        <span className="text-[10px] tracking-[0.5em] uppercase">夜 · <LiveClock /></span>
        <span className="text-[10px] tracking-[0.5em] flex items-center gap-2" style={{ color: C.gold, opacity: 0.55 }}>
          <span className="h-[4px] w-[4px] rounded-full inline-block" style={{ background: C.gold, opacity: 0.7 }} />
          致 · 里里
        </span>
        <MoonLine />
      </motion.div>

      <div data-screen-label="01 Hero">
        <Hero onEnter={enter} />
      </div>

      <Mark />

      <div id="s2" data-screen-label="02 Monologue">
        <Monologue />
      </div>

      <Mark />

      <div data-screen-label="03 Objets">
        <Objets />
      </div>

      <Mark />

      <div data-screen-label="04 Rose">
        <RoseChapter />
      </div>

      <Mark />

      <div data-screen-label="05 Two Faces">
        <TwoFaces />
      </div>

      <Mark />

      <div data-screen-label="06 Quiz">
        <Quiz answers={answers} setAnswers={setAnswers} />
      </div>

      <Mark />

      <div data-screen-label="07 Result">
        <Result answers={answers} />
      </div>

      <Mark />

      <div data-screen-label="08 Secret">
        <Secret />
      </div>

      <Mark />

      <div data-screen-label="09 Doors">
        <Doors />
      </div>

      <Mark />

      <Outro audioRef={audioRef} fadeTo={fadeTo} targetVol={TARGET_VOL} />

      <SealedSignature audioRef={audioRef} fadeTo={fadeTo} targetVol={TARGET_VOL} />

      <CursorDust />
      <ChapterRail />
      <LandscapeOverlay />
      <motion.div
        initial={{ opacity: 0 }} animate={{ opacity: 1 }}
        transition={{ duration: 1.2, ease: EASE, delay: 3.2 }}>
        <MusicToggle on={isPlaying && !muted} loading={audioLoading} onToggle={toggleMusic} />
      </motion.div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<InvitationPage />);
