// ============================================================
// Dhobify landing v2 — shared lib
// Tokens, icons, atoms, hooks. Exported on window for sibling
// <script type="text/babel"> files (Babel-standalone gives each
// script its own scope, so we re-publish symbols globally).
// ============================================================

// --- Brand tokens ---------------------------------------------------
const DHB_T = {
  blue:        '#3D5AFE',
  blueDeep:    '#2C3FB8',
  blueWash:    '#EEF2FF',
  yellow:      '#FFD43B',
  green:       '#10B981',
  greenSoft:   '#D1FAE5',
  red:         '#EF4444',
  amber:       '#F59E0B',
  amberSoft:   '#FEF3C7',
  violet:      '#7C3AED',
  ink:         '#0F172A',
  slate:       '#64748B',
  slateLight:  '#94A3B8',
  surface:     '#FFFFFF',
  bg:          '#F8FAFC',
  border:      '#E2E8F0',
  borderSoft:  '#F1F5F9',
};

const DHB_MAX  = 1200;
const DHB_PADD = 80;
const DHB_PADM = 24;

const DHB_STORE = {
  resident: {
    apple:  'https://apps.apple.com/in/app/dhobify-doorstep-laundry/id6767681605',
    google: 'https://play.google.com/store/apps/details?id=app.dhobify.resident',
  },
  vendor: {
    apple:  'https://apps.apple.com/in/app/dhobify-vendor/id6767529565',
    google: 'https://play.google.com/store/apps/details?id=app.dhobify.vendor',
  },
};

// --- Analytics ------------------------------------------------------
// Privacy-friendly stub. Replace with Plausible/Umami in production.
function dhbTrack(event, props) {
  try {
    if (window.plausible) window.plausible(event, { props });
    if (window.umami)     window.umami.track(event, props);
    if (window.__DHB_DEBUG_EVENTS__) console.log('[dhb]', event, props);
  } catch (e) {}
}

// --- Hooks ----------------------------------------------------------
function useReducedMotion() {
  const [rm, setRm] = React.useState(() => {
    if (typeof window === 'undefined') return false;
    return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  });
  React.useEffect(() => {
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
    const fn = () => setRm(mq.matches);
    mq.addEventListener ? mq.addEventListener('change', fn) : mq.addListener(fn);
    return () => { mq.removeEventListener ? mq.removeEventListener('change', fn) : mq.removeListener(fn); };
  }, []);
  return rm;
}

function useMobile(bp = 767) {
  const [m, setM] = React.useState(false);
  React.useEffect(() => {
    const mq = window.matchMedia(`(max-width: ${bp}px)`);
    const fn = () => setM(mq.matches);
    fn();
    mq.addEventListener ? mq.addEventListener('change', fn) : mq.addListener(fn);
    return () => { mq.removeEventListener ? mq.removeEventListener('change', fn) : mq.removeListener(fn); };
  }, [bp]);
  return m;
}

function useScrollY() {
  const [y, setY] = React.useState(0);
  React.useEffect(() => {
    let raf = 0;
    const onScroll = () => {
      if (raf) return;
      raf = requestAnimationFrame(() => {
        setY(window.scrollY);
        raf = 0;
      });
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
    return () => { window.removeEventListener('scroll', onScroll); if (raf) cancelAnimationFrame(raf); };
  }, []);
  return y;
}

// IntersectionObserver-based reveal. Returns [ref, visible].
function useReveal(opts = {}) {
  const ref = React.useRef(null);
  const [visible, setVisible] = React.useState(false);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (typeof IntersectionObserver === 'undefined') { setVisible(true); return; }
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting) {
          setVisible(true);
          io.unobserve(el);
        }
      });
    }, { rootMargin: '0px 0px -10% 0px', threshold: 0.12, ...opts });
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return [ref, visible];
}

// Continuous visibility observer (unlike useReveal it keeps observing, so
// loops can pause when offscreen). Returns [ref, inView].
function useInView(opts = {}) {
  const ref = React.useRef(null);
  const [inView, setInView] = React.useState(false);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (typeof IntersectionObserver === 'undefined') { setInView(true); return; }
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => setInView(e.isIntersecting));
    }, { threshold: 0.15, ...opts });
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return [ref, inView];
}

