/* portfolio.jsx — combined nnpg portfolio, exported for DC x-import */



// tweaks-panel.jsx
// Reusable Tweaks shell + form-control helpers.
//
// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode,
// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so
// individual prototypes don't re-roll it. Ships a consistent set of controls so you
// don't hand-draw <input type="range">, segmented radios, steppers, etc.
//
// Usage (in an HTML file that loads React + Babel):
//
//   const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
//     "primaryColor": "#D97757",
//     "palette": ["#D97757", "#29261b", "#f6f4ef"],
//     "fontSize": 16,
//     "density": "regular",
//     "dark": false
//   }/*EDITMODE-END*/;
//
//   function App() {
//     const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
//     return (
//       <div style={{ fontSize: t.fontSize, color: t.primaryColor }}>
//         Hello
//         <TweaksPanel>
//           <TweakSection label="Typography" />
//           <TweakSlider label="Font size" value={t.fontSize} min={10} max={32} unit="px"
//                        onChange={(v) => setTweak('fontSize', v)} />
//           <TweakRadio  label="Density" value={t.density}
//                        options={['compact', 'regular', 'comfy']}
//                        onChange={(v) => setTweak('density', v)} />
//           <TweakSection label="Theme" />
//           <TweakColor  label="Primary" value={t.primaryColor}
//                        options={['#D97757', '#2A6FDB', '#1F8A5B', '#7A5AE0']}
//                        onChange={(v) => setTweak('primaryColor', v)} />
//           <TweakColor  label="Palette" value={t.palette}
//                        options={[['#D97757', '#29261b', '#f6f4ef'],
//                                  ['#475569', '#0f172a', '#f1f5f9']]}
//                        onChange={(v) => setTweak('palette', v)} />
//           <TweakToggle label="Dark mode" value={t.dark}
//                        onChange={(v) => setTweak('dark', v)} />
//         </TweaksPanel>
//       </div>
//     );
//   }
//
// ─────────────────────────────────────────────────────────────────────────────

const __TWEAKS_STYLE = `
  .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
    max-height:calc(100vh - 32px);display:flex;flex-direction:column;
    transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right;
    background:rgba(250,249,247,.78);color:#29261b;
    -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
    border:.5px solid rgba(255,255,255,.6);border-radius:14px;
    box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
    font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
  .twk-hd{display:flex;align-items:center;justify-content:space-between;
    padding:10px 8px 10px 14px;cursor:move;user-select:none}
  .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
  .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
    width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
  .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
  .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
    overflow-y:auto;overflow-x:hidden;min-height:0;
    scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
  .twk-body::-webkit-scrollbar{width:8px}
  .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
  .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
    border:2px solid transparent;background-clip:content-box}
  .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
    border:2px solid transparent;background-clip:content-box}
  .twk-row{display:flex;flex-direction:column;gap:5px}
  .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
  .twk-lbl{display:flex;justify-content:space-between;align-items:baseline;
    color:rgba(41,38,27,.72)}
  .twk-lbl>span:first-child{font-weight:500}
  .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}

  .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
    color:rgba(41,38,27,.45);padding:10px 0 0}
  .twk-sect:first-child{padding-top:0}

  .twk-field{appearance:none;width:100%;height:26px;padding:0 8px;
    border:.5px solid rgba(0,0,0,.1);border-radius:7px;
    background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
  .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
  select.twk-field{padding-right:22px;
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='rgba(0,0,0,.5)' d='M0 0h10L5 6z'/></svg>");
    background-repeat:no-repeat;background-position:right 8px center}

  .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
    border-radius:999px;background:rgba(0,0,0,.12);outline:none}
  .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
    width:14px;height:14px;border-radius:50%;background:#fff;
    border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
  .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
    background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}

  .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
    background:rgba(0,0,0,.06);user-select:none}
  .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
    background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
    transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
  .twk-seg.dragging .twk-seg-thumb{transition:none}
  .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;
    background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;
    border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
    overflow-wrap:anywhere}

  .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
    background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
  .twk-toggle[data-on="1"]{background:#34c759}
  .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
    background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
  .twk-toggle[data-on="1"] i{transform:translateX(14px)}

  .twk-num{display:flex;align-items:center;height:26px;padding:0 0 0 8px;
    border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
  .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize;
    user-select:none;padding-right:8px}
  .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;
    font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0;
    outline:none;color:inherit;-moz-appearance:textfield}
  .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{
    -webkit-appearance:none;margin:0}
  .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)}

  .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
    background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
  .twk-btn:hover{background:rgba(0,0,0,.88)}
  .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
  .twk-btn.secondary:hover{background:rgba(0,0,0,.1)}

  .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
    border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
    background:transparent;flex-shrink:0}
  .twk-swatch::-webkit-color-swatch-wrapper{padding:0}
  .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
  .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}

  .twk-chips{display:flex;gap:6px}
  .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px;
    padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default;
    box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06);
    transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s}
  .twk-chip:hover{transform:translateY(-1px);
    box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)}
  .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85),
    0 2px 6px rgba(0,0,0,.15)}
  .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%;
    display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)}
  .twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)}
  .twk-chip>span>i:first-child{box-shadow:none}
  .twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px;
    filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))}
`;

// ── useTweaks ───────────────────────────────────────────────────────────────
// Single source of truth for tweak values. setTweak persists via the host
// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk).
function useTweaks(defaults) {
  const [values, setValues] = React.useState(defaults);
  // Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a
  // useState-style call doesn't write a "[object Object]" key into the persisted
  // JSON block.
  const setTweak = React.useCallback((keyOrEdits, val) => {
    const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null
      ? keyOrEdits : { [keyOrEdits]: val };
    setValues((prev) => ({ ...prev, ...edits }));
    window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*');
    // Same-window signal so in-page listeners (deck-stage rail thumbnails)
    // can react — the parent message only reaches the host, not peers.
    window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits }));
  }, []);
  return [values, setTweak];
}

