// shared.jsx — 1518 · Japandi design language
// Tokens, fonts, grain, atoms, recipe data, loop hook.
// Everything exported to window for cross-babel-file use.

// ── Palette: cool-muted Japandi, warm-hearted ───────────────
const J = {
  paper:   '#F1ECE3',  // oat paper canvas
  paper2:  '#E7E0D3',  // deeper oat
  card:    '#FBF9F4',  // raised card surface
  cardSunk:'#EDE8DD',  // sunken / inset surface
  ink:     '#2B2A26',  // warm near-black
  inkSoft: '#544F47',  // soft ink (body)
  stone:   '#8B867B',  // muted label text
  stoneLt: '#A8A296',  // faint text
  line:    '#D9D1C2',  // hairline
  lineSoft:'#E6DFD2',  // faintest hairline
  sage:    '#8B9A7D',  // the "alive" accent
  sageDeep:'#5C6850',  // deep sage (text on light)
  sageSoft:'#BAC4AB',  // soft sage fill
  sageWash:'#E4E7DC',  // sage tint wash
  clay:    '#B6906F',  // warm secondary, sparing
  claySoft:'#DAC1A8',  // soft clay
  clayWash:'#EFE3D6',  // clay tint wash
  dark:    false,
};

// ── Bold palette: "Ember" — low-light dusk cafe ─────────────
// Espresso canvas, warm cream type, bold ember-orange glow.
// Mirrors J's keys so themed components swap in cleanly.
const JB = {
  paper:   '#211B16',  // espresso canvas
  paper2:  '#191410',  // deeper espresso
  card:    '#2C251F',  // raised card surface
  cardSunk:'#1B1510',  // sunken / inset
  ink:     '#F4ECDD',  // warm cream (primary text)
  inkSoft: '#D6C9B4',  // soft cream (body)
  stone:   '#9E9079',  // muted warm taupe
  stoneLt: '#71665757',  // faint — overridden below
  line:    '#3E342B',  // hairline
  lineSoft:'#2C241D',  // faintest hairline
  sage:    '#E26F38',  // BOLD ember — the "alive" accent
  sageDeep:'#F3905A',  // bright ember (text/leading on dark)
  sageSoft:'#B85327',  // ember glow fill
  sageWash:'#36251A',  // ember tint wash
  clay:    '#E2A53C',  // bold amber secondary
  claySoft:'#8C6526',  // deep amber
  clayWash:'#2F2516',  // amber tint wash
  dark:    true,
};
JB.stoneLt = '#7A6E5D';

// ── Palette context ─────────────────────────────────────────
const PaletteCtx = React.createContext(J);
function useP(){ return React.useContext(PaletteCtx) || J; }

// ── Viewport hook (responsive) ──────────────────────────────
function useViewport(){
  const [w, setW] = React.useState(typeof window!=='undefined' ? window.innerWidth : 1200);
  React.useEffect(()=>{
    const on = ()=> setW(window.innerWidth);
    window.addEventListener('resize', on, { passive:true });
    return ()=> window.removeEventListener('resize', on);
  },[]);
  return { w, isMobile: w < 760, isNarrow: w < 1000 };
}
Object.assign(window, { useViewport });

// ── Fonts ───────────────────────────────────────────────────
(function injectFonts(){
  if (document.getElementById('bloom-fonts')) return;
  const l = document.createElement('link');
  l.id = 'bloom-fonts';
  l.rel = 'stylesheet';
  l.href = 'https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400;1,6..72,500&family=Hanken+Grotesk:wght@400;500;600;700&display=swap';
  document.head.appendChild(l);
  const s = document.createElement('style');
  s.textContent = `
    *{box-sizing:border-box}
    .serif{font-family:'Newsreader',Georgia,serif}
    .sans{font-family:'Hanken Grotesk',-apple-system,system-ui,sans-serif}
    .tnum{font-variant-numeric:tabular-nums}
  `;
  document.head.appendChild(s);
})();

const SERIF = "'Newsreader', Georgia, serif";
const SANS  = "'Hanken Grotesk', -apple-system, system-ui, sans-serif";

// ── Grain overlay (paper tooth) ─────────────────────────────
const GRAIN_URL = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")";
function Grain({ opacity, blend }) {
  const P = useP();
  const op = opacity != null ? opacity : (P.dark ? 0.07 : 0.05);
  const bl = blend || (P.dark ? 'soft-light' : 'multiply');
  return <div aria-hidden style={{
    position:'absolute', inset:0, backgroundImage:GRAIN_URL,
    backgroundSize:'160px 160px', opacity:op, mixBlendMode:bl,
    pointerEvents:'none', zIndex:3,
  }} />;
}