// Pointer-follow 3D tilt. Returns { ref, onMouseMove, onMouseLeave } to spread
// on a wrapper; the wrapped child rotates toward the cursor. Desktop only —
// callers gate on !mobile && !reduced.
function useTilt({ maxX = 4, maxY = 7 } = {}) {
  const ref = React.useRef(null);
  const onMouseMove = React.useCallback((e) => {
    const el = ref.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    const px = (e.clientX - r.left) / r.width - 0.5;   // -0.5 .. 0.5
    const py = (e.clientY - r.top) / r.height - 0.5;
    el.style.transform =
      `perspective(1100px) rotateY(${(px * maxY * 2).toFixed(2)}deg) rotateX(${(-py * maxX * 2).toFixed(2)}deg)`;
  }, [maxX, maxY]);
  const onMouseLeave = React.useCallback(() => {
    const el = ref.current;
    if (el) el.style.transform = 'perspective(1100px) rotateY(0deg) rotateX(0deg)';
  }, []);
  return { ref, onMouseMove, onMouseLeave };
}

// Count-up that triggers on viewport entry. Target can be number; suffix is appended.
function useCountUp(target, { duration = 1200, decimals = 0 } = {}) {
  const [ref, visible] = useReveal();
  const reduced = useReducedMotion();
  const [val, setVal] = React.useState(0);
  React.useEffect(() => {
    if (!visible) return;
    if (reduced) { setVal(target); return; }
    const start = performance.now();
    let raf = 0;
    const tick = (now) => {
      const t = Math.min(1, (now - start) / duration);
      const eased = 1 - Math.pow(1 - t, 3);
      setVal(target * eased);
      if (t < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [visible, target, duration, reduced]);
  const fmt = decimals > 0 ? val.toFixed(decimals) : Math.floor(val).toLocaleString('en-IN');
  return [ref, fmt, visible];
}

// Count-up rendered as a drop-in <span>. Inherits the parent's font styling
// so it can sit inside any existing number treatment.
function DhbCountText({ value, prefix = '', suffix = '', decimals = 0, duration = 1100 }) {
  const [ref, fmt] = useCountUp(value, { duration, decimals });
  return <span ref={ref} style={{ fontVariantNumeric: 'tabular-nums' }}>{prefix}{fmt}{suffix}</span>;
}

// Odometer: each digit is a column of 0-9 that rolls into place on viewport
// entry, with a slight per-digit stagger. Non-digit chars (commas, %, ★, .)
// render static. `text` is the final formatted string.
function DhbOdometer({ text, duration = 900 }) {
  const [ref, visible] = useReveal();
  const reduced = useReducedMotion();
  const chars = String(text).split('');
  let digitIdx = -1;
  return (
    <span ref={ref} aria-label={String(text)} style={{ display: 'inline-flex', fontVariantNumeric: 'tabular-nums' }}>
      {chars.map((c, i) => {
        if (!/[0-9]/.test(c)) {
          return <span key={i} aria-hidden>{c}</span>;
        }
        digitIdx += 1;
        const d = parseInt(c, 10);
        const on = visible || reduced;
        return (
          <span key={i} aria-hidden style={{
            display: 'inline-block', overflow: 'hidden',
            height: '1.12em', lineHeight: '1.12em',
          }}>
            <span style={{
              display: 'inline-flex', flexDirection: 'column',
              transform: on ? `translateY(-${d * 1.12}em)` : 'translateY(0)',
              transition: reduced ? 'none' : `transform ${duration}ms cubic-bezier(.16,1,.3,1) ${digitIdx * 90}ms`,
            }}>
              {[0,1,2,3,4,5,6,7,8,9].map((n) => (
                <span key={n} style={{ height: '1.12em', lineHeight: '1.12em' }}>{n}</span>
              ))}
            </span>
          </span>
        );
      })}
    </span>
  );
}

// 2px reading-progress bar pinned above the sticky header.
function DhbScrollProgress() {
  const ref = React.useRef(null);
  React.useEffect(() => {
    let raf = 0;
    const onScroll = () => {
      if (raf) return;
      raf = requestAnimationFrame(() => {
        const h = document.documentElement;
        const max = h.scrollHeight - h.clientHeight;
        const p = max > 0 ? h.scrollTop / max : 0;
        if (ref.current) ref.current.style.transform = `scaleX(${p})`;
        raf = 0;
      });
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
    return () => { window.removeEventListener('scroll', onScroll); if (raf) cancelAnimationFrame(raf); };
  }, []);
  return (
    <div aria-hidden style={{
      position: 'fixed', top: 0, left: 0, right: 0, height: 2.5, zIndex: 95,
      pointerEvents: 'none',
    }}>
      <div ref={ref} style={{
        width: '100%', height: '100%',
        background: `linear-gradient(90deg, ${DHB_T.blue}, ${DHB_T.violet})`,
        transform: 'scaleX(0)', transformOrigin: 'left',
      }} />
    </div>
  );
}

// Scene-setting: the body background takes a barely-there tint per section.
// Sections opt in via data-dhb-zone="<color>"; sections with their own solid
// background simply cover it.
function DhbColorZones() {
  React.useEffect(() => {
    if (typeof IntersectionObserver === 'undefined') return;
    const base = DHB_T.bg;
    document.body.style.transition = 'background 1s ease';
    const els = Array.from(document.querySelectorAll('[data-dhb-zone]'));
    if (!els.length) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting) document.body.style.background = e.target.getAttribute('data-dhb-zone') || base;
      });
    }, { rootMargin: '-40% 0px -40% 0px' });
    els.forEach((el) => io.observe(el));
    return () => io.disconnect();
  }, []);
  return null;
}