// ── TweaksPanel ─────────────────────────────────────────────────────────────
// Floating shell. Registers the protocol listener BEFORE announcing
// availability — if the announce ran first, the host's activate could land
// before our handler exists and the toolbar toggle would silently no-op.
// The close button posts __edit_mode_dismissed so the host's toolbar toggle
// flips off in lockstep; the host echoes __deactivate_edit_mode back which
// is what actually hides the panel.
function TweaksPanel({ title = 'Tweaks', noDeckControls = false, children }) {
  const [open, setOpen] = React.useState(false);
  const dragRef = React.useRef(null);
  // Auto-inject a rail toggle when a <deck-stage> is on the page. The
  // toggle drives the deck's per-viewer _railVisible via window message;
  // state is mirrored from the same localStorage key the deck reads so
  // the control reflects reality across reloads. The mechanism is the
  // message — authors who want custom placement can post it directly
  // and pass noDeckControls to suppress this one.
  const hasDeckStage = React.useMemo(
    () => typeof document !== 'undefined' && !!document.querySelector('deck-stage'),
    [],
  );
  // Hide the toggle until the host has actually enabled the rail (the
  // __omelette_rail_enabled window message, posted only when the
  // omelette_deck_rail_enabled flag is on for this user). The initial read
  // covers TweaksPanel mounting after the message already arrived; the
  // listener covers the common case of mounting first.
  const [railEnabled, setRailEnabled] = React.useState(
    () => hasDeckStage && !!document.querySelector('deck-stage')?._railEnabled,
  );
  React.useEffect(() => {
    if (!hasDeckStage || railEnabled) return undefined;
    const onMsg = (e) => {
      if (e.data && e.data.type === '__omelette_rail_enabled') setRailEnabled(true);
    };
    window.addEventListener('message', onMsg);
    return () => window.removeEventListener('message', onMsg);
  }, [hasDeckStage, railEnabled]);
  const [railVisible, setRailVisible] = React.useState(() => {
    try { return localStorage.getItem('deck-stage.railVisible') !== '0'; } catch (e) { return true; }
  });
  const toggleRail = (on) => {
    setRailVisible(on);
    window.postMessage({ type: '__deck_rail_visible', on }, '*');
  };
  const offsetRef = React.useRef({ x: 16, y: 16 });
  const PAD = 16;

  const clampToViewport = React.useCallback(() => {
    const panel = dragRef.current;
    if (!panel) return;
    const w = panel.offsetWidth, h = panel.offsetHeight;
    const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
    const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
    offsetRef.current = {
      x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
      y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)),
    };
    panel.style.right = offsetRef.current.x + 'px';
    panel.style.bottom = offsetRef.current.y + 'px';
  }, []);

  React.useEffect(() => {
    if (!open) return;
    clampToViewport();
    if (typeof ResizeObserver === 'undefined') {
      window.addEventListener('resize', clampToViewport);
      return () => window.removeEventListener('resize', clampToViewport);
    }
    const ro = new ResizeObserver(clampToViewport);
    ro.observe(document.documentElement);
    return () => ro.disconnect();
  }, [open, clampToViewport]);

  React.useEffect(() => {
    const onMsg = (e) => {
      const t = e?.data?.type;
      if (t === '__activate_edit_mode') setOpen(true);
      else if (t === '__deactivate_edit_mode') setOpen(false);
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({ type: '__edit_mode_available' }, '*');
    return () => window.removeEventListener('message', onMsg);
  }, []);

  const dismiss = () => {
    setOpen(false);
    window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*');
  };

  const onDragStart = (e) => {
    const panel = dragRef.current;
    if (!panel) return;
    const r = panel.getBoundingClientRect();
    const sx = e.clientX, sy = e.clientY;
    const startRight = window.innerWidth - r.right;
    const startBottom = window.innerHeight - r.bottom;
    const move = (ev) => {
      offsetRef.current = {
        x: startRight - (ev.clientX - sx),
        y: startBottom - (ev.clientY - sy),
      };
      clampToViewport();
    };
    const up = () => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
    };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
  };

  if (!open) return null;
  return (
    <>
      <style>{__TWEAKS_STYLE}</style>
      <div ref={dragRef} className="twk-panel" data-noncommentable=""
           style={{ right: offsetRef.current.x, bottom: offsetRef.current.y }}>
        <div className="twk-hd" onMouseDown={onDragStart}>
          <b>{title}</b>
          <button className="twk-x" aria-label="Close tweaks"
                  onMouseDown={(e) => e.stopPropagation()}
                  onClick={dismiss}>✕</button>
        </div>
        <div className="twk-body">
          {children}
          {hasDeckStage && railEnabled && !noDeckControls && (
            <TweakSection label="Deck">
              <TweakToggle label="Thumbnail rail" value={railVisible} onChange={toggleRail} />
            </TweakSection>
          )}
        </div>
      </div>
    </>
  );
}

// ── Layout helpers ──────────────────────────────────────────────────────────

function TweakSection({ label, children }) {
  return (
    <>
      <div className="twk-sect">{label}</div>
      {children}
    </>
  );
}

function TweakRow({ label, value, children, inline = false }) {
  return (
    <div className={inline ? 'twk-row twk-row-h' : 'twk-row'}>
      <div className="twk-lbl">
        <span>{label}</span>
        {value != null && <span className="twk-val">{value}</span>}
      </div>
      {children}
    </div>
  );
}

// ── Controls ────────────────────────────────────────────────────────────────

function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) {
  return (
    <TweakRow label={label} value={`${value}${unit}`}>
      <input type="range" className="twk-slider" min={min} max={max} step={step}
             value={value} onChange={(e) => onChange(Number(e.target.value))} />
    </TweakRow>
  );
}

function TweakToggle({ label, value, onChange }) {
  return (
    <div className="twk-row twk-row-h">
      <div className="twk-lbl"><span>{label}</span></div>
      <button type="button" className="twk-toggle" data-on={value ? '1' : '0'}
              role="switch" aria-checked={!!value}
              onClick={() => onChange(!value)}><i /></button>
    </div>
  );
}

function TweakRadio({ label, value, options, onChange }) {
  const trackRef = React.useRef(null);
  const [dragging, setDragging] = React.useState(false);
  // The active value is read by pointer-move handlers attached for the lifetime
  // of a drag — ref it so a stale closure doesn't fire onChange for every move.
  const valueRef = React.useRef(value);
  valueRef.current = value;

  // Segments wrap mid-word once per-segment width runs out. The track is
  // ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px
  // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2
  // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall
  // back to a dropdown rather than wrap.
  const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length;
  const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0);
  const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0);
  if (!fitsAsSegments) {
    // <select> emits strings — map back to the original option value so the
    // fallback stays type-preserving (numbers, booleans) like the segment path.
    const resolve = (s) => {
      const m = options.find((o) => String(typeof o === 'object' ? o.value : o) === s);
      return m === undefined ? s : typeof m === 'object' ? m.value : m;
    };
    return <TweakSelect label={label} value={value} options={options}
                        onChange={(s) => onChange(resolve(s))} />;
  }
  const opts = options.map((o) => (typeof o === 'object' ? o : { value: o, label: o }));
  const idx = Math.max(0, opts.findIndex((o) => o.value === value));
  const n = opts.length;

  const segAt = (clientX) => {
    const r = trackRef.current.getBoundingClientRect();
    const inner = r.width - 4;
    const i = Math.floor(((clientX - r.left - 2) / inner) * n);
    return opts[Math.max(0, Math.min(n - 1, i))].value;
  };

  const onPointerDown = (e) => {
    setDragging(true);
    const v0 = segAt(e.clientX);
    if (v0 !== valueRef.current) onChange(v0);
    const move = (ev) => {
      if (!trackRef.current) return;
      const v = segAt(ev.clientX);
      if (v !== valueRef.current) onChange(v);
    };
    const up = () => {
      setDragging(false);
      window.removeEventListener('pointermove', move);
      window.removeEventListener('pointerup', up);
    };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
  };

  return (
    <TweakRow label={label}>
      <div ref={trackRef} role="radiogroup" onPointerDown={onPointerDown}
           className={dragging ? 'twk-seg dragging' : 'twk-seg'}>
        <div className="twk-seg-thumb"
             style={{ left: `calc(2px + ${idx} * (100% - 4px) / ${n})`,
                      width: `calc((100% - 4px) / ${n})` }} />
        {opts.map((o) => (
          <button key={o.value} type="button" role="radio" aria-checked={o.value === value}>
            {o.label}
          </button>
        ))}
      </div>
    </TweakRow>
  );
}