// ── Atoms ───────────────────────────────────────────────────
// Tracked small-caps eyebrow label
function Eyebrow({ children, color, style = {} }) {
  const P = useP();
  return <div className="sans" style={{
    fontSize:10.5, fontWeight:600, letterSpacing:'0.18em',
    textTransform:'uppercase', color: color || P.stone, ...style,
  }}>{children}</div>;
}

// Hairline rule
function Rule({ color, style = {} }) {
  const P = useP();
  return <div style={{ height:1, background: color || P.line, ...style }} />;
}

// The screen shell that sits inside an IOSDevice — sets paper bg, grain,
// and the safe-area padding around iOS chrome.
function Screen({ children, bg, pad = '64px 26px 30px', innerRef }) {
  const P = useP();
  return (
    <div ref={innerRef} className="sans" style={{
      position:'relative', height:'100%', width:'100%',
      background: bg || P.paper, color:P.ink, padding:pad, overflow:'hidden',
    }}>
      {children}
      <Grain />
    </div>
  );
}

// ── Recipe data (Finca La Esperanza · V60) ──────────────────
const RECIPE = {
  roaster:'Manhattan Coffee Roasters',
  name:'Finca La Esperanza',
  origin:'Colombia · Huila',
  variety:'Pink Bourbon',
  process:'Washed',
  method:'V60 · Pour-over',
  dose:'15 g',
  water:'250 g',
  ratio:'1 : 16.7',
  temp:'96°C',
  grind:'Medium-fine',
  total:195,
  stages:[
    { n:'Bloom',       t0:0,   t1:45,  water:45,  total:45,
      note:'Saturate the grounds. Let the bed breathe and dome.' },
    { n:'First pour',  t0:45,  t1:75,  water:73,  total:118,
      note:'Steady concentric circles — keep the bed level.' },
    { n:'Second pour', t0:75,  t1:105, water:132, total:250,
      note:'A touch faster, then settle with a gentle swirl.' },
    { n:'Drawdown',    t0:105, t1:195, water:0,   total:250,
      note:'Let it draw down. A flat bed means even extraction.' },
  ],
};
const SCAN_FIELDS = [
  { k:'Roaster', v:'Manhattan Coffee' },
  { k:'Origin',  v:'Colombia · Huila' },
  { k:'Variety', v:'Pink Bourbon' },
  { k:'Process', v:'Washed' },
  { k:'Altitude',v:'1850 masl' },
  { k:'Notes',   v:'Raspberry · Jasmine' },
];

function fmt(s){ const m=Math.floor(s/60); return m+':'+String(Math.floor(s%60)).padStart(2,'0'); }

// ── Looping progress hook (for scan ambience) ───────────────
// Returns t in [0,1] that ramps over `dur` ms, holds at 1 for `hold` ms,
// then resets. `start` offsets the initial phase. setInterval-driven so it
// keeps advancing even when the tab/iframe is backgrounded.
function useLoop(dur = 4200, hold = 1600, start = 0, active = true){
  const [t, setT] = React.useState(start || 0.001);
  const tRef = React.useRef(t); tRef.current = t;
  React.useEffect(() => {
    if (!active) return;
    const cycle = dur + hold;
    const t0 = performance.now() - tRef.current * dur; // resume near current phase
    const id = setInterval(() => {
      const e = (performance.now() - t0) % cycle;
      setT(e < dur ? e / dur : 1);
    }, 1000 / 15);
    return () => clearInterval(id);
  }, [dur, hold, active]);
  return t;
}

// ── Visibility gate ─ only animate frames that are on screen ─────
function useInView(ref, rootMargin = '120px'){
  const [vis, setVis] = React.useState(false);
  React.useEffect(() => {
    const el = ref && ref.current;
    if (!el || typeof IntersectionObserver === 'undefined') { setVis(true); return; }
    const io = new IntersectionObserver(
      ([e]) => setVis(e.isIntersecting),
      { root: null, rootMargin, threshold: 0.01 }
    );
    io.observe(el);
    return () => io.disconnect();
  }, [ref]);
  return vis;
}

// ── Looping seconds timer (for brew timer) ──────────────────
// Counts seconds 0..total, holds, loops. `from` sets initial elapsed.
function useBrew(total, from = 0, active = true){
  const [s, setS] = React.useState(from);
  React.useEffect(() => {
    if (!active) return;
    let id = setInterval(() => {
      setS(prev => prev >= total + 3 ? 0 : prev + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [total, active]);
  return s;
}

Object.assign(window, {
  J, JB, PaletteCtx, useP, SERIF, SANS, GRAIN_URL, Grain, Eyebrow, Rule, Screen,
  RECIPE, SCAN_FIELDS, fmt, useLoop, useBrew, useInView,
});