// QR for https://dhobify.app/get (UA-redirect page) — pre-generated, inline,
// no runtime dependency. Shown to desktop visitors who can't tap a store badge.
function DhbQR({ size = 116 }) {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 25" width={size} height={size}
      shapeRendering="crispEdges" role="img" aria-label="QR code — scan to get the Dhobify app">
      <path fill="#ffffff" d="M0 0h25v25H0z"/>
      <path stroke="#0F172A" d="M0 0.5h7m7 0h1m1 0h1m1 0h7M0 1.5h1m5 0h1m8 0h2m1 0h1m5 0h1M0 2.5h1m1 0h3m1 0h1m1 0h1m3 0h1m2 0h1m2 0h1m1 0h3m1 0h1M0 3.5h1m1 0h3m1 0h1m1 0h2m1 0h5m2 0h1m1 0h3m1 0h1M0 4.5h1m1 0h3m1 0h1m1 0h1m5 0h1m1 0h1m1 0h1m1 0h3m1 0h1M0 5.5h1m5 0h1m1 0h1m6 0h1m2 0h1m5 0h1M0 6.5h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7M8 7.5h1m1 0h1m1 0h1m3 0h1M0 8.5h1m1 0h5m3 0h1m2 0h1m4 0h5M0 9.5h2m2 0h2m1 0h2m4 0h1m5 0h1m3 0h1M0 10.5h1m2 0h14m4 0h1m1 0h2M1 11.5h1m1 0h2m2 0h4m3 0h2m2 0h2m4 0h1M4 12.5h1m1 0h1m2 0h6m1 0h3m1 0h1m1 0h3M0 13.5h1m3 0h1m3 0h1m1 0h1m5 0h1m2 0h1m1 0h1m1 0h1M0 14.5h1m4 0h5m1 0h5m1 0h1m1 0h3m1 0h2M0 15.5h1m2 0h1m4 0h1m2 0h1m2 0h1m1 0h2m1 0h2m3 0h1M0 16.5h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h2m1 0h8m1 0h1M8 17.5h3m1 0h1m2 0h2m3 0h2M0 18.5h7m2 0h1m4 0h1m1 0h1m1 0h1m1 0h1m1 0h3M0 19.5h1m5 0h1m1 0h2m2 0h1m1 0h1m1 0h1m3 0h2m1 0h1M0 20.5h1m1 0h3m1 0h1m1 0h2m2 0h9m1 0h3M0 21.5h1m1 0h3m1 0h1m1 0h1m1 0h1m3 0h1m2 0h2m1 0h5M0 22.5h1m1 0h3m1 0h1m1 0h1m2 0h2m1 0h3m4 0h2m1 0h1M0 23.5h1m5 0h1m2 0h1m1 0h1m3 0h2m1 0h4m2 0h1M0 24.5h7m1 0h1m2 0h2m6 0h6"/>
    </svg>
  );
}

