// Terminal.jsx — PROYECTO Access Terminal

const Terminal = ({ onAccess }) => {
  const [input, setInput] = React.useState('');
  const [status, setStatus] = React.useState('idle');
  const [shake, setShake] = React.useState(false);
  const [subtitle, setSubtitle] = React.useState('');
  const [brandVisible, setBrandVisible] = React.useState(false);
  const [promptVisible, setPromptVisible] = React.useState(false);
  const [isMobile, setIsMobile] = React.useState(!!window.__IS_MOBILE);
  const inputRef = React.useRef(null);
  // Passwords no longer stored client-side. Auth happens via /api/check-terminal
  // (welcome / proyecto levels) and /api/check-admin (CMS access). The submit
  // function below tries the cheaper terminal check first, then admin if that fails.
  const cmsContent = (window.__PROYECTO_CMS_STORE && window.__PROYECTO_CMS_STORE.get());
  const FULL_SUBTITLE = (cmsContent && cmsContent.terminal && cmsContent.terminal.subtitle) || 'Where the show is built.';

  // Grant-state copy is editable in the CMS under CLEARANCE LABELS. Each read
  // falls back to the original hardcoded string so the terminal still renders
  // correctly if content.json is from before this change, or if a field was
  // accidentally cleared.
  const gt = (cmsContent && cmsContent.grantText) || {};
  const grant = {
    terminalLowHeading:    gt.terminalLowHeading    || 'ACCESS GRANTED',
    terminalHighHeading:   gt.terminalHighHeading   || 'CLEARANCE ELEVATED',
    terminalHighSubtitle:  gt.terminalHighSubtitle  || 'level 02 — proyecto',
    terminalHighTicker1:   gt.terminalHighTicker1   || 'AUTH OK',
    terminalHighTicker2:   gt.terminalHighTicker2   || 'VAULT UNLOCKED',
    terminalHighTicker3:   gt.terminalHighTicker3   || 'FULL ACCESS',
  };

  // Welcome key shown in the RedactedKey hover-reveal. Sourced from the
  // public-display store (populated by index.html on bootstrap from
  // public-display.json). Falls back to the literal 'welcome' if the fetch
  // failed for any reason — keeps the terminal usable even with no network.
  const welcomeKey = (window.__PROYECTO_PUBLIC_DISPLAY && window.__PROYECTO_PUBLIC_DISPLAY.welcomeKey) || 'welcome';

  React.useEffect(() => {
    if (typeof window.__onMobileChange === 'function') {
      return window.__onMobileChange(setIsMobile);
    }
  }, []);

  React.useEffect(() => {
    const t1 = setTimeout(() => setBrandVisible(true), 150);
    const t2 = setTimeout(() => {
      let i = 0;
      const interval = setInterval(() => {
        i++;
        setSubtitle(FULL_SUBTITLE.slice(0, i));
        if (i >= FULL_SUBTITLE.length) clearInterval(interval);
      }, 45);
    }, 700);
    const t3 = setTimeout(() => setPromptVisible(true), 2200);
    return () => { clearTimeout(t1); clearTimeout(t2); clearTimeout(t3); };
  }, []);

  // Auto-focus the input once the prompt appears.
  // Desktop: focus the hidden input. Mobile: don't auto-focus (avoids unwanted keyboard
  // pop-up on landing); user taps the visible field to focus.
  React.useEffect(() => {
    if (inputRef.current && promptVisible && !isMobile) inputRef.current.focus();
  }, [promptVisible, isMobile]);

  // Tap on background:
  // Mobile: no-op — user taps the visible input directly.
  // Desktop: refocus the hidden input (in case clicked elsewhere).
  const handleTap = () => {
    if (status.startsWith('granted')) return;
    if (isMobile) return;
    if (inputRef.current) inputRef.current.focus();
  };

  // Single source of truth for input: native onChange drives state.
  // This works identically on desktop and mobile and avoids double-typing.
  const handleChange = (e) => {
    if (status.startsWith('granted') || status === 'verifying') return;
    setInput(e.target.value.slice(0, 200));
  };

  // Dynamically loads the admin component source after a successful admin
  // auth check. The source is only fetched when this function runs — public
  // visitors never receive it. Returns true on success, false on failure.
  //
  // Implementation: fetch the source as text, transform with Babel standalone
  // (already loaded by index.html), inject the transformed code as a regular
  // <script> tag, then poll briefly for window.CMS to confirm registration.
  const loadAdminModule = async () => {
    // If the admin component is already on window (e.g. retry after a previous
    // successful login this session), skip the fetch.
    if (typeof window.CMS === 'function') return true;
    try {
      const url = '/' + ['module', '7f3a2c'].join('-') + '.jsx';
      const res = await fetch(url, { cache: 'no-store' });
      if (!res.ok) return false;
      const source = await res.text();
      if (!window.Babel || typeof window.Babel.transform !== 'function') {
        // Babel standalone failed to load — should never happen since
        // index.html includes it before this script. Fail closed.
        return false;
      }
      let transformed;
      try {
        transformed = window.Babel.transform(source, { presets: ['react'] }).code;
      } catch (_) {
        // Transform failed (syntax error in admin source, etc).
        return false;
      }
      // Append as a regular <script> with textContent (NOT type="text/babel")
      // so the browser executes it directly. Inline scripts added via textContent
      // run synchronously when appended, so window.CMS should be registered
      // by the time appendChild returns.
      const tag = document.createElement('script');
      tag.textContent = transformed;
      try {
        document.body.appendChild(tag);
      } catch (_) {
        // Script execution threw — admin source has a runtime error.
        return false;
      }
      // Yield once in case any browser is async-y about inline-script execution.
      await new Promise((r) => setTimeout(r, 0));
      return typeof window.CMS === 'function';
    } catch (_) {
      return false;
    }
  };

  // Submit the typed input — runs the access logic. Used by Enter key (desktop)
  // and the mobile submit button. Auth is now server-side: we try the terminal
  // welcome/proyecto endpoint first, and if both miss, try the admin endpoint.
  // Any unhandled failure → denied (matches the old visual behavior).
  const submit = async () => {
    if (status.startsWith('granted') || status === 'verifying') return;
    const v = input.trim();
    if (!v) return;

    setStatus('verifying');

    // Helper: POST a JSON body to one of the check endpoints. Returns the
    // parsed JSON on success, or null on any failure (HTTP error, network
    // error, malformed JSON). We deliberately do NOT differentiate "wrong
    // password" from "server down" in the UI — both look the same as 'denied',
    // which is what the user already expects.
    const post = async (url, body) => {
      try {
        const res = await fetch(url, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
        if (!res.ok) return null;
        return await res.json();
      } catch (_) {
        return null;
      }
    };

    // 1) Try the welcome (low) level.
    let r = await post('/api/check-terminal', { password: v, level: 'welcome' });
    if (r && r.ok) {
      setStatus('granted-low');
      setTimeout(() => onAccess && onAccess('site'), 1800);
      return;
    }

    // 2) Try the proyecto (high) level.
    r = await post('/api/check-terminal', { password: v, level: 'proyecto' });
    if (r && r.ok) {
      setStatus('granted-high');
      setTimeout(() => onAccess && onAccess('site-elevated'), 2400);
      return;
    }

    // 3) Try the admin (CMS) password.
    r = await post('/api/check-admin', { password: v });
    if (r && r.ok) {
      // Auth succeeded — but the admin React component (window.CMS) only
      // exists if its source has been loaded into the page. Public visitors
      // never see this code; we fetch + transform + inject it now.
      // If the load fails, surface a denied state so the user can retry.
      const loaded = await loadAdminModule();
      if (!loaded) {
        // Treat load failure the same as a wrong password: deny + shake.
        // Honest framing: we don't tell the user "couldn't load admin module"
        // because that confirms the endpoint exists. Same UX as a miss.
        setStatus('denied');
        setShake(true);
        setTimeout(() => { setStatus('idle'); setInput(''); setShake(false); }, 1000);
        return;
      }
      setStatus('granted-cms');
      setTimeout(() => onAccess && onAccess('cms'), 1800);
      return;
    }

    // None matched (or the network is down — same UX). Show denied.
    setStatus('denied');
    setShake(true);
    setTimeout(() => { setStatus('idle'); setInput(''); setShake(false); }, 1000);
  };

  // Enter submits.
  const handleKey = (e) => {
    if (status.startsWith('granted') || status === 'verifying') return;
    if (e.key === 'Enter') {
      e.preventDefault();
      submit();
    }
  };

  const masked = '•'.repeat(input.length);

  return (
    <div style={{...ts.wrap, cursor: isMobile ? 'auto' : 'none'}} onClick={handleTap}>
      {/* Hidden input — desktop only. On mobile we render a visible input below. */}
      {!isMobile && (
        <input
          ref={inputRef}
          value={input}
          onChange={handleChange}
          onKeyDown={handleKey}
          autoCapitalize="none"
          autoCorrect="off"
          autoComplete="off"
          spellCheck="false"
          type="text"
          aria-label="Access key"
          style={{ position: 'absolute', opacity: 0, pointerEvents: 'none', width: 1, height: 1, caretColor: 'transparent' }}
          readOnly={status.startsWith('granted')}
        />
      )}

      {/* Diamond plate texture — terminal only, acts as a locked-gate backdrop */}
      <div style={ts.texture} />
      <div style={ts.textureVignette} />

      {/* Scanlines */}
      <div style={ts.scanlines} aria-hidden />

      {/* Sweep line */}
      <div style={ts.sweep} aria-hidden />

      {/* Brand — top left */}
      <div style={{
        ...ts.brand,
        opacity: brandVisible ? 1 : 0,
        ...(isMobile ? { top: 56, left: 20, right: 20, fontSize: 'clamp(24px, 7vw, 30px)', whiteSpace: 'nowrap' } : {}),
      }}>PROYECTO</div>

      {/* Mobile-only KEY hint — sits between the wordmark and the centered prompt.
          Tap to scramble-reveal the low-clearance password. */}
      {isMobile && brandVisible && !status.startsWith('granted') && (
        <div style={ts.mobileKeyWrap}>
          <RedactedKey value={welcomeKey} isMobile={true} />
        </div>
      )}

      {/* Meta — top right (hidden on mobile to avoid overlap) */}
      {!isMobile && (
        <div style={{ ...ts.meta, opacity: brandVisible ? 1 : 0 }}>
          <div>LOS ANGELES</div>
          <div>proyecto.la</div>
          <div style={ts.metaDivider} />
          <div>SYS · OPERATIONAL</div>
          <RedactedKey value={welcomeKey} />
        </div>
      )}

      {/* Corner brackets */}
      <div style={{ ...ts.corner, top: 24, left: 24, borderTop: '1px solid #1a1a1a', borderLeft: '1px solid #1a1a1a' }} />
      <div style={{ ...ts.corner, top: 24, right: 24, borderTop: '1px solid #1a1a1a', borderRight: '1px solid #1a1a1a' }} />
      <div style={{ ...ts.corner, bottom: 24, left: 24, borderBottom: '1px solid #1a1a1a', borderLeft: '1px solid #1a1a1a' }} />
      <div style={{ ...ts.corner, bottom: 24, right: 24, borderBottom: '1px solid #1a1a1a', borderRight: '1px solid #1a1a1a' }} />

      {/* High-clearance flash overlay — single full-screen white pulse on grant */}
      {status === 'granted-high' && <div style={ts.flashOverlay} aria-hidden />}

      {/* Center prompt */}
      <div style={ts.center}>
        {status.startsWith('granted') ? (
          status === 'granted-high' ? (
            <div style={ts.grantedBlockHigh}>
              <div style={ts.grantedLineHigh}>{grant.terminalHighHeading}</div>
              <div style={ts.grantedSubHigh}>{grant.terminalHighSubtitle}<span style={ts.blockCursorCyan} /></div>
              <div style={ts.grantedTickerHigh}>● {grant.terminalHighTicker1} · ● {grant.terminalHighTicker2} · ● {grant.terminalHighTicker3}</div>
            </div>
          ) : (
            <div style={ts.grantedBlock}>
              <div style={ts.grantedLine}>{grant.terminalLowHeading}</div>
              <div style={ts.grantedSub}>{status === 'granted-cms' ? 'admin' : welcomeKey}<span style={ts.blockCursor} /></div>
            </div>
          )
        ) : isMobile ? (
          /* Mobile: visible input + submit button (mirrors events gate). */
          promptVisible && (
            <div style={ts.mobilePromptBlock}>
              <div style={ts.mobilePromptHelper}>
                <span style={{ color: 'var(--accent, #e6351e)' }}>&gt;</span>&nbsp;ENTER ACCESS CODE
              </div>
              <div
                style={{
                  ...ts.mobileInputWrap,
                  ...(shake ? ts.shake : {}),
                  borderColor: status === 'denied' ? 'var(--accent, #e6351e)' : '#1a1a1a',
                }}
                onClick={(e) => { e.stopPropagation(); if (inputRef.current) inputRef.current.focus(); }}
              >
                <span style={ts.promptArrow}>&gt;</span>
                <input
                  ref={inputRef}
                  value={input}
                  onChange={handleChange}
                  onKeyDown={handleKey}
                  type="text"
                  inputMode="text"
                  autoCapitalize="none"
                  autoCorrect="off"
                  autoComplete="off"
                  spellCheck="false"
                  aria-label="Access key"
                  placeholder="key"
                  style={{
                    ...ts.mobileInput,
                    WebkitTextSecurity: 'disc',
                  }}
                />
              </div>
              <button
                onClick={(e) => { e.stopPropagation(); submit(); }}
                disabled={!input.trim() || status === 'verifying'}
                style={{
                  ...ts.mobileSubmit,
                  opacity: (input.trim() && status !== 'verifying') ? 1 : 0.3,
                }}
              >
                {status === 'verifying' ? 'VERIFYING...' : 'SUBMIT ▸'}
              </button>
            </div>
          )
        ) : (
          /* Desktop: password entry */
          <div style={{ ...ts.promptLine, ...(shake ? ts.shake : {}) }}>
            {promptVisible && (<>
              <span style={ts.promptKey}>key</span>
              <span style={ts.promptArrow}>&gt;</span>
              <span style={ts.promptInput}>{masked}</span>
              <span style={{ ...ts.blockCursor, background: status === 'denied' ? 'var(--accent, #e6351e)' : '#fff' }} />
            </>)}
          </div>
        )}
        {status === 'denied' && <div style={ts.deniedLabel}>access denied</div>}
      </div>

      {/* Bottom left — subtitle */}
      <div style={{
        ...ts.subtitle,
        ...(isMobile ? { bottom: 28, left: 22, fontSize: 11 } : {}),
      }}>
        {subtitle}
        {subtitle.length < FULL_SUBTITLE.length && subtitle.length > 0 && <span style={ts.miniCursor} />}
      </div>

      {/* Bottom right — hide on mobile (redundant with meta) */}
      {!isMobile && (
        <div style={ts.footerMeta}>PRIVATE FACILITY · BY APPOINTMENT</div>
      )}
    </div>
  );
};

const ts = {
  wrap: {
    width: '100vw', height: '100vh', background: '#000',
    position: 'relative', overflow: 'hidden',
    cursor: 'none',
    fontFamily: "'Share Tech Mono', monospace",
    color: '#fff', boxSizing: 'border-box', userSelect: 'none',
  },
  texture: {
    position: 'absolute', inset: 0, pointerEvents: 'none',
    backgroundImage: "url('/diamond-plate.png')",
    backgroundSize: 'cover',
    backgroundPosition: 'center',
    backgroundRepeat: 'no-repeat',
    opacity: 1,
    filter: 'grayscale(100%) brightness(1.35) contrast(1.15)',
    zIndex: 0,
  },
  textureVignette: {
    position: 'absolute', inset: 0, pointerEvents: 'none',
    background: 'radial-gradient(ellipse at center, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.55) 55%, rgba(0,0,0,0.85) 100%)',
    zIndex: 0,
  },
  scanlines: {
    position: 'absolute', inset: 0, pointerEvents: 'none',
    background: 'repeating-linear-gradient(0deg, rgba(255,255,255,0.012) 0, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 3px)',
    zIndex: 2,
  },
  sweep: {
    position: 'absolute', left: 0, right: 0, height: 60,
    background: 'linear-gradient(180deg, transparent 0%, rgba(230,53,30,0.075) 50%, transparent 100%)',
    pointerEvents: 'none',
    animation: 'sweep 12s linear infinite',
    zIndex: 3,
  },
  brand: {
    position: 'absolute', top: 52, left: 52,
    fontFamily: "'Cindie2', sans-serif",
    fontSize: 78, fontWeight: 700,
    letterSpacing: '0.04em', textTransform: 'uppercase',
    color: '#fff', lineHeight: 1,
    transition: 'opacity 600ms linear',
    pointerEvents: 'none', zIndex: 1,
  },
  meta: {
    position: 'absolute', top: 40, right: 48,
    fontSize: 12, color: '#3a3a3a',
    letterSpacing: '0.15em', textTransform: 'uppercase',
    textAlign: 'right', lineHeight: 1.9,
    transition: 'opacity 600ms linear',
    zIndex: 10,
  },
  corner: {
    position: 'absolute', width: 16, height: 16,
    pointerEvents: 'none', zIndex: 1,
  },
  center: {
    position: 'absolute', inset: 0,
    display: 'flex', flexDirection: 'column',
    justifyContent: 'center', alignItems: 'center',
    gap: 16, zIndex: 4, pointerEvents: 'none',
  },
  promptLine: {
    display: 'flex', alignItems: 'center',
    fontSize: 17, color: '#fff', letterSpacing: '0.10em', gap: 10,
  },
  promptKey: { color: '#8A9099' },
  promptArrow: { color: 'var(--accent, #e6351e)', fontWeight: 500 },
  promptInput: { color: '#fff', letterSpacing: '0.18em', fontSize: 18 },
  blockCursor: {
    display: 'inline-block', width: 9, height: 15, background: '#fff',
    animation: 'blink 1.1s step-end infinite', marginLeft: 2,
  },
  miniCursor: {
    display: 'inline-block', width: 7, height: 12, background: '#8A9099',
    animation: 'blink 1.1s step-end infinite', marginLeft: 3,
  },
  grantedBlock: {
    display: 'flex', flexDirection: 'column',
    alignItems: 'center', gap: 14,
    animation: 'flicker 0.4s linear',
  },
  grantedLine: {
    fontSize: 15, color: 'var(--welcome-flash, #e6351e)',
    letterSpacing: '0.28em', textTransform: 'uppercase',
  },
  grantedSub: {
    display: 'flex', alignItems: 'center',
    fontSize: 15, color: 'var(--welcome-flash-soft, #8A9099)', letterSpacing: '0.16em',
  },
  // ── High-clearance variant ─────────────────────────────────────
  grantedBlockHigh: {
    display: 'flex', flexDirection: 'column',
    alignItems: 'center', gap: 16,
    animation: 'flickerCyan 0.5s linear', // animation name kept for backwards-compat
    // Glow inherits text color so it follows the theme variables below
    // (currentColor resolves to var(--high-flash) on the heading line).
    textShadow: '0 0 12px currentColor',
  },
  grantedLineHigh: {
    fontSize: 17, color: 'var(--high-flash, #00ffd1)',
    letterSpacing: '0.32em', textTransform: 'uppercase',
    fontWeight: 600,
  },
  grantedSubHigh: {
    display: 'flex', alignItems: 'center',
    fontSize: 14, color: 'var(--high-flash-soft, #7af5d8)', letterSpacing: '0.18em',
    textTransform: 'lowercase',
  },
  grantedTickerHigh: {
    fontSize: 11, color: 'var(--high-flash, #00ffd1)',
    letterSpacing: '0.22em', textTransform: 'uppercase',
    marginTop: 8, opacity: 0.8,
    animation: 'tickerSlide 0.6s ease-out',
  },
  blockCursorCyan: {
    display: 'inline-block', width: 9, height: 15, background: 'var(--high-flash, #00ffd1)',
    boxShadow: '0 0 8px currentColor',
    animation: 'blink 1.1s step-end infinite', marginLeft: 2,
  },
  flashOverlay: {
    position: 'absolute', inset: 0, background: 'var(--high-flash, #00ffd1)',
    pointerEvents: 'none', zIndex: 50,
    animation: 'highFlash 0.4s ease-out forwards',
  },
  deniedLabel: {
    fontSize: 13, color: 'var(--accent, #e6351e)',
    letterSpacing: '0.2em', textTransform: 'uppercase',
    animation: 'flicker 0.3s linear',
  },
  mobileEnter: {
    display: 'flex', flexDirection: 'column', alignItems: 'center',
    gap: 12,
  },
  mobileEnterLine: {
    display: 'flex', alignItems: 'center', gap: 10,
    fontSize: 15, color: '#fff',
    letterSpacing: '0.22em', textTransform: 'uppercase',
  },
  mobilePromptBlock: {
    display: 'flex', flexDirection: 'column', gap: 14,
    width: '88%', maxWidth: 360,
    padding: '0 4px',
    pointerEvents: 'auto', // re-enable: ts.center has pointerEvents: 'none'
  },
  mobileKeyWrap: {
    position: 'absolute',
    top: 110,
    left: 20,
    zIndex: 10,
    animation: 'fadeIn 600ms 700ms backwards linear',
  },
  mobilePromptHelper: {
    fontSize: 10, color: '#666',
    letterSpacing: '0.24em', textTransform: 'uppercase',
    textAlign: 'left',
  },
  mobileInputWrap: {
    display: 'flex', alignItems: 'center', gap: 10,
    padding: '12px 14px',
    border: '1px solid #1a1a1a',
    background: 'rgba(0,0,0,0.55)',
    transition: 'border-color 120ms linear',
  },
  mobileInput: {
    flex: 1,
    background: 'transparent',
    border: 'none', outline: 'none',
    color: '#fff',
    fontFamily: "'Share Tech Mono', monospace",
    fontSize: 16, // 16px to prevent iOS zoom on focus
    letterSpacing: '0.18em',
    padding: 0,
    caretColor: 'var(--accent, #e6351e)',
  },
  mobileSubmit: {
    background: 'transparent', border: '1px solid var(--accent, #e6351e)',
    fontFamily: "'Share Tech Mono', monospace",
    fontSize: 12, letterSpacing: '0.22em', textTransform: 'uppercase',
    color: 'var(--accent, #e6351e)', padding: '13px 24px',
    borderRadius: 0, cursor: 'pointer',
    width: '100%',
    transition: 'opacity 120ms linear',
  },
  shake: { animation: 'shake 0.35s linear' },
  subtitle: {
    position: 'absolute', bottom: 40, left: 52,
    fontSize: 13, color: '#555',
    letterSpacing: '0.1em', minHeight: 18,
    pointerEvents: 'none', zIndex: 1,
  },
  footerMeta: {
    position: 'absolute', bottom: 40, right: 48,
    fontSize: 12, color: '#222',
    letterSpacing: '0.18em', textTransform: 'uppercase',
    pointerEvents: 'none', zIndex: 1,
  },
  metaDivider: {
    width: 28, height: 1, background: '#1a1a1a',
    marginLeft: 'auto', marginTop: 10, marginBottom: 10,
  },
  keyLine: {
    display: 'inline-flex', alignItems: 'center', gap: 8,
    padding: '6px 10px', border: '1px solid', borderRadius: 0,
    fontSize: 12, letterSpacing: '0.16em', textTransform: 'uppercase',
    marginTop: 6,
    transition: 'color 180ms linear, border-color 180ms linear',
    cursor: 'none',
  },
  keyValue: {
    fontSize: 13, letterSpacing: '0.24em',
    fontFamily: "'Share Tech Mono', monospace",
  },
};

const RedactedKey = ({ value, isMobile }) => {
  const [display, setDisplay] = React.useState('░'.repeat(value.length));
  const [revealed, setRevealed] = React.useState(false);
  const animRef = React.useRef(null);

  const scramble = (target, reveal) => {
    if (animRef.current) clearInterval(animRef.current);
    const chars = 'abcdefghijklmnopqrstuvwxyz0123456789░▓▒';
    let step = 0; const steps = 6;
    animRef.current = setInterval(() => {
      step++;
      const out = target.split('').map((ch, i) => {
        const locked = step >= (i + 1) * (steps / target.length);
        if (locked) return reveal ? ch : '░';
        return chars[Math.floor(Math.random() * chars.length)];
      }).join('');
      setDisplay(out);
      if (step >= steps + 2) {
        clearInterval(animRef.current);
        setDisplay(reveal ? target : '░'.repeat(target.length));
      }
    }, 45);
  };

  const reveal = () => { setRevealed(true);  scramble(value, true); };
  const hide   = () => { setRevealed(false); scramble(value, false); };

  // Desktop: hover. Mobile: tap to toggle (since hover doesn't exist on touch).
  const handlers = isMobile
    ? {
        onClick: (e) => {
          e.stopPropagation(); // don't let the wrap intercept
          revealed ? hide() : reveal();
        },
      }
    : { onMouseEnter: reveal, onMouseLeave: hide };

  return (
    <div {...handlers}
      style={{
        ...ts.keyLine,
        color: revealed ? 'var(--accent, #e6351e)' : '#bbb',
        borderColor: revealed ? 'rgba(230,53,30,0.4)' : 'rgba(187,187,187,0.4)',
        ...(isMobile ? { cursor: 'pointer', pointerEvents: 'auto' } : {}),
      }}
      data-hoverable="true">
      <span style={{ color: revealed ? '#8A9099' : '#bbb' }}>KEY</span>
      <span style={{ ...ts.keyValue, textTransform: 'lowercase' }}>{display}</span>
    </div>
  );
};

Object.assign(window, { Terminal });