function TweakSelect({ label, value, options, onChange }) {
  return (
    <TweakRow label={label}>
      <select className="twk-field" value={value} onChange={(e) => onChange(e.target.value)}>
        {options.map((o) => {
          const v = typeof o === 'object' ? o.value : o;
          const l = typeof o === 'object' ? o.label : o;
          return <option key={v} value={v}>{l}</option>;
        })}
      </select>
    </TweakRow>
  );
}

function TweakText({ label, value, placeholder, onChange }) {
  return (
    <TweakRow label={label}>
      <input className="twk-field" type="text" value={value} placeholder={placeholder}
             onChange={(e) => onChange(e.target.value)} />
    </TweakRow>
  );
}

function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) {
  const clamp = (n) => {
    if (min != null && n < min) return min;
    if (max != null && n > max) return max;
    return n;
  };
  const startRef = React.useRef({ x: 0, val: 0 });
  const onScrubStart = (e) => {
    e.preventDefault();
    startRef.current = { x: e.clientX, val: value };
    const decimals = (String(step).split('.')[1] || '').length;
    const move = (ev) => {
      const dx = ev.clientX - startRef.current.x;
      const raw = startRef.current.val + dx * step;
      const snapped = Math.round(raw / step) * step;
      onChange(clamp(Number(snapped.toFixed(decimals))));
    };
    const up = () => {
      window.removeEventListener('pointermove', move);
      window.removeEventListener('pointerup', up);
    };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
  };
  return (
    <div className="twk-num">
      <span className="twk-num-lbl" onPointerDown={onScrubStart}>{label}</span>
      <input type="number" value={value} min={min} max={max} step={step}
             onChange={(e) => onChange(clamp(Number(e.target.value)))} />
      {unit && <span className="twk-num-unit">{unit}</span>}
    </div>
  );
}

// Relative-luminance contrast pick — checkmarks drawn over a swatch need to
// read on both #111 and #fafafa without per-option configuration. Hex input
// only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light".
function __twkIsLight(hex) {
  const h = String(hex).replace('#', '');
  const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, '0');
  const n = parseInt(x.slice(0, 6), 16);
  if (Number.isNaN(n)) return true;
  const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
  return r * 299 + g * 587 + b * 114 > 148000;
}

const __TwkCheck = ({ light }) => (
  <svg viewBox="0 0 14 14" aria-hidden="true">
    <path d="M3 7.2 5.8 10 11 4.2" fill="none" strokeWidth="2.2"
          strokeLinecap="round" strokeLinejoin="round"
          stroke={light ? 'rgba(0,0,0,.78)' : '#fff'} />
  </svg>
);

// TweakColor — curated color/palette picker. Each option is either a single
// hex string or an array of 1-5 hex strings; the card adapts — a lone color
// renders solid, a palette renders colors[0] as the hero (left ~2/3) with the
// rest stacked in a sharp column on the right. onChange emits the
// option in the shape it was passed (string stays string, array stays array).
// Without options it falls back to the native color input for back-compat.
function TweakColor({ label, value, options, onChange }) {
  if (!options || !options.length) {
    return (
      <div className="twk-row twk-row-h">
        <div className="twk-lbl"><span>{label}</span></div>
        <input type="color" className="twk-swatch" value={value}
               onChange={(e) => onChange(e.target.value)} />
      </div>
    );
  }
  // Native <input type=color> emits lowercase hex per the HTML spec, so
  // compare case-insensitively. String() guards JSON.stringify(undefined),
  // which returns the primitive undefined (no .toLowerCase).
  const key = (o) => String(JSON.stringify(o)).toLowerCase();
  const cur = key(value);
  return (
    <TweakRow label={label}>
      <div className="twk-chips" role="radiogroup">
        {options.map((o, i) => {
          const colors = Array.isArray(o) ? o : [o];
          const [hero, ...rest] = colors;
          const sup = rest.slice(0, 4);
          const on = key(o) === cur;
          return (
            <button key={i} type="button" className="twk-chip" role="radio"
                    aria-checked={on} data-on={on ? '1' : '0'}
                    aria-label={colors.join(', ')} title={colors.join(' · ')}
                    style={{ background: hero }}
                    onClick={() => onChange(o)}>
              {sup.length > 0 && (
                <span>
                  {sup.map((c, j) => <i key={j} style={{ background: c }} />)}
                </span>
              )}
              {on && <__TwkCheck light={__twkIsLight(hero)} />}
            </button>
          );
        })}
      </div>
    </TweakRow>
  );
}

function TweakButton({ label, onClick, secondary = false }) {
  return (
    <button type="button" className={secondary ? 'twk-btn secondary' : 'twk-btn'}
            onClick={onClick}>{label}</button>
  );
}

Object.assign(window, {
  useTweaks, TweaksPanel, TweakSection, TweakRow,
  TweakSlider, TweakToggle, TweakRadio, TweakSelect,
  TweakText, TweakNumber, TweakColor, TweakButton,
});


/* components.jsx — shared building blocks for the nnpg portfolio */

const NNPG_ASCII = `
  ███╗   ██╗███╗   ██╗██████╗  ██████╗
  ████╗  ██║████╗  ██║██╔══██╗██╔════╝
  ██╔██╗ ██║██╔██╗ ██║██████╔╝██║  ███╗
  ██║╚██╗██║██║╚██╗██║██╔═══╝ ██║   ██║
  ██║ ╚████║██║ ╚████║██║     ╚██████╔╝
  ╚═╝  ╚═══╝╚═╝  ╚═══╝╚═╝      ╚═════╝
`.replace(/^\n/, '');