// --- Icons (Lucide-style, 2px stroke, 24px default) -----------------
function DhbIc({ children, size = 24, stroke = DHB_T.ink, weight = 2, fill = 'none', style }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill={fill} stroke={stroke}
      strokeWidth={weight} strokeLinecap="round" strokeLinejoin="round" style={style} aria-hidden="true">
      {children}
    </svg>
  );
}
const IcCheck    = (p) => <DhbIc {...p}><path d="M4 12l5 5L20 6" /></DhbIc>;
const IcCamera   = (p) => <DhbIc {...p}><path d="M3 8a2 2 0 012-2h2l1.5-2h7L17 6h2a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V8z" /><circle cx="12" cy="13" r="4" /></DhbIc>;
const IcClock    = (p) => <DhbIc {...p}><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></DhbIc>;
const IcLoc      = (p) => <DhbIc {...p}><path d="M12 21s7-6.5 7-12a7 7 0 10-14 0c0 5.5 7 12 7 12z" /><circle cx="12" cy="9" r="2.5" /></DhbIc>;
const IcRupee    = (p) => <DhbIc {...p}><path d="M6 4h12" /><path d="M6 9h12" /><path d="M14 4a5 5 0 010 10H6l8 6" /></DhbIc>;
const IcStar     = (p) => <DhbIc {...p} fill={p.fill || DHB_T.amber} stroke={p.stroke || DHB_T.amber}><path d="M12 3l2.7 5.6 6.1.9-4.4 4.3 1 6.1L12 17l-5.5 2.9 1-6.1L3 9.5l6.1-.9L12 3z" /></DhbIc>;
const IcArrow    = (p) => <DhbIc {...p}><path d="M5 12h14" /><path d="M13 5l7 7-7 7" /></DhbIc>;
const IcChev     = (p) => <DhbIc {...p}><path d="M6 9l6 6 6-6" /></DhbIc>;
const IcMenu     = (p) => <DhbIc {...p}><path d="M3 6h18" /><path d="M3 12h18" /><path d="M3 18h18" /></DhbIc>;
const IcX        = (p) => <DhbIc {...p}><path d="M6 6l12 12" /><path d="M18 6L6 18" /></DhbIc>;
const IcShield   = (p) => <DhbIc {...p}><path d="M12 3l8 3v6c0 5-3.5 8.5-8 9-4.5-.5-8-4-8-9V6l8-3z" /><path d="M9 12l2 2 4-4" /></DhbIc>;
const IcPhone    = (p) => <DhbIc {...p}><path d="M5 4h3l2 5-2.5 1.5a11 11 0 005 5L14 13l5 2v3a2 2 0 01-2 2A14 14 0 013 6a2 2 0 012-2z" /></DhbIc>;
const IcZap      = (p) => <DhbIc {...p}><path d="M13 3L4 14h7l-1 7 9-11h-7l1-7z" /></DhbIc>;
const IcShirt    = (p) => <DhbIc {...p}><path d="M6 4l3-1 3 2 3-2 3 1 2 4-3 1v11H7V9L4 8l2-4z" /></DhbIc>;
const IcSparkle  = (p) => <DhbIc {...p}><path d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z" /><path d="M19 14l.7 2.1L22 17l-2.3.9L19 20l-.7-2.1L16 17l2.3-.9L19 14z" /></DhbIc>;
const IcInsta    = (p) => <DhbIc {...p}><rect x="3" y="3" width="18" height="18" rx="5" /><circle cx="12" cy="12" r="4" /><circle cx="17.5" cy="6.5" r="0.8" fill={p.stroke || DHB_T.ink} /></DhbIc>;
const IcWA       = (p) => <DhbIc {...p}><path d="M3 21l1.5-5A9 9 0 113 12c0 1.6.4 3.1 1.2 4.4L3 21z" /><path d="M8.5 9c0 4 3.5 7.5 7.5 7.5l-.5-2-2 .5-1.5-1.5.5-2-2-.5L10 9H8.5z" fill={p.stroke || DHB_T.ink} /></DhbIc>;
const IcX_logo   = (p) => <DhbIc {...p}><path d="M4 4l16 16" /><path d="M20 4L4 20" /></DhbIc>;
const IcApple    = ({ size = 18, fill = '#fff' }) => (
  <svg viewBox="0 0 24 24" width={size} height={size} fill={fill} aria-hidden="true">
    <path d="M17.05 12.04c-.03-2.95 2.41-4.36 2.52-4.43-1.37-2-3.5-2.28-4.27-2.31-1.82-.18-3.55 1.07-4.47 1.07-.93 0-2.35-1.05-3.86-1.02-1.98.03-3.82 1.15-4.84 2.93-2.07 3.58-.53 8.86 1.49 11.77.99 1.42 2.16 3.01 3.7 2.96 1.49-.06 2.05-.96 3.85-.96 1.79 0 2.31.96 3.88.93 1.6-.03 2.61-1.45 3.59-2.87 1.13-1.65 1.6-3.25 1.62-3.33-.04-.02-3.11-1.2-3.14-4.74zM14.65 3.34c.82-1 1.37-2.39 1.22-3.78-1.18.05-2.62.79-3.47 1.79-.76.88-1.43 2.3-1.25 3.66 1.32.1 2.67-.67 3.5-1.67z"/>
  </svg>
);
const IcGoogle   = ({ size = 18 }) => (
  <svg viewBox="0 0 40 44" width={size} height={size} aria-hidden="true">
    <defs>
      <linearGradient id="dhbgp-a" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stopColor="#00C2FF"/><stop offset="1" stopColor="#1376EE"/></linearGradient>
      <linearGradient id="dhbgp-b" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor="#FFE000"/><stop offset="1" stopColor="#FFBD00"/></linearGradient>
      <linearGradient id="dhbgp-c" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stopColor="#FF3A44"/><stop offset="1" stopColor="#C31162"/></linearGradient>
      <linearGradient id="dhbgp-d" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stopColor="#00A971"/><stop offset="1" stopColor="#00F076"/></linearGradient>
    </defs>
    <path fill="url(#dhbgp-a)" d="M2 1.6c-.4.4-.6 1-.6 1.8v37.2c0 .8.2 1.4.6 1.8l.1.1L23.4 22v-.5L2.1 1.5l-.1.1z"/>
    <path fill="url(#dhbgp-b)" d="M30.2 28.7l-6.8-6.8v-.5l6.8-6.8.2.1 8 4.6c2.3 1.3 2.3 3.4 0 4.7l-8 4.6-.2.1z"/>
    <path fill="url(#dhbgp-c)" d="M30.4 28.6l-7-7L2 43c.8.8 2 .9 3.4.1l25-14.5"/>
    <path fill="url(#dhbgp-d)" d="M30.4 14.7L5.4 .4C4 -.4 2.8-.3 2 .5l21.4 21.4 7-7.2z"/>
  </svg>
);

