/* Overview "Market pressure": the Bitcoin pressure score (0-100) with its history, its rate of change
 * and the contribution of each component. Data comes from /api/pressure-score (src/pressure_store.js);
 * the score itself is defined in src/pressure_score.js. */

const { useState: useStatePS, useEffect: useEffectPS, useRef: useRefPS } = React;

const PS_RANGES = [{ id: '24h', label: '24h' }, { id: '7d', label: '7 days' }, { id: '30d', label: '30 days' }];
const PS_REFRESH_MS = 10 * 60 * 1000;
const PS_HORIZONS = [{ id: 0, label: 'off' }, { id: 4, label: '4h' }, { id: 8, label: '8h' }];

const psColour = (v) => (v >= 50 ? 'var(--bull)' : 'var(--bear)');
// Helpers shared with tests (public/src/overview-format.js, loaded as a plain script before this file).
const psFmt = () => window.OverviewFormat;
const psSigned = (v, d = 1) => (v == null ? '–' : (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v).toFixed(d));
const psUSD = (n) => '$' + Math.round(n).toLocaleString('en-US');
const psTime = (t, range) => {
  const d = new Date(t);
  return range === '24h'
    ? d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
    : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
};

function usePSWidth(def = 640) {
  const ref = useRefPS(null);
  const [w, setW] = useStatePS(def);
  useEffectPS(() => {
    const el = ref.current;
    if (!el) return undefined;
    const set = () => setW(Math.max(260, Math.floor(el.getBoundingClientRect().width)));
    set();
    if (typeof ResizeObserver === 'undefined') return undefined;
    const ro = new ResizeObserver(set);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  return [ref, w];
}

function psPath(vals, x, y) {
  return vals.map((v, i) => (i ? 'L' : 'M') + x(i).toFixed(1) + ' ' + y(v).toFixed(1)).join(' ');
}

// === Score + derivative chart (one SVG, two stacked panels, shared hover) ===
function PS_Chart({ series, range, showPrice, premium, horizon: horizonProp, paths }) {
  const [ref, W] = usePSWidth();
  const [hover, setHover] = useStatePS(null);
  const uid = React.useId().replace(/:/g, '');
  const n = series.length;
  const horizon = showPrice && horizonProp ? horizonProp : 0;
  if (n < 2) {
    return <div ref={ref} style={{ padding: '40px 0', textAlign: 'center', color: 'var(--text-3)', fontSize: 12.5 }}>Not enough history for this range yet.</div>;
  }
  // US demand panel: Coinbase premium (6h average, bp) matched to the score's hours
  const premAt = new Map((premium || []).map((p) => [p.ts, p.avg6h]));
  const prem = series.map((r) => (premAt.has(r.ts) ? premAt.get(r.ts) : null));
  const hasPrem = prem.filter((v) => v != null).length >= 2;
  const padL = 6, padR = 40, topH = 190, gap = 14, botH = 86, axisH = 20, premH = hasPrem ? 64 : 0, premGap = hasPrem ? 14 : 0;
  const H = topH + gap + botH + premGap + premH + axisH;
  const plotW = W - padL - padR;
  const slots = n - 1 + (horizon || 0);
  const x = (i) => padL + (i / slots) * plotW;
  const premTop = topH + gap + botH + premGap;
  const pMax = Math.max(1, ...prem.filter((v) => v != null).map(Math.abs));
  const yq = (v) => premTop + premH / 2 - (v / pMax) * (premH / 2 - 6);
  const premPath = prem.map((v, i) => (v == null ? '' : ((i && prem[i - 1] != null ? 'L' : 'M') + x(i).toFixed(1) + ' ' + yq(v).toFixed(1)))).join(' ');

  const sm = series.map((r) => r.smoothed), raw = series.map((r) => r.score);
  // Forecast from the latest hour (dotted, beyond "now") and, on hover, from the hovered past hour (same model, data up to that hour only).
  const PF = window.PressureForecast, prices = series.map((r) => r.price);
  const liveCone = horizon ? PF.priceCone(prices, horizon) : null;
  const hoverCone = horizon && hover != null && hover < n - 1 ? PF.priceCone(prices.slice(0, hover + 1), horizon) : null;
  // The server's lean toward the nearest support/resistance (src/price_path.js); without it the line stays flat.
  const byTs = new Map((paths || []).map((q) => [q.ts, q]));
  const livePath = horizon ? byTs.get(series[n - 1].ts) || null : null;
  const hoverPath = horizon && hover != null && hover < n - 1 ? byTs.get(series[hover].ts) || null : null;
  const midsOf = (path, cone) => (path ? path.mid.slice(0, cone.length) : cone.map((q) => q.mid));
  const conePrices = [liveCone, hoverCone].filter(Boolean).flatMap((c) => c.flatMap((q) => [q.lo, q.hi]));
  let lo = Math.min(...sm, ...raw, 42), hi = Math.max(...sm, ...raw, 58);
  lo = Math.max(0, Math.floor(lo / 5) * 5 - 2); hi = Math.min(100, Math.ceil(hi / 5) * 5 + 2);
  const y = (v) => 6 + (1 - (v - lo) / (hi - lo)) * (topH - 12);
  const y50 = y(50);
  const line = psPath(sm, x, y);
  // BTC price on its own scale (min..max of the visible range) so its shape can be compared with the score
  const pLo = Math.min(...prices, ...conePrices), pHi = Math.max(...prices, ...conePrices), pSpan = pHi - pLo || 1;
  const yp = (v) => 10 + (1 - (v - pLo) / pSpan) * (topH - 20);
  const pricePath = psPath(prices, x, yp);
  const area = `${line} L${x(n - 1).toFixed(1)} ${y50.toFixed(1)} L${x(0).toFixed(1)} ${y50.toFixed(1)} Z`;

  const dMax = Math.max(1, ...series.map((r) => Math.abs(r.delta)));
  const dMid = topH + gap + botH / 2, dScale = (botH / 2 - 6) / dMax;
  const bw = Math.max(1, plotW / (slots + 1) - (n > 200 ? 0 : 1));

  const ticks = [0, 0.25, 0.5, 0.75, 1].map((f) => Math.round(f * (n - 1)));
  const onMove = (e) => {
    const r = e.currentTarget.getBoundingClientRect();
    const i = Math.round(((e.clientX - r.left - padL) / plotW) * slots);
    setHover(Math.max(0, Math.min(n - 1, i)));
  };
  const hv = hover != null ? series[hover] : null;
  const fcPath = (i0, vals, yf, base) => [`M${x(i0).toFixed(1)} ${yf(base).toFixed(1)}`, ...vals.map((v, k) => `L${x(i0 + k + 1).toFixed(1)} ${yf(v).toFixed(1)}`)].join(' ');
  // A smooth curve (Catmull-Rom turned into cubic segments) through the hourly points, starting at the price.
  const smoothD = (i0, vals, base) => {
    const pts = [[x(i0), yp(base)], ...vals.map((v, k) => [x(i0 + k + 1), yp(v)])];
    const f = (v) => v.toFixed(1);
    let d = `M${f(pts[0][0])} ${f(pts[0][1])}`;
    for (let k = 0; k < pts.length - 1; k++) {
      const a = pts[Math.max(0, k - 1)], b = pts[k], c = pts[k + 1], e = pts[Math.min(pts.length - 1, k + 2)];
      d += ` C${f(b[0] + (c[0] - a[0]) / 6)} ${f(b[1] + (c[1] - a[1]) / 6)} ${f(c[0] - (e[0] - b[0]) / 6)} ${f(c[1] - (e[1] - b[1]) / 6)} ${f(c[0])} ${f(c[1])}`;
    }
    return d;
  };
  const conePoly = (i0, cone, base) => {
    const up = cone.map((q, k) => `${x(i0 + k + 1).toFixed(1)} ${yp(q.hi).toFixed(1)}`);
    const dn = cone.map((q, k) => `${x(i0 + k + 1).toFixed(1)} ${yp(q.lo).toFixed(1)}`).reverse();
    return `M${x(i0).toFixed(1)} ${yp(base).toFixed(1)} L${up.join(' L')} L${dn.join(' L')} Z`;
  };
  const FC = 'var(--text)';
  const tipW = 168, reach = x(horizon) - x(0) + 16;
  const tipLeft = hover != null ? (x(hover) + reach + tipW <= W - padR + 30 ? x(hover) + reach : Math.max(4, x(hover) - tipW - 16)) : 0;

  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <svg width={W} height={H} style={{ display: 'block' }} onMouseMove={onMove} onMouseLeave={() => setHover(null)}
           role="img" aria-label="Pressure score history and its hourly rate of change">
        <defs>
          <clipPath id={`up${uid}`}><rect x="0" y="0" width={W} height={y50} /></clipPath>
          <clipPath id={`dn${uid}`}><rect x="0" y={y50} width={W} height={topH - y50 + 6} /></clipPath>
        </defs>

        {/* score panel */}
        {[lo, 50, hi].filter((v, i, a) => a.indexOf(v) === i).map((v) => (
          <g key={v}>
            <line x1={padL} x2={W - padR} y1={y(v)} y2={y(v)} stroke={v === 50 ? 'var(--text-4)' : 'var(--border-soft)'} strokeDasharray={v === 50 ? '4 4' : undefined} strokeWidth="0.8" />
            <text x={W - padR + 6} y={y(v) + 3} fontSize="10" fontFamily="var(--font-mono)" fill={v === 50 ? 'var(--text-2)' : 'var(--text-4)'}>{v}</text>
          </g>
        ))}
        <path d={area} fill="var(--bull)" opacity="0.16" clipPath={`url(#up${uid})`} />
        <path d={area} fill="var(--bear)" opacity="0.16" clipPath={`url(#dn${uid})`} />
        <path d={psPath(raw, x, y)} fill="none" stroke="var(--text-3)" strokeWidth="0.8" opacity="0.5" />
        <path d={line} fill="none" stroke="var(--bull)" strokeWidth="2" strokeLinejoin="round" clipPath={`url(#up${uid})`} />
        <path d={line} fill="none" stroke="var(--bear)" strokeWidth="2" strokeLinejoin="round" clipPath={`url(#dn${uid})`} />
        <circle cx={x(n - 1)} cy={y(sm[n - 1])} r="3.5" fill={psColour(sm[n - 1])} />
        {showPrice && <path d={pricePath} fill="none" stroke="var(--gold, #e0b252)" strokeWidth="1.6" strokeLinejoin="round" />}
        {showPrice && <circle cx={x(n - 1)} cy={yp(prices[n - 1])} r="3" fill="var(--gold, #e0b252)" />}
        {horizon > 0 && <line x1={x(n - 1)} x2={x(n - 1)} y1="4" y2={topH - 4} stroke="var(--text-4)" strokeWidth="0.8" strokeDasharray="1 3" />}
        {liveCone && <path d={conePoly(n - 1, liveCone, prices[n - 1])} fill="var(--gold, #e0b252)" opacity="0.16" />}
        {liveCone && <path d={smoothD(n - 1, midsOf(livePath, liveCone), prices[n - 1])} fill="none" stroke={FC} strokeWidth="2.6" strokeDasharray="0.1 5.5" strokeLinecap="round" />}
        {hoverCone && <path d={conePoly(hover, hoverCone, prices[hover])} fill="var(--gold, #e0b252)" fillOpacity="0.24" stroke="var(--gold, #e0b252)" strokeWidth="1" strokeDasharray="2 3" />}
        {hoverCone && <path d={smoothD(hover, midsOf(hoverPath, hoverCone), prices[hover])} fill="none" stroke={FC} strokeWidth="3" strokeDasharray="0.1 5.5" strokeLinecap="round" />}
        {liveCone && <circle cx={x(n - 1 + liveCone.length)} cy={yp(midsOf(livePath, liveCone)[liveCone.length - 1])} r="3.2" fill="var(--bg-card)" stroke={FC} strokeWidth="1.6" />}
        {hoverCone && <circle cx={x(hover + hoverCone.length)} cy={yp(midsOf(hoverPath, hoverCone)[hoverCone.length - 1])} r="3.4" fill="var(--bg-card)" stroke={FC} strokeWidth="1.8" />}
        <text x={padL + 2} y="14" fontSize="9.5" letterSpacing="0.08em" fill="var(--text-4)">SCORE{showPrice ? ' · BTC PRICE (own scale)' : ''}</text>
        {horizon > 0 && <text x={W - padR - 2} y="14" textAnchor="end" fontSize="9.5" letterSpacing="0.08em" fill="var(--text-4)">DOTTED = LEAN TO NEAREST S/R · SHADED = USUAL RANGE</text>}
        {showPrice && <text x={padL + 2} y={topH - 2} fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--gold, #e0b252)">low {psUSD(pLo)} · high {psUSD(pHi)}</text>}

        {/* derivative panel */}
        <text x={padL + 2} y={topH + gap + 10} fontSize="9.5" letterSpacing="0.08em" fill="var(--text-4)">CHANGE PER HOUR</text>
        <line x1={padL} x2={W - padR} y1={dMid} y2={dMid} stroke="var(--text-4)" strokeWidth="0.8" />
        {series.map((r, i) => {
          const h = Math.abs(r.delta) * dScale;
          return <rect key={i} x={x(i) - bw / 2} y={r.delta >= 0 ? dMid - h : dMid} width={bw} height={Math.max(h, 0.5)}
                       fill={r.delta >= 0 ? 'var(--bull)' : 'var(--bear)'} opacity="0.75" />;
        })}
        <text x={W - padR + 6} y={dMid - botH / 2 + 12} fontSize="10" fontFamily="var(--font-mono)" fill="var(--text-4)">+{dMax.toFixed(1)}</text>
        <text x={W - padR + 6} y={dMid + botH / 2 - 4} fontSize="10" fontFamily="var(--font-mono)" fill="var(--text-4)">−{dMax.toFixed(1)}</text>

        {/* US demand panel */}
        {hasPrem && (
          <g>
            <text x={padL + 2} y={premTop + 10} fontSize="9.5" letterSpacing="0.08em" fill="var(--text-4)">US DEMAND · COINBASE PREMIUM, 6H AVG (BP)</text>
            <line x1={padL} x2={W - padR} y1={yq(0)} y2={yq(0)} stroke="var(--text-4)" strokeWidth="0.8" strokeDasharray="4 4" />
            <path d={premPath} fill="none" stroke="var(--info)" strokeWidth="1.5" strokeLinejoin="round" />
            <text x={W - padR + 6} y={premTop + 10} fontSize="10" fontFamily="var(--font-mono)" fill="var(--text-4)">+{pMax.toFixed(0)}</text>
            <text x={W - padR + 6} y={premTop + premH - 2} fontSize="10" fontFamily="var(--font-mono)" fill="var(--text-4)">−{pMax.toFixed(0)}</text>
          </g>
        )}

        {/* time axis */}
        {ticks.map((i, k) => (
          <text key={k} x={Math.min(Math.max(x(i), 24), W - padR - 24)} y={H - 5} textAnchor="middle" fontSize="9.5" fill="var(--text-4)">{psTime(series[i].ts, range)}</text>
        ))}

        {/* hover crosshair */}
        {hover != null && (
          <g pointerEvents="none">
            <line x1={x(hover)} x2={x(hover)} y1="4" y2={hasPrem ? premTop + premH : topH + gap + botH} stroke="var(--text-3)" strokeWidth="0.8" strokeDasharray="3 3" />
            <circle cx={x(hover)} cy={y(hv.smoothed)} r="4" fill={psColour(hv.smoothed)} stroke="var(--bg-card)" strokeWidth="1.5" />
            {showPrice && <circle cx={x(hover)} cy={yp(hv.price)} r="3.5" fill="var(--gold, #e0b252)" stroke="var(--bg-card)" strokeWidth="1.5" />}
          </g>
        )}
      </svg>
      {livePath && (
        <div style={{ marginTop: 4, fontSize: 11, color: 'var(--text-3)', lineHeight: 1.5 }}
             title={`RSI ${psSigned(livePath.parts && livePath.parts.rsi, 2)} · Bollinger ${psSigned(livePath.parts && livePath.parts.bands, 2)} · EMAs ${psSigned(livePath.parts && livePath.parts.emas, 2)} (each -1..+1; positive = stretched down, room to rise)`}>
          {livePath.kind
            ? <>BTC dotted line: leaning <b style={{ color: 'var(--text)' }}>{livePath.kind === 'resistance' ? 'up' : 'down'}</b> toward the nearest {livePath.kind}{livePath.target ? <> at <b className="mono" style={{ color: 'var(--text)' }}>{psUSD(livePath.target)}</b></> : ' (none nearby, so only as far as the usual range)'} · lean strength {Math.round(Math.abs(livePath.bias) * 100)}%</>
            : <>BTC dotted line: no lean right now, RSI, Bollinger Bands and the averages do not agree.</>}
        </div>
      )}
      {hv && (
        <div style={{ position: 'absolute', top: 8, left: tipLeft, width: 168, pointerEvents: 'none', background: 'color-mix(in srgb, var(--bg-elev) 72%, transparent)', backdropFilter: 'blur(2px)', border: '1px solid var(--border)',
                      borderRadius: 7, padding: '6px 9px', fontSize: 11, lineHeight: 1.55, boxShadow: '0 4px 14px rgba(0,0,0,0.35)' }}>
          <div style={{ color: 'var(--text-3)' }}>{new Date(hv.ts).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</div>
          <div>Score <b className="mono" style={{ color: psColour(hv.smoothed) }}>{hv.smoothed.toFixed(1)}</b> <span style={{ color: 'var(--text-4)' }}>(raw {hv.score.toFixed(1)})</span></div>
          <div>Change <b className="mono" style={{ color: hv.delta >= 0 ? 'var(--bull)' : 'var(--bear)' }}>{psSigned(hv.delta, 2)}</b>/h</div>
          <div>BTC <span className="mono">{psUSD(hv.price)}</span></div>
          {prem[hover] != null && <div>US demand <span className="mono">{psSigned(prem[hover], 1)} bp</span></div>}
          {hoverCone && (() => {
            const c = hoverCone[hoverCone.length - 1], at = hover + hoverCone.length, px = at <= n - 1 ? prices[at] : null;
            const end = midsOf(hoverPath, hoverCone)[hoverCone.length - 1], base = prices[hover];
            const lean = hoverPath && hoverPath.kind ? (hoverPath.kind === 'resistance' ? 'up toward resistance' : 'down toward support') + (hoverPath.target ? ' ' + psUSD(hoverPath.target) : '') : null;
            const right = px != null && lean ? (Math.sign(px - base) === Math.sign(end - base) ? ' · direction right' : ' · direction wrong') : '';
            return (
              <div style={{ marginTop: 3, paddingTop: 3, borderTop: '1px solid var(--border-soft)' }}>
                <div style={{ color: 'var(--text-3)' }}>From here, +{hoverCone.length}h</div>
                {hoverPath && <div>{lean ? 'Leaning ' + lean : 'No lean'}</div>}
                <div>Usual range <span className="mono">{psUSD(c.lo)}–{psUSD(c.hi)}</span></div>
                <div style={{ color: 'var(--text-4)' }}>{px != null ? 'it was ' + psUSD(px) + (px >= c.lo && px <= c.hi ? ' · inside' : ' · outside') + right : 'not yet known'}</div>
              </div>
            );
          })()}
        </div>
      )}
    </div>
  );
}

// === Horizontal pressure bar (0 .. 100, marker at the score) ===============
function PS_Bar({ value }) {
  return (
    <div style={{ marginTop: 14 }}>
      <div style={{ position: 'relative', height: 10, borderRadius: 5, background: 'linear-gradient(90deg, var(--bear), var(--bear-dim) 30%, var(--gold-dim) 50%, var(--bull-dim) 70%, var(--bull))' }}>
        <div style={{ position: 'absolute', left: '50%', top: -3, bottom: -3, width: 1, background: 'var(--text-3)' }} />
        <div style={{ position: 'absolute', top: -4, width: 4, height: 18, borderRadius: 2, background: 'var(--text)', left: `calc(${Math.max(0, Math.min(100, value))}% - 2px)`, boxShadow: '0 0 0 2px var(--bg-card)', transition: 'left 0.5s ease' }} />
      </div>
      <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 9.5, letterSpacing: '0.07em', color: 'var(--text-4)', marginTop: 8 }}>
        <span>0 · NEGATIVE</span><span>50</span><span>POSITIVE · 100</span>
      </div>
    </div>
  );
}

// === Component breakdown card ==============================================
function PS_Component({ b }) {
  const colour = b.value >= 50 ? 'var(--bull)' : 'var(--bear)';
  const half = Math.abs(b.value - 50);
  return (
    <div style={{ background: 'var(--bg-card-2)', border: '1px solid var(--border-soft)', borderRadius: 8, padding: '10px 12px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 6 }}>
        <span style={{ fontSize: 11.5, fontWeight: 500 }}>{b.label}</span>
        <span style={{ fontSize: 10, color: 'var(--text-4)' }}>{b.weight}% weight</span>
      </div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginTop: 4 }}>
        <span className="mono" style={{ fontSize: 20, fontWeight: 600, color: colour }}>{b.value.toFixed(0)}</span>
        <span className="mono" style={{ fontSize: 11.5, color: b.contribution >= 0 ? 'var(--bull)' : 'var(--bear)' }}>{psSigned(b.contribution, 2)} pts</span>
      </div>
      <div style={{ position: 'relative', height: 5, background: 'var(--bg-elev)', borderRadius: 3, marginTop: 7 }} title={`${b.label}: ${b.value} out of 100`}>
        <div style={{ position: 'absolute', left: '50%', top: -2, bottom: -2, width: 1, background: 'var(--border-strong)' }} />
        <div style={{ position: 'absolute', top: 0, bottom: 0, borderRadius: 3, background: colour, left: b.value >= 50 ? '50%' : `${b.value}%`, width: `${half}%`, transition: 'all 0.4s ease' }} />
      </div>
    </div>
  );
}

// === The section ===========================================================
function W_PressureScore() {
  const [range, setRange] = useStatePS('7d');
  const [showPrice, setShowPrice] = useStatePS(() => {
    try { return localStorage.getItem('ps-show-price') !== '0'; } catch (e) { return true; }
  });
  const togglePrice = () => setShowPrice((v) => {
    try { localStorage.setItem('ps-show-price', v ? '0' : '1'); } catch (e) { /* ignore */ }
    return !v;
  });
  const [horizon, setHorizon] = useStatePS(() => {
    try { const raw = localStorage.getItem('ps-forecast-h'), v = Number(raw); return raw != null && [0, 4, 8].includes(v) ? v : 4; } catch (e) { return 4; }
  });
  const pickHorizon = (h) => { setHorizon(h); try { localStorage.setItem('ps-forecast-h', String(h)); } catch (e) { /* ignore */ } };
  const [state, setState] = useStatePS({ data: null, error: null, loading: true });

  useEffectPS(() => {
    let cancelled = false, timer;
    const load = () => {
      fetch('/api/pressure-score?range=' + range)
        .then((r) => r.json())
        .then((j) => { if (!cancelled) setState({ data: j.ok ? j.data : null, error: j.ok ? null : (j.error || 'unavailable'), loading: false }); })
        .catch(() => { if (!cancelled) setState((s) => ({ ...s, error: 'unavailable', loading: false })); })
        .finally(() => { if (!cancelled) timer = setTimeout(load, PS_REFRESH_MS); });
    };
    setState((s) => ({ ...s, loading: true }));
    load();
    return () => { cancelled = true; clearTimeout(timer); };
  }, [range]);

  const d = state.data, cur = d && d.current;
  const colour = cur ? psColour(cur.smoothed) : 'var(--text-3)';
  const rising = cur && cur.delta > 0.05, falling = cur && cur.delta < -0.05;

  return (
    <div className="card" style={{ gridColumn: 'span 12' }}>
      <div className="card-h" style={{ flexWrap: 'wrap', gap: 10 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
          <h3 style={{ margin: 0 }}>Market pressure · BTC</h3>
          {cur && cur.stale && <span className="chip bear"><span className="chip-dot" /> data is stale</span>}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          {cur && <span style={{ fontSize: 10.5, color: 'var(--text-4)' }}>1h candles · last closed {new Date(psFmt().pressureCloseTs(cur)).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>}
          <label style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 11.5, color: 'var(--text-3)', cursor: 'pointer' }}>
            <input type="checkbox" checked={showPrice} onChange={togglePrice} />
            <span style={{ color: 'var(--gold, #e0b252)' }}>━</span> BTC price
          </label>
          <div role="tablist" aria-label="Forecast horizon" title="Dotted BTC line leaning toward the nearest support or resistance (RSI, Bollinger Bands and EMAs decide which), inside a shaded one-sigma range" style={{ display: 'inline-flex', alignItems: 'center', border: '1px solid var(--border)', borderRadius: 7, overflow: 'hidden' }}>
            <span style={{ padding: '4px 8px', fontSize: 11, color: 'var(--text-3)' }}>Forecast</span>
            {PS_HORIZONS.map((r) => (
              <button key={r.id} role="tab" aria-selected={horizon === r.id} onClick={() => pickHorizon(r.id)}
                style={{ padding: '4px 9px', fontSize: 11.5, border: 'none', cursor: 'pointer', fontFamily: 'var(--font-mono)',
                         background: horizon === r.id ? 'var(--bg-elev)' : 'transparent', color: horizon === r.id ? 'var(--text)' : 'var(--text-3)' }}>
                {r.label}
              </button>
            ))}
          </div>
          <div role="tablist" aria-label="Range" style={{ display: 'inline-flex', border: '1px solid var(--border)', borderRadius: 7, overflow: 'hidden' }}>
            {PS_RANGES.map((r) => (
              <button key={r.id} role="tab" aria-selected={range === r.id} onClick={() => setRange(r.id)}
                style={{ padding: '4px 11px', fontSize: 11.5, border: 'none', cursor: 'pointer', fontFamily: 'var(--font-mono)',
                         background: range === r.id ? 'var(--bg-elev)' : 'transparent', color: range === r.id ? 'var(--text)' : 'var(--text-3)' }}>
                {r.label}
              </button>
            ))}
          </div>
        </div>
      </div>

      <div className="card-b">
        {!cur && (
          <div style={{ padding: '28px 0', textAlign: 'center', color: 'var(--text-3)', fontSize: 12.5, lineHeight: 1.6 }}>
            {state.loading ? 'Loading the pressure score…'
              : state.error ? 'The pressure score is unavailable right now. It will retry shortly.'
              : 'Collecting data. The first scores are calculated shortly after the server starts (it backfills 30 days of hourly history).'}
          </div>
        )}

        {cur && (
          <>
            <div className="rg-split" style={{ display: 'grid', gridTemplateColumns: 'minmax(230px, 290px) 1fr', gap: 24, alignItems: 'center' }}>
              <div>
                <div style={{ display: 'flex', alignItems: 'baseline', gap: 10 }}>
                  <span className="mono" style={{ fontSize: 68, lineHeight: 1, fontWeight: 600, color: colour, letterSpacing: '-0.02em' }} aria-label={`Pressure score ${cur.smoothed.toFixed(1)} out of 100`}>{cur.smoothed.toFixed(1)}</span>
                  <span style={{ fontSize: 13, color: 'var(--text-3)' }}>/ 100</span>
                </div>
                <div style={{ marginTop: 8, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                  <span className={'chip ' + (cur.smoothed >= 50 ? 'bull' : 'bear')} style={{ fontSize: 12 }}><span className="chip-dot" /> {cur.label}</span>
                  <span style={{ fontSize: 12.5, color: rising ? 'var(--bull)' : falling ? 'var(--bear)' : 'var(--text-3)', fontFamily: 'var(--font-mono)' }}>
                    {rising ? '▲' : falling ? '▼' : '►'} {psSigned(cur.delta, 2)}/h
                  </span>
                </div>
                <div style={{ display: 'flex', gap: 16, marginTop: 10, fontSize: 11.5, color: 'var(--text-3)', flexWrap: 'wrap' }}>
                  <span>6h <b className="mono" style={{ color: psFmt().signedColour(cur.change6h) }}>{psSigned(cur.change6h)}</b></span>
                  <span>24h <b className="mono" style={{ color: psFmt().signedColour(cur.change24h) }}>{psSigned(cur.change24h)}</b></span>
                  <span>raw <b className="mono" style={{ color: 'var(--text-2)' }}>{cur.score.toFixed(1)}</b></span>
                  <span>BTC <b className="mono" style={{ color: 'var(--text-2)' }}>{psUSD(cur.price)}</b></span>
                </div>
                <PS_Bar value={cur.smoothed} />
              </div>
              <PS_Chart series={d.series} range={d.range} showPrice={showPrice} premium={d.premium} horizon={horizon} paths={d.paths} />
            </div>

            <div style={{ marginTop: 18, display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
              <span style={{ fontSize: 10.5, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-3)' }}>What is driving the score</span>
              <span style={{ fontSize: 10.5, color: 'var(--text-4)' }}>
                raw score <b className="mono" style={{ color: 'var(--text-2)' }}>{cur.score.toFixed(1)}</b> = 50 + sum of the points below · the headline <span className="mono">{cur.smoothed.toFixed(1)}</span> is the raw score smoothed over about 3 hours
              </span>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))', gap: 10, marginTop: 8 }}>
              {cur.breakdown.map((b) => <PS_Component key={b.id} b={b} />)}
            </div>

            <div style={{ marginTop: 12, fontSize: 10.5, color: 'var(--text-4)', lineHeight: 1.5 }}>
              Above 50 = positive pressure, below 50 = negative; the further from 50, the stronger. A weighted blend of price structure, moving averages, RSI, Bollinger Bands, relative volume and MACD momentum on closed 1h Binance candles, lightly smoothed, saved once an hour. The score describes current pressure. The dotted BTC line leans toward the nearest swing support or resistance, in the direction RSI, Bollinger Bands and the EMA20/50 point to (low RSI, lower band and price under the averages lean up), and is eased in so it never passes the level or the usual range. It is a fixed rule, not a fitted model. Over a year of hourly data it pointed the right way 52-53% of the time, steady in every quarter, but the average move in that direction was close to zero: a mild lean, not an edge. The shaded band is the usual one-sigma range. Not financial advice.
            </div>
          </>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { W_PressureScore });