// ── Pixel avatar (16x16 hand-coded grid; "N" glyph + green frame) ─────────
function PixelAvatar() {
  // Lay out a 16×16 mosaic where the green tones suggest a Minecraft-grass feel
  // and a stylized N glyph reads through. Stored as rows of digits 0..3.
  const rows = [
    "1111111111111111",
    "1222222222222221",
    "1233333333333321",
    "1230000000000321",
    "1230322000003321",
    "1230332200023321",
    "1230333220023321",
    "1230322322023321",
    "1230322232023321",
    "1230322223023321",
    "1230322220323321",
    "1230322220033321",
    "1230000000000321",
    "1233333333333321",
    "1222222222222221",
    "1111111111111111",
  ];
  const palette = {
    "0": "#0c1410",
    "1": "rgba(109,255,122,0.55)",
    "2": "rgba(109,255,122,0.18)",
    "3": "#0e1a13",
  };
  return (
    <svg viewBox="0 0 16 16" shapeRendering="crispEdges" preserveAspectRatio="none">
      {rows.map((r, y) =>
        r.split('').map((c, x) => (
          <rect key={`${x}-${y}`} x={x} y={y} width="1" height="1" fill={palette[c]} />
        ))
      )}
      <text x="8" y="11" textAnchor="middle" fontSize="7" fontFamily="VT323, monospace"
            fill="#6dff7a" style={{letterSpacing: 0}}>N</text>
    </svg>
  );
}

// ── ASCII hero block ──────────────────────────────────────────────────────
function AsciiHero({ tight = false }) {
  return <pre className={"ascii" + (tight ? " tight" : "")}>{NNPG_ASCII}</pre>;
}