// --- Logo (locked, reused from project) -----------------------------
function DhbMark({ size = 32 }) {
  return (
    <svg viewBox="0 0 96 96" width={size} height={size} aria-hidden="true">
      <defs>
        <linearGradient id="dhbspiralG" x1="0" x2="1" y1="0" y2="1">
          <stop offset="0" stopColor={DHB_T.blue} />
          <stop offset="1" stopColor={DHB_T.blueDeep} />
        </linearGradient>
      </defs>
      <rect width="96" height="96" rx="28" fill="url(#dhbspiralG)" />
      <g fill="none" stroke="#fff" strokeWidth="6" strokeLinecap="round">
        <path d="M48 22 a26 26 0 1 1 -22 38" />
        <path d="M48 34 a14 14 0 1 0 12 21" />
      </g>
      <circle cx="68" cy="34" r="5" fill={DHB_T.yellow} />
    </svg>
  );
}
function DhbWordmark({ size = 22, color = DHB_T.ink }) {
  return (
    <span style={{
      fontFamily: '"Plus Jakarta Sans", system-ui, sans-serif',
      fontWeight: 800, fontSize: size, letterSpacing: -1.1,
      color, lineHeight: 1, whiteSpace: 'nowrap',
    }}>Dhobify</span>
  );
}

