const SPECTRUM_VERSION = Date.now();

function SpectrumGraph() {
  const { locale, t } = useI18n();
  const sectionRef = React.useRef(null);
  const iframeRef = React.useRef(null);
  const readyRef = React.useRef(false);
  const lastTRef = React.useRef(-1);
  const [isMobile, setIsMobile] = React.useState(
    typeof window !== 'undefined' && window.matchMedia('(max-width: 820px)').matches
  );

  React.useEffect(() => {
    const mq = window.matchMedia('(max-width: 820px)');
    const onChange = () => setIsMobile(mq.matches);
    mq.addEventListener?.('change', onChange);
    return () => mq.removeEventListener?.('change', onChange);
  }, []);

  // Eight Sleep–style scroll-pin: the section is N×100vh tall, a sticky child
  // pins the iframe to the viewport, and we drive horizontal pan via the
  // page's own scroll progress. No body lock, no wheel hijack — native scroll.
  React.useEffect(() => {
    if (isMobile) return;

    const onReady = (e) => {
      const d = e.data;
      if (d && d.type === 'spectrum-ready') {
        readyRef.current = true;
        send(); // sync immediately
      }
    };
    window.addEventListener('message', onReady);

    let raf = 0;
    const send = () => {
      const sec = sectionRef.current;
      const iframe = iframeRef.current;
      if (!sec || !iframe || !iframe.contentWindow) return;
      const r = sec.getBoundingClientRect();
      const vh = window.innerHeight;
      const span = sec.offsetHeight - vh;
      let t = span > 0 ? (-r.top) / span : 0;
      if (t < 0) t = 0; else if (t > 1) t = 1;
      // Intro/outro hold: keep scene 0 pinned and readable for the first
      // ~18% of the scroll before panning starts, and let the final scene
      // rest for the last 8% before the section unpins.
      const HOLD_IN = 0.18, HOLD_OUT = 0.08;
      t = (t - HOLD_IN) / (1 - HOLD_IN - HOLD_OUT);
      if (t < 0) t = 0; else if (t > 1) t = 1;
      // Avoid spamming postMessage when t hasn't changed meaningfully.
      if (Math.abs(t - lastTRef.current) < 0.0008) return;
      lastTRef.current = t;
      try {
        iframe.contentWindow.postMessage({ type: 'pan-progress', t }, '*');
      } catch (_) {}
    };
    const onScroll = () => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(send);
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    // Initial push in case iframe loads after first scroll
    send();

    return () => {
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
      window.removeEventListener('message', onReady);
      cancelAnimationFrame(raf);
    };
  }, [isMobile]);

  if (isMobile) return <SpectrumMobile />;

  return (
    <section
      id="spectrum"
      ref={sectionRef}
      data-section="spectrum"
      data-screen-label="Spectrum"
      style={{
        position: 'relative',
        width: '100%',
        height: '560vh', /* extra 60vh covers the intro/outro hold zones */
        background: '#0a0a0a',
      }}
    >
      <div
        style={{
          /* Offset below the fixed site header so the 450nm spike (100%, at the
             top of the graph) isn't clipped behind it — mirrors the mobile layout
             where the graph sits in a padded region under the header. */
          position: 'sticky',
          top: 72,
          width: '100%',
          height: 'calc(100vh - 72px)',
          overflow: 'hidden',
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        {/* Pinned intro — kept visible above the graph on desktop (mirrors mobile) */}
        <div style={{
          flex: '0 0 auto',
          padding: '18px 5vw 14px 8vw',
          background: '#0a0a0a',
          borderBottom: '1px solid rgba(255,255,255,.07)',
        }}>
          <h2 style={{
            font: '300 clamp(20px, 2.4vw, 30px)/1.2 var(--font-nav)',
            letterSpacing: '-0.01em',
            color: '#fff',
            margin: '0 0 8px',
          }}>{t('spectrum.heroTitle').replace(/\n/g, ' ')}</h2>
          <p style={{
            font: '300 13px/1.6 var(--font-nav)',
            color: 'rgba(255,255,255,.5)',
            margin: 0,
            maxWidth: 640,
          }}>{t('spectrum.heroBody')}</p>
        </div>
        <div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
          <iframe
            ref={iframeRef}
            src={`spectrum.html?lang=${locale}&v=${SPECTRUM_VERSION}`}
            title={t('spectrum.title')}
            style={{
              display: 'block',
              width: '100%',
              height: '100%',
              border: 0,
              background: '#0a0a0a',
            }}
          />
        </div>
      </div>
    </section>
  );
}

// ── Mobile fallback ─────────────────────────────────────────────────────
// ── Mobile sticky-pinned spectrum (Eight Sleep-style scroll-driven) ─────
// Marker dot stays fixed at center; curves pan beneath it as you scroll.
const SPECTRUM_SCENES_MOBILE = [
  {nm:380, zone:null, ztag:null,
   kicker:'SOLADUO Overview',
   title:'What is SunLike White?',
   body:"SunLike White of Seoul Semiconductor has a similar spectrum as natural sunlight spectrum. It may give you benefits to reduce eye fatigue and improve focus, learning efficiency and better sleep quality. SunLike White may create a comfortable and natural ambiance in your space."},
  {nm:415, zone:'D', ztag:{l:'D',bg:'rgba(160,80,255,.25)',fg:'#c880ff'},
   kicker:'Opsin 5 · 415nm · Myopia Prevention',
   title:'SunLike White : Human Centric Lighting',
   body:"White spectrum designed to aid concentration and balance circadian rhythm. The closest spectrum to natural daylight."},
  {nm:450, zone:'A', ztag:{l:'A',bg:'rgba(100,130,255,.25)',fg:'#9ab3ff'},
   kicker:'Blue Hazard · 450nm',
   title:'The Limitation of Standard Lighting: The 400nm Blue Hazard',
   body:"Conventional white LEDs emit an abnormally high spike of blue light at the 400nm wavelength. This artificial peak causes eye strain and disrupts your sleep cycle. SunLike White eliminates this jagged curve, delivering a smooth, balanced spectrum that perfectly matches natural sunlight."},
  {nm:490, zone:'B', ztag:{l:'B',bg:'rgba(50,200,170,.2)',fg:'#32c8b4'},
   kicker:'Melanopic Zone · 480nm',
   title:'Restoring the True Colors of Nature',
   body:"Standard lighting loses crucial wavelengths in the 470-490nm range, significantly limiting its color rendition. By fully restoring this missing spectrum without peaks or valleys, SunLike White reveals the innate true colors of your surroundings across 99 distinct shades with over 95% fidelity."},
  {nm:530, zone:null, ztag:null,
   kicker:'True Colors · 470–500nm',
   title:'Restoring the True Colors of Nature',
   body:"Standard lighting loses crucial wavelengths in the 470-490nm range, significantly limiting its color rendition. By fully restoring this missing spectrum without peaks or valleys, SunLike White reveals the innate true colors of your surroundings across 99 distinct shades with over 95% fidelity."},
  {nm:570, zone:null, ztag:null,
   kicker:'Broad Peak Rising · 530–570nm',
   title:'Improve learning capability',
   body:"SunLike White rises smoothly from the valley to its broad peak region — mimicking natural sunlight's gradual increase. Increases alertness and learning effect during the daytime. Standard LED took a jagged path to reach the same region."},
  {nm:650, zone:null, ztag:null,
   kicker:'CRI-97 · Rf 95+',
   title:'Accurate color rendition & Certified Safe',
   body:"By tracking natural sunlight across the full visible spectrum, SunLike White achieves CRI-97. Effective on eye health for growing children. SunLike White declines gracefully after 650nm, unlike Standard LED's jagged history."},
  {nm:720, zone:null, ztag:null,
   kicker:'5개국 10개 글로벌 혁신 기술상',
   title:'Certified Safe Lighting',
   body:"We have won 10 Global Innovation Technology Awards in 5 countries. SunLike White establishes a comfortable environment in any space — certified safe and healthy by global authorities."}
];

// Curve data (matches desktop spectrum.html exactly)
// Desktop coords: x = (nm-380)*18 + 150, y = 800 - intensity*7.6
const _SPEC_SUN_DT = [[150,724],[510,656],[780,595],[1050,566],[1410,526],[1950,473],[2310,444],[2850,405],[3210,382],[3570,351],[3750,337],[4110,306],[4470,192],[4650,200],[4830,222],[5010,238],[5190,268],[5370,316],[5550,382],[5730,481],[5910,587],[6090,663],[6270,724],[6450,762],[6630,785],[8000,800]];
const _SPEC_STD_DT = [[150,762],[510,738],[870,682],[1050,604],[1200,440],[1290,356],[1410,40],[1500,250],[1620,620],[1770,740],[1950,762],[2130,747],[2310,709],[2670,595],[2850,542],[3210,420],[3570,329],[3750,306],[3930,299],[4110,299],[4290,316],[4470,344],[4650,374],[5010,435],[5550,572],[5910,663],[6270,724],[6810,768],[8000,790]];
const _SPEC_NAT_DT = [[150,648],[510,610],[780,572],[1050,534],[1410,496],[1950,450],[2310,420],[2850,382],[3210,359],[3570,329],[3750,314],[4110,283],[4650,245],[5010,222],[5550,192],[5910,177],[6270,169],[6810,169],[8000,196]];

// Mobile SVG geometry — wider curve space than viewBox so curves can pan.
const _SPEC_NM_LO = 380, _SPEC_NM_HI = 780, _SPEC_NM_END = 720;
const _SPEC_VIEW_W = 360, _SPEC_H = 200;
const _SPEC_CENTER_X = 180;
const _SPEC_PAD_T = 14, _SPEC_PAD_B = 30;
const _SPEC_plotH = _SPEC_H - _SPEC_PAD_T - _SPEC_PAD_B;
const _SPEC_baseY = _SPEC_PAD_T + _SPEC_plotH;
// Curve-space x: 4 units per nm  →  spectrum 380-780nm spans 0-1600.
const _SPEC_NM_PER_UNIT = 4;
const _SPEC_nmX = (nm) => (nm - _SPEC_NM_LO) * _SPEC_NM_PER_UNIT;

function _specConvert(pts){
  return pts.map(([x,y])=>{
    const nm = (x - 150) / 18 + _SPEC_NM_LO;
    const intensity = (800 - y) / 7.6;
    return {nm, intensity, mx:_SPEC_nmX(nm), my:_SPEC_PAD_T + (1 - intensity/100) * _SPEC_plotH};
  });
}
const _SPEC_SUN = _specConvert(_SPEC_SUN_DT);
const _SPEC_STD = _specConvert(_SPEC_STD_DT);
const _SPEC_NAT = _specConvert(_SPEC_NAT_DT);

function _specIntAt(data, nm){
  if(nm<=data[0].nm) return data[0].intensity;
  for(let i=0;i<data.length-1;i++){
    if(nm<=data[i+1].nm){
      const t=(nm-data[i].nm)/(data[i+1].nm-data[i].nm);
      return data[i].intensity+(data[i+1].intensity-data[i].intensity)*t;
    }
  }
  return data[data.length-1].intensity;
}
function _specYAt(data, nm){ return _SPEC_PAD_T + (1 - _specIntAt(data, nm)/100) * _SPEC_plotH; }

function _specSmoothPath(data){
  const P = data.map(d => [d.mx, d.my]);
  if(P.length<2) return '';
  let d = `M${P[0][0].toFixed(2)},${P[0][1].toFixed(2)}`;
  for(let k=0;k<P.length-1;k++){
    const p0=P[k-1]||P[k], p1=P[k], p2=P[k+1], p3=P[k+2]||p2;
    const c1x=p1[0]+(p2[0]-p0[0])/6, c1y=p1[1]+(p2[1]-p0[1])/6;
    const c2x=p2[0]-(p3[0]-p1[0])/6, c2y=p2[1]-(p3[1]-p1[1])/6;
    d += ` C${c1x.toFixed(2)},${c1y.toFixed(2)} ${c2x.toFixed(2)},${c2y.toFixed(2)} ${p2[0].toFixed(2)},${p2[1].toFixed(2)}`;
  }
  return d;
}
const _SPEC_sunPath = _specSmoothPath(_SPEC_SUN);
const _SPEC_natPath = _specSmoothPath(_SPEC_NAT);
const _SPEC_stdPath = _specSmoothPath(_SPEC_STD);
const _SPEC_sunFill = `${_SPEC_sunPath} L${_SPEC_nmX(_SPEC_NM_HI).toFixed(2)},${_SPEC_baseY} L${_SPEC_nmX(_SPEC_NM_LO).toFixed(2)},${_SPEC_baseY} Z`;

const _SPEC_ZONES = {
  D:{nm:415, c:'rgba(160,80,255,.7)', fg:'#c880ff', label:'D · 415nm'},
  A:{nm:450, c:'rgba(100,130,255,.7)', fg:'#9ab3ff', label:'A · 450nm'},
  B:{nm:480, c:'rgba(50,210,180,.7)', fg:'#32c8b4', label:'B · 480nm'},
  C:{nm:620, c:'rgba(255,130,60,.7)', fg:'#ff8c50', label:'C · 620nm'}
};
const _SPEC_XTICKS = [400, 450, 500, 550, 600, 650, 700];

function SpectrumMobile() {
  const { t } = useI18n();
  const sceneTexts = t('spectrum.scenes');
  const scenes = SPECTRUM_SCENES_MOBILE.map((scene, i) => ({ ...scene, ...sceneTexts[i] }));
  const outerRef = React.useRef(null);
  const panRef = React.useRef(null);          // group that pans horizontally
  const sunDotRef = React.useRef(null);
  const sunHaloRef = React.useRef(null);
  const natDotRef = React.useRef(null);
  const stdDotRef = React.useRef(null);
  const zoneRefs = { D: React.useRef(null), A: React.useRef(null), B: React.useRef(null), C: React.useRef(null) };
  const progRef = React.useRef(null);
  const hintRef = React.useRef(null);
  const [activeIdx, setActiveIdx] = React.useState(0);

  React.useEffect(() => {
    const outer = outerRef.current;
    if (!outer) return;
    let raf = 0, lastIdx = -1;

    const update = () => {
      raf = 0;
      const r = outer.getBoundingClientRect();
      const vh = window.innerHeight;
      const span = outer.offsetHeight - vh;
      let rawT = span > 0 ? (-r.top) / span : 0;
      if (rawT < 0) rawT = 0; else if (rawT > 1) rawT = 1;

      if (rawT > 0.02 && hintRef.current) hintRef.current.classList.add('hide');

      // Intro/outro hold: first scene stays readable before panning starts;
      // last scene rests briefly before the section unpins.
      const HOLD_IN = 0.16, HOLD_OUT = 0.08;
      let t = (rawT - HOLD_IN) / (1 - HOLD_IN - HOLD_OUT);
      if (t < 0) t = 0; else if (t > 1) t = 1;

      // focus nm — what the central marker is "looking at"
      const nm = _SPEC_NM_LO + t * (_SPEC_NM_END - _SPEC_NM_LO);
      const curveX = _SPEC_nmX(nm);
      const offset = curveX - _SPEC_CENTER_X;
      const sy = _specYAt(_SPEC_SUN, nm);
      const ny = _specYAt(_SPEC_NAT, nm);
      const dy = _specYAt(_SPEC_STD, nm);

      // Pan the curves so the focus point sits at center.
      if (panRef.current) panRef.current.setAttribute('transform', `translate(${-offset.toFixed(2)}, 0)`);

      // Update fixed-center marker dot y positions (cx stays = CENTER_X).
      if (sunDotRef.current)  { sunDotRef.current.setAttribute('cy', sy);  }
      if (sunHaloRef.current) { sunHaloRef.current.setAttribute('cy', sy); }
      if (natDotRef.current)  { natDotRef.current.setAttribute('cy', ny);  }
      if (stdDotRef.current)  { stdDotRef.current.setAttribute('cy', dy);  }
      // Progress bar tracks raw scroll so the user gets feedback even
      // during the hold zones (otherwise the section feels "stuck").
      if (progRef.current)    progRef.current.style.width = (rawT * 100) + '%';

      // Nearest scene
      let bestI = 0, bestD = Infinity;
      for (let i = 0; i < SPECTRUM_SCENES_MOBILE.length; i++) {
        const d = Math.abs(SPECTRUM_SCENES_MOBILE[i].nm - nm);
        if (d < bestD) { bestD = d; bestI = i; }
      }
      if (bestI !== lastIdx) {
        lastIdx = bestI;
        setActiveIdx(bestI);
        const scene = SPECTRUM_SCENES_MOBILE[bestI];
        ['D','A','B','C'].forEach(k => {
          if (zoneRefs[k].current) zoneRefs[k].current.setAttribute('opacity', scene.zone === k ? '1' : '0');
        });
      }
    };

    const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); };
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    update();
    return () => {
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
      cancelAnimationFrame(raf);
    };
  }, []);

  return (
    <section id="spectrum" data-section="spectrum" data-screen-label="Spectrum"
      style={{ position:'relative', width:'100%', background:'#0a0a0a', color:'#fff', padding:0 }}>
      <style>{`
        #spectrum, #spectrum * {
          font-family: 'AvenirLTStd','Avenir LT Std','AvenirNextLTPro-Regular','Avenir Next','Avenir','Mulish','Nunito Sans',-apple-system,system-ui,sans-serif !important;
        }
        /* Persistent section intro band — sits between the moving graph and the
           changing scene cards (replaces the old full-screen hero intro). */
        #spectrum .sm-intro{flex:0 0 auto;padding:11px 22px 9px;background:#0a0a0a;border-top:1px solid rgba(255,255,255,.07);}
        #spectrum .sm-intro h2{font:300 18px/1.24 inherit;letter-spacing:-0.01em;color:#fff;margin:0 0 5px;}
        #spectrum .sm-intro p{font:300 11px/1.5 inherit;color:rgba(255,255,255,.5);margin:0;max-width:520px;}

        #spectrum .sm-pin-outer{position:relative;height:1000vh;}
        /* Offset the pinned panel below the fixed site header (~64px) so the
           380 NM readout, 01/09 counter and graph top aren't clipped behind it
           — mirrors the desktop layout's top:72 offset. */
        #spectrum .sm-pin-inner{position:sticky;top:64px;height:calc(100vh - 64px);display:flex;flex-direction:column;overflow:hidden;background:#0a0a0a;}
        #spectrum .sm-graph-area{flex:0 0 40vh;position:relative;padding:7vh 0 12px;background:radial-gradient(ellipse 85% 75% at 50% 50%, #0d1a28 0%, transparent 70%);}
        #spectrum .sm-graph-svg{width:100%;height:100%;display:block;overflow:hidden;}
        /* Hide zone highlight overlays (dashed verticals + "D · 415nm" labels).
           display:none beats the inline opacity JS keeps writing via zoneRefs. */
        #spectrum .sm-zone{display:none!important;}
        /* Hide the scene-card kicker row — the round A/B/C/D badge and its
           "Opsin 5 · 415nm …" label. Title + body remain. */
        #spectrum .sm-kicker{display:none!important;}
        /* Soft fade at the SVG's left & right edges so panning content
           fades in/out instead of cutting hard. */
        #spectrum .sm-graph-area::before,
        #spectrum .sm-graph-area::after{
          content:'';position:absolute;top:0;bottom:0;width:48px;pointer-events:none;z-index:3;
        }
        #spectrum .sm-graph-area::before{left:0;background:linear-gradient(90deg,#0a0a0a 0%,rgba(10,10,10,0.85) 35%,transparent 100%);}
        #spectrum .sm-graph-area::after{right:0;background:linear-gradient(270deg,#0a0a0a 0%,rgba(10,10,10,0.85) 35%,transparent 100%);}

        /* Fixed center indicator (sits on top of the SVG) */
        #spectrum .sm-center-nm{position:absolute;left:50%;top:7vh;transform:translateX(-50%);
          font-size:11px;letter-spacing:.2em;font-weight:600;color:#F5A623;
          text-transform:uppercase;z-index:4;pointer-events:none;
          font-family:'AvenirLTStd','Avenir LT Std','Avenir Next',monospace;}
        #spectrum .sm-ilbl{position:absolute;left:4px;top:50%;transform:translate(-30%,-50%) rotate(-90deg);
          font-size:9px;letter-spacing:.18em;color:rgba(255,255,255,.35);font-weight:600;
          text-transform:uppercase;white-space:nowrap;pointer-events:none;z-index:4;}
        #spectrum .sm-axis-label{position:absolute;bottom:0;left:0;right:0;text-align:center;
          font-size:9.5px;letter-spacing:.14em;color:rgba(255,255,255,.4);font-weight:500;
          text-transform:uppercase;z-index:4;}
        #spectrum .sm-counter-fixed{position:absolute;top:14px;right:18px;
          font-size:10px;letter-spacing:.16em;color:rgba(255,255,255,.45);font-weight:600;z-index:4;}
        /* Persistent legend inside the pinned graph — stays visible after the
           hero (with its legend) has scrolled away. */
        #spectrum .sm-leg{position:absolute;top:12px;left:16px;z-index:4;display:flex;flex-direction:column;gap:5px;pointer-events:none;}
        #spectrum .sm-leg-i{display:flex;align-items:center;gap:7px;font-size:9px;letter-spacing:.04em;color:rgba(255,255,255,.5);white-space:nowrap;}
        #spectrum .sm-leg-i.sun{color:#FFFFFF;}
        #spectrum .sm-leg-i.std{color:rgba(150,170,200,.7);}
        #spectrum .sm-leg-i .sm-ll{width:16px;height:2px;border-radius:1px;flex-shrink:0;}
        #spectrum .sm-leg-i .sm-ll.g{background:#FFFFFF;}
        #spectrum .sm-leg-i .sm-ll.n{background:#6FC3F0;}
        #spectrum .sm-leg-i .sm-ll.s{background:transparent;height:0;border-top:1.5px dashed rgba(150,170,200,.7);}

        #spectrum .sm-text-area{flex:1;position:relative;padding:16px 22px 20px;background:#0a0a0a;border-top:1px solid rgba(255,255,255,.05);}
        #spectrum .sm-card{position:absolute;left:22px;right:22px;top:16px;bottom:20px;
          opacity:0;transform:translateY(14px);transition:opacity 380ms ease,transform 380ms ease;
          pointer-events:none;display:flex;flex-direction:column;}
        #spectrum .sm-card.on{opacity:1;transform:none;pointer-events:auto;}
        #spectrum .sm-kicker{font-size:10.5px;letter-spacing:.22em;color:#F5A623;text-transform:uppercase;
          margin-bottom:9px;font-weight:600;display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
        #spectrum .sm-ztag{display:inline-flex;align-items:center;justify-content:center;
          width:18px;height:18px;border-radius:50%;font-size:10px;font-weight:700;letter-spacing:0;}
        #spectrum .sm-h{font:300 18px/1.24 inherit;letter-spacing:-0.01em;color:#fff;margin:0 0 9px;max-width:340px;}
        #spectrum .sm-b{font:300 12px/1.58 inherit;color:rgba(255,255,255,.55);max-width:480px;margin:0 0 auto;}
        #spectrum .sm-card-foot{display:flex;align-items:center;gap:10px;margin-top:11px;}
        #spectrum .sm-dot{width:7px;height:7px;border-radius:50%;background:#F5A623;flex-shrink:0;}
        #spectrum .sm-nm{font-size:11px;letter-spacing:.1em;color:rgba(255,255,255,.7);font-weight:500;}
        #spectrum .sm-rule{flex:1;height:1px;background:rgba(255,255,255,.08);}
        #spectrum .sm-prog{position:absolute;bottom:0;left:0;height:2px;background:#F5A623;width:0%;z-index:10;transition:width .1s linear;}
        #spectrum .sm-hint{position:absolute;left:50%;transform:translateX(-50%);bottom:14px;
          display:flex;flex-direction:column;align-items:center;gap:6px;font-size:9px;letter-spacing:.22em;
          color:rgba(255,255,255,.3);text-transform:uppercase;pointer-events:none;transition:opacity .4s;z-index:5;}
        #spectrum .sm-hint.hide{opacity:0;}
        #spectrum .sm-hint .arrow{width:1px;height:18px;background:linear-gradient(to bottom,rgba(255,255,255,.35),transparent);animation:sm-arr 1.6s ease-in-out infinite;}
        @keyframes sm-arr{0%,100%{opacity:.4}50%{opacity:1}}
        #spectrum .sm-foot-note{padding:48px 22px 56px;text-align:center;background:#0a0a0a;}
        #spectrum .sm-foot-note span{font-size:10.5px;letter-spacing:.18em;color:rgba(255,255,255,.3);text-transform:uppercase;}
      `}</style>

      <div ref={outerRef} className="sm-pin-outer">
        <div className="sm-pin-inner">
          <div className="sm-intro">
            <h2>{t('spectrum.heroTitle').replace(/\n/g,' ')}</h2>
            <p>{t('spectrum.heroBody')}</p>
          </div>
          <div className="sm-graph-area">
            <div className="sm-ilbl">{t('common.lightIntensity')}</div>
            <div className="sm-counter-fixed">{String(activeIdx+1).padStart(2,'0')} / 09</div>
            <div className="sm-center-nm">{Math.round(_SPEC_NM_LO + (activeIdx/(SPECTRUM_SCENES_MOBILE.length-1))*(_SPEC_NM_END-_SPEC_NM_LO))} nm</div>
            <div className="sm-leg">
              <div className="sm-leg-i sun"><span className="sm-ll g"/>SunLike White</div>
              <div className="sm-leg-i nat"><span className="sm-ll n"/>Natural</div>
              <div className="sm-leg-i std"><span className="sm-ll s"/>Standard</div>
            </div>

            <svg className="sm-graph-svg" viewBox={`0 0 ${_SPEC_VIEW_W} ${_SPEC_H}`} preserveAspectRatio="xMidYMid meet">
              <defs>
                <linearGradient id="smSunFill2" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="0%" stopColor="#FFFFFF" stopOpacity=".28"/>
                  <stop offset="100%" stopColor="#FFFFFF" stopOpacity="0"/>
                </linearGradient>
              </defs>

              {/* ── PANNING GROUP (curves + zones + ticks pan as you scroll) ── */}
              <g ref={panRef} transform="translate(180, 0)">
                {/* Baseline */}
                <line
                  x1={_SPEC_nmX(_SPEC_NM_LO)} y1={_SPEC_baseY}
                  x2={_SPEC_nmX(_SPEC_NM_HI)} y2={_SPEC_baseY}
                  stroke="rgba(255,255,255,0.18)" strokeWidth="0.6"/>

                {/* X-axis ticks every 50nm */}
                {_SPEC_XTICKS.map(nm => (
                  <g key={nm}>
                    <line x1={_SPEC_nmX(nm)} y1={_SPEC_baseY} x2={_SPEC_nmX(nm)} y2={_SPEC_baseY+4}
                      stroke="rgba(255,255,255,0.28)" strokeWidth="0.6"/>
                    <text x={_SPEC_nmX(nm)} y={_SPEC_baseY+15} textAnchor="middle"
                      fill="rgba(255,255,255,0.48)" fontSize="9">{nm}</text>
                  </g>
                ))}

                {/* Zone verticals + labels (fade in when active scene matches) */}
                {Object.entries(_SPEC_ZONES).map(([k,z]) => (
                  <g key={k} className="sm-zone" ref={zoneRefs[k]} opacity="0" style={{transition:'opacity 380ms ease'}}>
                    <line x1={_SPEC_nmX(z.nm)} y1={_SPEC_PAD_T} x2={_SPEC_nmX(z.nm)} y2={_SPEC_baseY}
                      stroke={z.c} strokeWidth="1.1" strokeDasharray="4 4"/>
                    <text x={_SPEC_nmX(z.nm)+5} y={_SPEC_PAD_T+10}
                      fill={z.fg} fontSize="9" fontWeight="600">{z.label}</text>
                  </g>
                ))}

                {/* Standard LED dashed */}
                <path d={_SPEC_stdPath} fill="none" stroke="rgba(150,170,200,0.7)"
                  strokeWidth="1.3" strokeDasharray="3 4"/>
                {/* Natural Sunlight */}
                <path d={_SPEC_natPath} fill="none" stroke="#6FC3F0" strokeWidth="1.5"/>
                {/* SunLike fill + line */}
                <path d={_SPEC_sunFill} fill="url(#smSunFill2)"/>
                <path d={_SPEC_sunPath} fill="none" stroke="#FFFFFF" strokeWidth="3.4"/>
              </g>

              {/* ── FIXED CENTER MARKER (stays at viewBox center) ── */}
              <line x1={_SPEC_CENTER_X} y1={_SPEC_PAD_T} x2={_SPEC_CENTER_X} y2={_SPEC_baseY}
                stroke="rgba(255,255,255,0.5)" strokeWidth="1" strokeDasharray="2 3"/>
              <circle ref={stdDotRef}
                cx={_SPEC_CENTER_X} cy={_specYAt(_SPEC_STD,_SPEC_NM_LO)} r="4"
                fill="rgba(150,170,200,0.9)" stroke="rgba(10,10,10,0.95)" strokeWidth="0.8"/>
              <circle ref={natDotRef}
                cx={_SPEC_CENTER_X} cy={_specYAt(_SPEC_NAT,_SPEC_NM_LO)} r="4"
                fill="#6FC3F0" stroke="rgba(10,10,10,0.95)" strokeWidth="0.8"/>
              <circle ref={sunHaloRef}
                cx={_SPEC_CENTER_X} cy={_specYAt(_SPEC_SUN,_SPEC_NM_LO)} r="14"
                fill="rgba(245,166,35,0.22)"/>
              <circle ref={sunDotRef}
                cx={_SPEC_CENTER_X} cy={_specYAt(_SPEC_SUN,_SPEC_NM_LO)} r="6.5"
                fill="#FFFFFF" stroke="rgba(10,10,10,0.95)" strokeWidth="0.8"/>
            </svg>

            <div className="sm-axis-label">{t('common.wavelength')}</div>
            <div ref={hintRef} className="sm-hint"><span>{t('spectrum.scroll')}</span><div className="arrow"/></div>
          </div>

          <div className="sm-text-area">
            {scenes.map((s, i) => (
              <div key={i} className={`sm-card${i===activeIdx?' on':''}`} data-screen-label={`Spectrum ${String(i+1).padStart(2,'0')}`}>
                <div className="sm-kicker">
                  {s.ztag && <span className="sm-ztag" style={{background:s.ztag.bg, color:s.ztag.fg}}>{s.ztag.l}</span>}
                  <span>{s.kicker}</span>
                </div>
                <h3 className="sm-h">{s.title}</h3>
                <p className="sm-b">{s.body}</p>
                <div className="sm-card-foot">
                  <span className="sm-dot"/>
                  <span className="sm-nm">{s.nm} nm</span>
                  <span className="sm-rule"/>
                </div>
              </div>
            ))}
          </div>
          <div ref={progRef} className="sm-prog"/>
        </div>
      </div>

      <div className="sm-foot-note">
        <span>{t('spectrum.footerNote')}</span>
      </div>
    </section>
  );
}

window.SpectrumGraph = SpectrumGraph;