// ── Animated counter ──────────────────────────────────────────────────────
function Counter({ to, suffix = "", duration = 1400 }) {
  const [n, setN] = React.useState(0);
  React.useEffect(() => {
    let raf;
    const start = performance.now();
    const step = (t) => {
      const p = Math.min(1, (t - start) / duration);
      const eased = 1 - Math.pow(1 - p, 3);
      setN(Math.floor(eased * to));
      if (p < 1) raf = requestAnimationFrame(step);
      else setN(to);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [to, duration]);
  const fmt = n >= 1000 ? `${(n/1000).toFixed(n >= 10000 ? 0 : 1)}k` : n;
  return <span>{fmt}{suffix}</span>;
}

// ── Live clock (HH:MM:SS UTC) ─────────────────────────────────────────────
function Clock() {
  const [t, setT] = React.useState(() => new Date());
  React.useEffect(() => {
    const id = setInterval(() => setT(new Date()), 1000);
    return () => clearInterval(id);
  }, []);
  const pad = (x) => String(x).padStart(2, '0');
  return <span>{pad(t.getUTCHours())}:{pad(t.getUTCMinutes())}:{pad(t.getUTCSeconds())} UTC</span>;
}

// ── Section divider ───────────────────────────────────────────────────────
function SectionRule({ num, label }) {
  return (
    <div className="section-rule">
      <span className="num">{num}</span>
      <span>{label}</span>
    </div>
  );
}

// ── Panel head ────────────────────────────────────────────────────────────
function PanelHead({ tag, title, badge, right }) {
  return (
    <div className="panel-head">
      <span className="tag">{tag}</span>
      <span style={{color: "var(--fg-dim)"}}>{title}</span>
      {badge && <span className="badge">{badge}</span>}
      <span className="right">{right}</span>
    </div>
  );
}

// ── Minecraft block (decorative — 8x8 grid of i tiles) ────────────────────
function McBlock() {
  return (
    <div className="mc-block" aria-hidden="true">
      {Array.from({length: 64}).map((_, i) => <i key={i} />)}
    </div>
  );
}

// ── Now-playing card (mock — animated progress bar) ───────────────────────
function NowPlaying() {
  const TRACKS = [
    { track: "What's the Difference", artist: "Dr. Dre · 2001", len: 244, art: "art/whats-the-difference.png", slot: "npcover-difference" },
    { track: "Houdini", artist: "Eminem · The Death of Slim Shady", len: 227, art: "art/houdini.png", slot: "npcover-houdini" },
    { track: "Gangsta's Paradise", artist: "Coolio ft. L.V.", len: 240, art: "art/gangstas-paradise.png", slot: "npcover-gangstas" },
  ];
  const [idx, setIdx] = React.useState(0);
  const [pos, setPos] = React.useState(0);
  const cur = TRACKS[idx];

  React.useEffect(() => {
    const id = setInterval(() => {
      setPos((p) => {
        if (p + 1 >= cur.len) {
          setIdx((i) => (i + 1) % TRACKS.length);
          return 0;
        }
        return p + 1;
      });
    }, 1000);
    return () => clearInterval(id);
  }, [cur.len]);

  const pad = (s) => String(s).padStart(2, '0');
  const fmt = (s) => `${Math.floor(s/60)}:${pad(s%60)}`;
  const pct = (pos / cur.len) * 100;

  return (
    <div className="np">
      <div className="np-art"><image-slot id={cur.slot} src={cur.art} shape="rounded" radius="4" placeholder="drop cover" style={{position:'absolute',inset:0,width:'100%',height:'100%'}}></image-slot></div>
      <div className="np-info">
        <div className="np-track">{cur.track}</div>
        <div className="np-artist">{cur.artist}</div>
        <div className="np-bar"><div className="fill" style={{width: pct + '%'}} /></div>
        <div className="np-time">
          <span>{fmt(pos)}</span>
          <span>−{fmt(cur.len - pos)}</span>
        </div>
      </div>
    </div>
  );
}

// ── Connect grid ──────────────────────────────────────────────────────────
const LINKS = [
  { lbl: "Discord", hdl: "@nnpgbutNOTtermed", url: "https://discord.com/users/505545687329538063",
    ico: <path d="M19 5.5a16 16 0 0 0-4-1.2l-.2.4a14 14 0 0 0-3.6 0L11 4.3a16 16 0 0 0-4 1.2C4 9 3.4 12.4 3.7 15.7c1.5 1 3 1.7 4.4 2.1l.5-1c-.7-.3-1.4-.6-2-1l.4-.3c3.7 1.7 7.7 1.7 11.4 0l.4.3c-.6.4-1.3.7-2 1l.5 1c1.5-.4 3-1 4.4-2.1.4-3.6-.4-7-2.7-10.2zM9 13.5c-.9 0-1.6-.8-1.6-1.8s.7-1.8 1.6-1.8 1.6.8 1.6 1.8-.7 1.8-1.6 1.8zm6 0c-.9 0-1.6-.8-1.6-1.8s.7-1.8 1.6-1.8 1.6.8 1.6 1.8-.7 1.8-1.6 1.8z" fill="currentColor"/> },
  { lbl: "GitHub", hdl: "realnnpg", url: "https://github.com/realnnpg/",
    ico: <path d="M12 2a10 10 0 0 0-3.2 19.5c.5.1.7-.2.7-.5v-1.7c-2.8.6-3.4-1.4-3.4-1.4-.5-1.2-1.1-1.5-1.1-1.5-.9-.6.1-.6.1-.6 1 0 1.5 1 1.5 1 .9 1.5 2.4 1.1 3 .8.1-.7.4-1.1.6-1.4-2.2-.2-4.6-1.1-4.6-5 0-1 .4-2 1-2.6-.1-.3-.4-1.3.1-2.7 0 0 .8-.3 2.7 1a9.5 9.5 0 0 1 5 0c1.9-1.3 2.7-1 2.7-1 .5 1.4.2 2.4.1 2.7.6.7 1 1.6 1 2.6 0 3.9-2.4 4.7-4.6 5 .4.3.7.9.7 1.9v2.8c0 .3.2.6.7.5A10 10 0 0 0 12 2z" fill="currentColor"/> },
  { lbl: "YouTube", hdl: "@realnnpg", url: "https://www.youtube.com/@realnnpg",
    ico: <path d="M23 7.4a3 3 0 0 0-2.1-2.1C19 4.8 12 4.8 12 4.8s-7 0-8.9.5A3 3 0 0 0 1 7.4 31 31 0 0 0 .5 12 31 31 0 0 0 1 16.6a3 3 0 0 0 2.1 2.1C5 19.2 12 19.2 12 19.2s7 0 8.9-.5a3 3 0 0 0 2.1-2.1A31 31 0 0 0 23.5 12 31 31 0 0 0 23 7.4zM9.7 15.5V8.5l6 3.5-6 3.5z" fill="currentColor"/> },
  { lbl: "Spotify", hdl: "nnpg", url: "https://open.spotify.com/user/hyecew4szvgmpxru120qugnib?si=u8cxS-OjSSmRm4RLDrt4VA",
    ico: <path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm4.6 14.4a.6.6 0 0 1-.9.2c-2.4-1.5-5.4-1.8-9-1a.6.6 0 1 1-.3-1.2c3.8-.9 7.2-.5 9.9 1.1.3.2.4.5.3.9zm1.2-2.7a.8.8 0 0 1-1 .3c-2.7-1.7-6.9-2.2-10.1-1.2a.8.8 0 1 1-.4-1.5c3.7-1.1 8.3-.6 11.4 1.3.4.2.5.7.1 1.1zm.1-2.8c-3.3-2-8.7-2.1-11.8-1.2a.9.9 0 1 1-.5-1.8c3.6-1.1 9.6-.9 13.4 1.4a.9.9 0 1 1-1 1.6z" fill="currentColor"/> },
  { lbl: "Chess.com", hdl: "realnnpg", url: "https://www.chess.com/member/realnnpg",
    ico: <path d="M12 2c-2.2 0-4 1.7-4 3.9 0 1 .4 2 1.1 2.7L8 11h2l-.9 4H8v2h8v-2h-1.1L14 11h2l-1.1-2.4c.7-.7 1.1-1.7 1.1-2.7 0-2.2-1.8-3.9-4-3.9zm-5 17h10v3H7v-3z" fill="currentColor"/> },
  { lbl: "Instagram", hdl: "@realnnpg", url: "https://www.instagram.com/realnnpg/",
    ico: <g fill="none" stroke="currentColor" strokeWidth="1.6"><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="1" fill="currentColor"/></g> },
  { lbl: "NameMC", hdl: "nnpg", url: "https://namemc.com/profile/nnpg",
    ico: <path d="M5 4h4l3 5 3-5h4v16h-3v-10l-3 5h-2l-3-5v10H5z" fill="currentColor"/> },
  { lbl: "Email", hdl: "tap to copy", url: "#email",
    ico: <g fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></g> },
];

function Connect({ onEmail }) {
  return (
    <div className="connect">
      {LINKS.map((l) => (
        <a key={l.lbl} href={l.url}
           target={l.url.startsWith('#') ? undefined : "_blank"}
           rel="noopener noreferrer"
           onClick={(e) => {
             if (l.url === '#email') { e.preventDefault(); onEmail && onEmail(); }
           }}>
          <svg className="ico" viewBox="0 0 24 24" aria-hidden="true">{l.ico}</svg>
          <div>
            <div className="lbl">{l.lbl}</div>
            <div className="hdl">{l.hdl}</div>
          </div>
        </a>
      ))}
    </div>
  );
}

// ── Floating Minecraft block easter egg ───────────────────────────────────
function CornerMc({ onClick }) {
  return (
    <div className="corner-mc" onClick={onClick} title="hmm…">
      <svg viewBox="0 0 32 32" shapeRendering="crispEdges">
        <rect width="32" height="32" fill="#3aa84a"/>
        <rect y="0" width="32" height="6" fill="#5cd66c"/>
        <rect x="2" y="8" width="4" height="4" fill="#2d7a36"/>
        <rect x="8" y="14" width="4" height="4" fill="#2d7a36"/>
        <rect x="20" y="10" width="4" height="4" fill="#2d7a36"/>
        <rect x="14" y="22" width="4" height="4" fill="#2d7a36"/>
        <rect x="24" y="20" width="4" height="4" fill="#2d7a36"/>
        <rect width="32" height="32" fill="none" stroke="#000" strokeWidth="2"/>
      </svg>
    </div>
  );
}

Object.assign(window, {
  AsciiHero, PixelAvatar, Counter, Clock, SectionRule, PanelHead,
  McBlock, NowPlaying, Connect, CornerMc, NNPG_ASCII, LINKS,
});


/* terminal.jsx — interactive command terminal */

const TERMINAL_BANNER = [
  "nnpg-shell v1.0.4 · type 'help' for commands",
  "",
];

function Terminal({ onTheme, onSetCrt, onResetBoot }) {
  const [history, setHistory] = React.useState(() =>
    TERMINAL_BANNER.map((t) => ({ kind: 'out', html: t }))
  );
  const [input, setInput] = React.useState('');
  const [cmdHistory, setCmdHistory] = React.useState([]);
  const [hIdx, setHIdx] = React.useState(-1);
  const bodyRef = React.useRef(null);
  const inputRef = React.useRef(null);

  React.useEffect(() => {
    if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
  }, [history]);

  const print = (lines) => {
    const arr = Array.isArray(lines) ? lines : [lines];
    setHistory((h) => [...h, ...arr.map((l) =>
      typeof l === 'string' ? { kind: 'out', html: l } : l
    )]);
  };

  const COMMANDS = {
    help: () => [
      { kind: 'out', html: '<span class="dim">Available commands:</span>' },
      'help            this menu',
      'whoami          who is nnpg',
      'about           about me',
      'projects        list projects',
      'skills          stack & learning',
      'hobbies         what i do offline',
      'socials         all the links',
      'now-playing     what i\'m listening to',
      'theme <name>    matrix | amber | cyan | pink',
      'crt on|off      toggle CRT scanlines',
      'reboot          replay boot sequence',
      'clear           clear the terminal',
      'sudo <cmd>      …try it',
      'date            current UTC time',
      'echo <text>     echoes back',
      '',
    ],
    whoami: () => [
      { kind: 'out', html: '<span class="ok">nnpg</span> <span class="dim">·</span> 16 <span class="dim">·</span> he/him' },
      { kind: 'out', html: '<span class="dim">role:</span> developer / minecraft client author / discord admin' },
      '',
    ],
    about: () => [
      'i make stuff. mostly minecraft mods, some web stuff,',
      'and i run a discord with about 10k people in it.',
      '',
      { kind: 'out', html: '<span class="dim">currently learning java &amp; reverse engineering minecraft internals</span>' },
      '',
    ],
    projects: () => [
      { kind: 'out', html: '<span class="ok">[1]</span> <b>Glazed Client</b> — meteor addon for donutsmp' },
      { kind: 'out', html: '    <span class="dim">10k member discord · &gt;</span> <a href="https://glazedclient.com/" target="_blank" rel="noreferrer">glazedclient.com</a>' },
      { kind: 'out', html: '<span class="ok">[2]</span> <b>HackDisabler</b> — meteor utility addon' },
      { kind: 'out', html: '    <span class="dim">&gt;</span> <a href="https://github.com/realnnpg/HackDisabler" target="_blank" rel="noreferrer">github.com/realnnpg/HackDisabler</a>' },
      { kind: 'out', html: '<span class="ok">[3]</span> <b>Vanilla Realms</b> — minecraft server site' },
      { kind: 'out', html: '    <span class="dim">&gt;</span> <a href="https://vanillarealms.com" target="_blank" rel="noreferrer">vanillarealms.com</a>' },
      '',
    ],
    skills: () => [
      { kind: 'out', html: '<span class="dim">comfortable:</span> <span class="ok">html</span> <span class="ok">css</span> <span class="ok">mysql</span>' },
      { kind: 'out', html: '<span class="dim">learning  :</span> <span class="warn">java</span> <span class="warn">js</span>' },
      '',
    ],
    hobbies: () => [
      'minecraft · brawl stars · ping pong · chess · programming',
      '',
    ],
    socials: () => [
      { kind: 'out', html: '<span class="dim">discord  </span> <a href="https://discord.com/users/828574795041341462" target="_blank" rel="noreferrer">@nnpg</a>' },
      { kind: 'out', html: '<span class="dim">github   </span> <a href="https://github.com/realnnpg/" target="_blank" rel="noreferrer">github.com/realnnpg</a>' },
      { kind: 'out', html: '<span class="dim">youtube  </span> <a href="https://www.youtube.com/@realnnpg" target="_blank" rel="noreferrer">@realnnpg</a>' },
      { kind: 'out', html: '<span class="dim">chess.com</span> <a href="https://www.chess.com/member/realnnpg" target="_blank" rel="noreferrer">realnnpg</a>' },
      { kind: 'out', html: '<span class="dim">spotify  </span> <a href="https://open.spotify.com/user/hyecew4szvgmpxru120qugnib" target="_blank" rel="noreferrer">nnpg</a>' },
      { kind: 'out', html: '<span class="dim">instagram</span> <a href="https://www.instagram.com/realnnpg/" target="_blank" rel="noreferrer">@realnnpg</a>' },
      { kind: 'out', html: '<span class="dim">namemc   </span> <a href="https://namemc.com/profile/nnpg" target="_blank" rel="noreferrer">nnpg</a>' },
      '',
    ],
    'now-playing': () => [
      { kind: 'out', html: '<span class="ok">♪</span> <b>Lose Yourself</b> <span class="dim">— Eminem</span>' },
      '',
    ],
    date: () => [new Date().toUTCString(), ''],
    clear: () => { setHistory([]); return null; },
    cls:   () => { setHistory([]); return null; },
    reboot: () => { onResetBoot && onResetBoot(); return ['rebooting…', '']; },
    sudo: (args) => [
      { kind: 'out', html: '<span class="warn">[sudo] permission denied:</span> nice try lol' },
      ''
    ],
    echo: (args) => [args.join(' '), ''],
    theme: (args) => {
      const t = (args[0] || '').toLowerCase();
      const valid = ['matrix', 'amber', 'cyan', 'pink'];
      if (!valid.includes(t)) {
        return [{ kind: 'out', html: `<span class="warn">usage:</span> theme &lt;${valid.join('|')}&gt;` }, ''];
      }
      onTheme && onTheme(t);
      return [{ kind: 'out', html: `theme set to <span class="ok">${t}</span>` }, ''];
    },
    crt: (args) => {
      const v = (args[0] || '').toLowerCase();
      if (v !== 'on' && v !== 'off') return [{ kind: 'out', html: '<span class="warn">usage:</span> crt on|off' }, ''];
      onSetCrt && onSetCrt(v === 'on');
      return [`crt scanlines: ${v}`, ''];
    },
    ls: () => [
      { kind: 'out', html: '<span class="blue">about/</span>  <span class="blue">projects/</span>  <span class="blue">skills/</span>  <span class="blue">connect/</span>  <span class="dim">.secret</span>' },
      ''
    ],
    cat: (args) => {
      const f = args[0];
      if (f === '.secret') return [
        { kind: 'out', html: '<span class="pink">↑↑↓↓←→←→ B A</span> <span class="dim">— try the konami code anywhere on the page</span>' },
        ''
      ];
      return [`cat: ${f || ''}: no such file`, ''];
    },
  };

  const run = (raw) => {
    const trimmed = raw.trim();
    setHistory((h) => [...h, { kind: 'cmd', input: raw }]);
    if (!trimmed) return;
    setCmdHistory((c) => [trimmed, ...c].slice(0, 30));
    const [cmd, ...args] = trimmed.split(/\s+/);
    const fn = COMMANDS[cmd.toLowerCase()];
    if (!fn) {
      print({ kind: 'out', html: `<span class="warn">command not found:</span> ${cmd}. try <span class="ok">help</span>` });
      print('');
      return;
    }
    const out = fn(args);
    if (out) print(out);
  };

  const onKey = (e) => {
    if (e.key === 'Enter') {
      run(input);
      setInput('');
      setHIdx(-1);
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      if (cmdHistory.length === 0) return;
      const next = Math.min(cmdHistory.length - 1, hIdx + 1);
      setHIdx(next);
      setInput(cmdHistory[next] || '');
    } else if (e.key === 'ArrowDown') {
      e.preventDefault();
      const next = Math.max(-1, hIdx - 1);
      setHIdx(next);
      setInput(next === -1 ? '' : cmdHistory[next]);
    } else if (e.key === 'l' && (e.ctrlKey || e.metaKey)) {
      e.preventDefault();
      setHistory([]);
    }
  };

  return (
    <div className="term" onClick={() => inputRef.current && inputRef.current.focus()}>
      <div className="term-head">
        <span className="lights"><i/><i/><i/></span>
        <span className="title">nnpg@portfolio: ~</span>
        <span className="hint" style={{marginLeft: 12}}>type <b style={{color:"var(--accent)"}}>help</b></span>
      </div>
      <div className="term-body" ref={bodyRef}>
        {history.map((l, i) => (
          l.kind === 'cmd' ? (
            <div key={i} className="term-line cmd">
              <span className="prompt">nnpg@portfolio ~ $</span>
              <span className="input">{l.input}</span>
            </div>
          ) : (
            <div key={i} className="term-line"
                 dangerouslySetInnerHTML={{__html: l.html}} />
          )
        ))}
      </div>
      <div className="term-input">
        <span className="prompt">nnpg@portfolio ~ $</span>
        <input ref={inputRef} value={input} autoFocus
               spellCheck="false" autoComplete="off"
               onChange={(e) => setInput(e.target.value)}
               onKeyDown={onKey} />
      </div>
    </div>
  );
}

window.Terminal = Terminal;


/* app.jsx — main composition for the nnpg portfolio */

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "matrix",
  "display": "grotesk",
  "crt": true,
  "showBoot": true,
  "showTerminal": true
}/*EDITMODE-END*/;

const BOOT_LINES = [
  { t: 50,  h: '<span class="dim">[ ok ] booting nnpg.dev / kernel 6.1.0-nnpg</span>' },
  { t: 80,  h: '<span class="dim">[ ok ] mounting /home/nnpg .................. </span><span class="ok">done</span>' },
  { t: 80,  h: '<span class="dim">[ ok ] loading discord modules .............. </span><span class="ok">done</span>' },
  { t: 80,  h: '<span class="dim">[ ok ] reading minecraft.client.glazed ...... </span><span class="ok">done</span>' },
  { t: 80,  h: '<span class="dim">[ ok ] starting chess.engine ................ </span><span class="ok">done</span>' },
  { t: 80,  h: '<span class="dim">[</span><span class="warn">warn</span><span class="dim">] homework.service ................. </span><span class="warn">skipped</span>' },
  { t: 80,  h: '<span class="dim">[ ok ] eminem.audio.daemon .................. </span><span class="ok">done</span>' },
  { t: 100, h: '<span class="dim">[ ok ] resolving identity ................... </span><span class="ok">nnpg</span>' },
  { t: 200, h: '' },
  { t: 0,   h: '<span class="ok">welcome back, nnpg.</span>' },
];

function Boot({ onDone }) {
  const [shown, setShown] = React.useState(0);
  const [fading, setFading] = React.useState(false);
  React.useEffect(() => {
    let cancelled = false;
    let i = 0;
    const tick = () => {
      if (cancelled) return;
      if (i >= BOOT_LINES.length) {
        setTimeout(() => {
          setFading(true);
          setTimeout(() => onDone && onDone(), 600);
        }, 500);
        return;
      }
      setShown(i + 1);
      const delay = BOOT_LINES[i].t;
      i++;
      setTimeout(tick, delay);
    };
    setTimeout(tick, 200);
    return () => { cancelled = true; };
  }, []);
  return (
    <div className={"boot" + (fading ? " fading" : "")}>
      {BOOT_LINES.slice(0, shown).map((l, i) => (
        <div key={i} className="line" dangerouslySetInnerHTML={{__html: l.h || '&nbsp;'}} />
      ))}
      {shown >= BOOT_LINES.length && (
        <div className="line"><span>nnpg@portfolio ~ $</span><span className="blink-cursor"/></div>
      )}
      <button className="boot-skip" onClick={() => { setFading(true); setTimeout(() => onDone && onDone(), 300); }}>
        skip ▸
      </button>
    </div>
  );
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [booting, setBooting] = React.useState(t.showBoot);
  const [toast, setToast] = React.useState(null);
  const [konami, setKonami] = React.useState(0);

  // Apply theme + display via attrs on <html>
  React.useEffect(() => {
    document.documentElement.setAttribute('data-theme', t.theme);
    document.documentElement.setAttribute('data-display', t.display);
    document.body.setAttribute('data-crt', t.crt ? '1' : '0');
  }, [t.theme, t.display, t.crt]);

  // Konami code
  React.useEffect(() => {
    const seq = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
    let pos = 0;
    const onKey = (e) => {
      const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
      if (k === seq[pos]) {
        pos++;
        if (pos === seq.length) {
          pos = 0;
          fireKonami();
        }
      } else {
        pos = (k === seq[0]) ? 1 : 0;
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  const fireKonami = () => {
    setKonami((k) => k + 1);
    showToast('🟩 konami unlocked — pixel mode engaged');
    setTweak({ display: 'pixel', theme: 'amber' });
    setTimeout(() => {
      setTweak({ display: 'grotesk', theme: 'matrix' });
    }, 6000);
  };

  const showToast = (msg) => {
    setToast(msg);
    setTimeout(() => setToast(null), 3500);
  };

  const onEmail = () => {
    const addr = 'mail' + '@' + 'nnpg.dev';
    navigator.clipboard?.writeText(addr).then(() => {
      showToast(`📋 copied: ${addr}`);
    }).catch(() => {
      showToast(addr);
    });
  };

  const onMcClick = () => {
    showToast('☄️ meteor.client.glazed loaded');
  };

  const resetBoot = () => setBooting(true);

  // Console easter egg — printed once
  React.useEffect(() => {
    if (window.__nnpg_greeted) return;
    window.__nnpg_greeted = true;
    const css = (c) => `color:${c};font-family:monospace`;
    console.log('%cnnpg.dev', css('#6dff7a') + ';font-size:18px;font-weight:700');
    console.log('%cu found the inspector. nice.', css('#93a39a'));
    console.log('%ctype `theme cyan` in the in-page terminal.', css('#7ad6ff'));
  }, []);

  return (
    <>
      {booting && <Boot onDone={() => setBooting(false)} />}

      <div className="shell">
        <div className="statusbar">
          <span className="dot" />
          <span>nnpg.dev</span>
          <span className="sep">/</span>
          <span>session_active</span>
          <span className="sep">/</span>
          <span>uptime 16y</span>
          <span className="right">
            <span><Clock /></span>
            <span>v1.0.4</span>
            <span><b>● online</b></span>
          </span>
        </div>

        <header className="hero">
          <div className="hero-inner">
            <AsciiHero />
          </div>
          <div className="hero-tag">
            hi. there once was a <span className="strike">goofy aah</span> site here.<br/>
            now it's <span className="accent">a slightly less goofy one</span>.
          </div>
          <div className="hero-sub">
            i build minecraft tooling, mess with web stuff, and run a 10k discord. mostly i just like making things.<span className="blink-cursor blink"/>
          </div>
        </header>

        <SectionRule num="01" label="// identity" />

        <div className="grid">
          <section className="panel col-7">
            <PanelHead tag="~/about" title="cat profile.txt" />
            <div className="about">
              <div className="avatar"><img src="nnpg-avatar.jpg" alt="nnpg avatar" style={{position:'absolute',inset:0,width:'100%',height:'100%',objectFit:'cover',imageRendering:'pixelated'}}/></div>
              <div className="bio">
                <div className="name">nnpg</div>
                <div className="meta">
                  <span><b>16</b> yrs</span>
                  <span><b>he/him</b></span>
                  <span>dev · gamer</span>
                </div>
                <p>
                  <span className="hi">hi.</span> i'm a 16 y/o developer. i make minecraft mods and play chess and uuh sum other shi like val sometimes but i suck. lowkey cringe asf site but idc {''}
                  <span style={{color:"var(--fg-mute)"}}>(p.s. i still don't know what to put on this site.)</span>
                </p>
                <dl className="kv">
                  <dt>focus</dt><dd>minecraft client dev</dd>
                  <dt>looking</dt><dd>collabs on minecraft</dd>
                  <dt>not</dt><dd>doing my homework</dd>
                </dl>
              </div>
            </div>
          </section>

          <div className="col-5" style={{display: "flex", flexDirection: "column", gap: 16}}>
            <section className="panel">
              <PanelHead tag="~/skills" />
              <div className="chips" style={{marginBottom: 12}}>
                <span className="chip">html</span>
                <span className="chip">css</span>
                <span className="chip">mysql</span>
                <span className="chip learning">java</span>
                <span className="chip learning">js</span>
              </div>
              <div className="panel-head" style={{margin: "8px 0 8px", borderBottom: 0, paddingBottom: 0}}>
                <span className="tag">~/hobbies</span>
              </div>
              <div className="chips">
                <span className="chip">minecraft</span>
                <span className="chip">brawl stars</span>
                <span className="chip">ping pong</span>
                <span className="chip">chess</span>
                <span className="chip">programming</span>
              </div>
            </section>

            <section className="panel">
              <PanelHead tag="~/now-playing" right="auto-rotating" />
              <NowPlaying/>
            </section>
          </div>
        </div>

        <SectionRule num="02" label="// projects" />

        <div className="grid">
          <section className="panel col-6 project">
            <PanelHead tag="~/projects/glazed" title="glazedclient.com" badge="active" />
            <div className="thumb" style={{width:96,height:96,aspectRatio:'auto',flex:'none'}}>
              <img src="glazed.png" alt="Glazed Client" style={{position:'absolute',inset:0,width:'100%',height:'100%',objectFit:'cover'}}/>
            </div>
            <div className="title">Glazed <b>Addon</b> · meteor addon for donutsmp</div>
            <div className="desc">
              a minecraft meteor client addon focused on the <span style={{color:"var(--accent)"}}>donutsmp</span> server —
              custom modules, utility tooling, and a community that hit <b style={{color:"var(--fg)"}}>~18,000</b> people
              before the server got termed.
            </div>
            <div className="links">
              <a className="btn primary" href="https://glazedclient.com/" target="_blank" rel="noopener noreferrer">
                visit site <span className="arrow">↗</span>
              </a>
              <a className="btn" href="https://discord.gg/glazed" target="_blank" rel="noopener noreferrer">
                join discord <span className="arrow">↗</span>
              </a>
              <a className="btn" href="https://github.com/realnnpg/glazed" target="_blank" rel="noopener noreferrer">
                github <span className="arrow">↗</span>
              </a>
            </div>
          </section>

          <section className="panel col-6 project">
            <PanelHead tag="~/projects/radium" title="radium_client" badge="active" />
            <div className="thumb" style={{width:96,height:96,aspectRatio:'auto',flex:'none'}}>
              <img src="radium.png" alt="Radium Client" style={{position:'absolute',inset:0,width:'100%',height:'100%',objectFit:'cover'}}/>
            </div>
            <div className="title">Radium <b>Client</b> · minecraft client for donutsmp</div>
            <div className="desc">
              a paid minecraft client built for the <span style={{color:"var(--accent)"}}>donutsmp</span> server.
            </div>
            <div className="links">
              <a className="btn primary" href="https://radiumclient.com/" target="_blank" rel="noopener noreferrer">
                visit site <span className="arrow">↗</span>
              </a>
              <a className="btn" href="https://discord.gg/radiumclient" target="_blank" rel="noopener noreferrer">
                join discord <span className="arrow">↗</span>
              </a>
            </div>
          </section>
        </div>

        <SectionRule num="03" label="// connect" />

        <div className="grid">
          <section className="panel col-12">
            <PanelHead tag="~/connect" title="ls socials/" right="8 endpoints" />
            <Connect onEmail={onEmail} />
          </section>
        </div>

        <footer className="footer">
          <div>© 2026 nnpg · made with a lot of prompting</div>
          <div>one day i'll make ts site better<span className="blink-cursor"/></div>
        </footer>
      </div>

      {toast && <div className="toast">{toast}</div>}

      <TweaksPanel title="Tweaks">
        <TweakSection label="Theme" />
        <TweakRadio  label="Accent" value={t.theme}
                     options={['matrix','amber','cyan','pink']}
                     onChange={(v) => setTweak('theme', v)} />
        <TweakRadio  label="Display font" value={t.display}
                     options={['grotesk','mono','pixel']}
                     onChange={(v) => setTweak('display', v)} />
        <TweakToggle label="CRT scanlines" value={t.crt}
                     onChange={(v) => setTweak('crt', v)} />

        <TweakSection label="Modules" />
        <TweakToggle label="Show boot intro" value={t.showBoot}
                     onChange={(v) => setTweak('showBoot', v)} />

        <TweakSection label="Actions" />
        <TweakButton label="Replay boot" onClick={resetBoot} />
        <TweakButton label="Trigger konami" secondary onClick={fireKonami} />
      </TweaksPanel>
    </>
  );
}





window.App = App;
if (typeof module !== 'undefined') { module.exports = { App }; }