// --- Atoms ----------------------------------------------------------
function DhbButton({ href, onClick, children, variant = 'primary', size = 'md', icon, target, rel, ariaLabel, style: extraStyle }) {
  const sizes = {
    sm: { h: 36, px: 14, fs: 13, gap: 6 },
    md: { h: 44, px: 20, fs: 14, gap: 8 },
    lg: { h: 56, px: 28, fs: 16, gap: 10 },
  };
  const s = sizes[size] || sizes.md;
  const variants = {
    primary: {
      background: DHB_T.blue, color: '#fff',
      boxShadow: '0 1px 0 rgba(255,255,255,.2) inset, 0 8px 20px -8px rgba(61,90,254,.55)',
      border: '1px solid ' + DHB_T.blueDeep,
    },
    ghost: {
      background: 'transparent', color: DHB_T.ink,
      border: '1px solid ' + DHB_T.border,
    },
    dark: {
      background: DHB_T.ink, color: '#fff',
      border: '1px solid ' + DHB_T.ink,
    },
    yellow: {
      background: DHB_T.yellow, color: DHB_T.ink,
      border: '1px solid #E6BD22',
    },
  };
  const v = variants[variant] || variants.primary;
  // Solid variants get a light "shine" sweep across the face on hover.
  const shineable = variant === 'primary' || variant === 'dark' || variant === 'yellow';
  const [shineKey, setShineKey] = React.useState(0);
  const base = {
    display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: s.gap,
    height: s.h, padding: `0 ${s.px}px`, borderRadius: 999,
    fontFamily: '"Plus Jakarta Sans", system-ui, sans-serif',
    fontWeight: 700, fontSize: s.fs, letterSpacing: -0.1,
    textDecoration: 'none', cursor: 'pointer',
    transition: 'transform .15s ease, box-shadow .15s ease, background .15s ease',
    whiteSpace: 'nowrap',
    position: 'relative', overflow: 'hidden',
    ...v,
    ...extraStyle,
  };
  const hoverIn  = (e) => {
    e.currentTarget.style.transform = 'translateY(-1px)';
    if (shineable) setShineKey((k) => k + 1);
  };
  const hoverOut = (e) => { e.currentTarget.style.transform = 'translateY(0)'; };
  const pressIn  = (e) => { e.currentTarget.style.transform = 'scale(.97)'; };
  const pressOut = (e) => { e.currentTarget.style.transform = 'translateY(-1px)'; };
  const shine = shineable && shineKey > 0 ? (
    <span key={shineKey} aria-hidden style={{
      position: 'absolute', top: 0, bottom: 0, left: 0, width: '45%',
      background: 'linear-gradient(105deg, rgba(255,255,255,0) 0%, rgba(255,255,255,.35) 50%, rgba(255,255,255,0) 100%)',
      animation: 'dhbShine .7s ease both',
      pointerEvents: 'none',
    }} />
  ) : null;
  const props = {
    onMouseEnter: hoverIn, onMouseLeave: hoverOut,
    onMouseDown: pressIn,  onMouseUp: pressOut,
    onTouchStart: pressIn, onTouchEnd: pressOut,
    'aria-label': ariaLabel,
    style: base,
  };
  if (href) return <a href={href} target={target} rel={rel} onClick={onClick} {...props}>{children}{icon}{shine}</a>;
  return <button onClick={onClick} {...props}>{children}{icon}{shine}</button>;
}

function DhbStoreBadge({ variant = 'apple', height = 52, onDark = false }) {
  const isApple = variant === 'apple';
  const bg     = onDark ? '#fff' : '#000';
  const fg     = onDark ? DHB_T.ink : '#fff';
  const border = onDark ? '1px solid rgba(15,23,42,.08)' : '1px solid rgba(255,255,255,.14)';
  return (
    <div role="img" aria-label={isApple ? 'Download on the App Store' : 'Get it on Google Play'} style={{
      height, padding: '0 18px',
      display: 'inline-flex', alignItems: 'center', gap: 12,
      background: bg, color: fg,
      borderRadius: 12, border,
      boxShadow: onDark ? '0 6px 18px -10px rgba(0,0,0,.45)' : '0 8px 22px -12px rgba(15,23,42,.55)',
      cursor: 'pointer', userSelect: 'none',
      transition: 'transform .15s ease, box-shadow .15s ease',
      fontFamily: '"Plus Jakarta Sans", system-ui, sans-serif',
    }}
    onMouseEnter={(e) => { e.currentTarget.style.transform = 'translateY(-1px)'; }}
    onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0)'; }}>
      {isApple
        ? <IcApple size={22} fill={fg} />
        : <IcGoogle size={22} />}
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', lineHeight: 1.1 }}>
        <span style={{ fontSize: 10, fontWeight: 500, opacity: .85 }}>
          {isApple ? 'Download on the' : 'GET IT ON'}
        </span>
        <span style={{ fontSize: 17, fontWeight: 700, letterSpacing: -.3, marginTop: 2 }}>
          {isApple ? 'App Store' : 'Google Play'}
        </span>
      </div>
    </div>
  );
}

function DhbEyebrow({ children, color = DHB_T.blue }) {
  return (
    <div style={{
      display: 'inline-flex', alignItems: 'center', gap: 10,
      fontSize: 12, fontWeight: 800, letterSpacing: 1.6,
      textTransform: 'uppercase', color,
    }}>
      <span style={{ width: 20, height: 2, background: color, borderRadius: 1 }} />
      {children}
    </div>
  );
}

function DhbHeadline({ children, level = 2, mobile = false, balance = true, style }) {
  const sizes = {
    1: mobile ? 40 : 64,
    2: mobile ? 32 : 44,
    3: mobile ? 24 : 28,
  };
  const fs = sizes[level] || sizes[2];
  const Tag = `h${level}`;
  return (
    <Tag style={{
      margin: 0, fontWeight: 800,
      fontSize: fs,
      letterSpacing: level === 1 ? '-0.03em' : '-0.025em',
      lineHeight: level === 1 ? 1.04 : 1.1,
      color: DHB_T.ink,
      textWrap: balance ? 'balance' : 'pretty',
      ...style,
    }}>{children}</Tag>
  );
}

function DhbHighlight({ children, color = DHB_T.yellow, thickness = 0.42, active = true, drawDelay = 0 }) {
  // Marker-style highlight: yellow strip behind text, sitting below the
  // baseline. When `active` flips true the strip draws itself left-to-right
  // like a real marker stroke; pass active={visible} from a reveal to time it.
  const reduced = useReducedMotion();
  return (
    <span style={{ position: 'relative', display: 'inline-block', whiteSpace: 'nowrap' }}>
      <span style={{ position: 'relative', zIndex: 2 }}>{children}</span>
      <span aria-hidden style={{
        position: 'absolute', left: '-0.08em', right: '-0.08em',
        bottom: '0.04em', height: `${thickness}em`,
        background: color, zIndex: 1, borderRadius: 4,
        transform: active || reduced ? 'scaleX(1)' : 'scaleX(0)',
        transformOrigin: 'left center',
        transition: reduced ? 'none' : `transform .55s cubic-bezier(.6,0,.2,1) ${drawDelay}ms`,
      }} />
    </span>
  );
}

function DhbReveal({ children, delay = 0, distance = 16, as: Tag = 'div', style }) {
  const [ref, visible] = useReveal();
  const reduced = useReducedMotion();
  const base = {
    transition: reduced ? 'opacity .2s ease' : 'opacity .6s cubic-bezier(.16,1,.3,1), transform .6s cubic-bezier(.16,1,.3,1)',
    transitionDelay: `${delay}ms`,
    opacity: visible ? 1 : 0,
    transform: visible || reduced ? 'none' : `translateY(${distance}px)`,
    willChange: 'opacity, transform',
  };
  return <Tag ref={ref} style={{ ...base, ...style }}>{children}</Tag>;
}

// Container shell with consistent max-width + responsive padding.
function DhbContainer({ children, mobile, style }) {
  const px = mobile ? DHB_PADM : DHB_PADD;
  return (
    <div style={{
      maxWidth: DHB_MAX, margin: '0 auto',
      padding: `0 ${px}px`, ...style,
    }}>{children}</div>
  );
}

// Dotted-grid background pattern (CSS, very subtle).
function DhbDotGrid({ opacity = 0.04, size = 22, color = DHB_T.ink }) {
  return (
    <div aria-hidden style={{
      position: 'absolute', inset: 0, pointerEvents: 'none',
      backgroundImage: `radial-gradient(${color} 1px, transparent 1px)`,
      backgroundSize: `${size}px ${size}px`,
      opacity,
    }} />
  );
}

// Avatar — initial-letter on warm palette, placeholder for real dhobi photos.
const DHB_AVATAR_PALETTE = [
  { bg: '#FED7AA', fg: '#9A3412' },
  { bg: '#C7D2FE', fg: '#3730A3' },
  { bg: '#A7F3D0', fg: '#065F46' },
  { bg: '#FBCFE8', fg: '#9D174D' },
  { bg: '#FDE68A', fg: '#92400E' },
  { bg: '#DDD6FE', fg: '#5B21B6' },
  { bg: '#A5F3FC', fg: '#155E75' },
  { bg: '#FECACA', fg: '#991B1B' },
];
function DhbAvatar({ name = '?', size = 56, seed = 0 }) {
  const initials = name.trim().split(/\s+/).slice(0, 2).map(s => s[0]).join('').toUpperCase();
  const p = DHB_AVATAR_PALETTE[(seed + name.length) % DHB_AVATAR_PALETTE.length];
  return (
    <div style={{
      width: size, height: size, borderRadius: size / 2,
      background: `linear-gradient(135deg, ${p.bg} 0%, ${p.bg} 60%, color-mix(in oklab, ${p.bg} 70%, ${p.fg} 30%) 100%)`,
      color: p.fg,
      display: 'grid', placeItems: 'center',
      fontFamily: '"Plus Jakarta Sans", system-ui, sans-serif',
      fontWeight: 800, fontSize: size * 0.36, letterSpacing: -.5,
      flex: '0 0 auto',
      boxShadow: 'inset 0 -2px 6px rgba(0,0,0,.06)',
    }}>{initials}</div>
  );
}

// Star rating row — fills the integer part, half for fractional.
// `pop` makes the stars spring in one by one (used by the phone demo).
function DhbStars({ value = 5, size = 14, pop = false }) {
  const stars = [0, 1, 2, 3, 4].map((i) => {
    const filled = value >= i + 1;
    const star = (
      <IcStar key={pop ? undefined : i} size={size}
        fill={filled ? DHB_T.amber : 'transparent'}
        stroke={DHB_T.amber} weight={1.6}/>
    );
    if (!pop) return star;
    return (
      <span key={i} style={{
        display: 'inline-flex',
        animation: `dhbPopIn .4s cubic-bezier(.34,1.56,.64,1) ${i * 110}ms both`,
      }}>{star}</span>
    );
  });
  return <div style={{ display: 'inline-flex', alignItems: 'center', gap: 2 }} aria-label={`${value} out of 5 stars`}>{stars}</div>;
}

// --- export ---------------------------------------------------------
Object.assign(window, {
  DHB_T, DHB_MAX, DHB_PADD, DHB_PADM, DHB_STORE,
  dhbTrack,
  useReducedMotion, useMobile, useScrollY, useReveal, useCountUp,
  useInView, useTilt,
  DhbIc, IcCheck, IcCamera, IcClock, IcLoc, IcRupee, IcStar, IcArrow,
  IcChev, IcMenu, IcX, IcShield, IcPhone, IcZap, IcShirt, IcSparkle,
  IcInsta, IcWA, IcX_logo, IcApple, IcGoogle,
  DhbMark, DhbWordmark,
  DhbButton, DhbStoreBadge, DhbEyebrow, DhbHeadline, DhbHighlight,
  DhbReveal, DhbContainer, DhbDotGrid, DhbAvatar, DhbStars,
  DhbCountText, DhbOdometer, DhbScrollProgress, DhbColorZones, DhbQR,
});
