/* Chat guide -- right-side AI guide panel.
   External states (driven by parent App):
     'circle'     -- absolute-positioned ✦ FAB top:36 right:18 (lines up with
                     the V3MatterSwitcher pill). Takes NO flex space, so the
                     case content uses the full case-area width.
     'expanded'   -- 420 px wide flex member, pushes case content left
     ('fullscreen' was dropped 2026-05-11 -- only circle ↔ expanded now.)

   Internal stages (drive content mounting + animation):
     idle           -- circle visible OR panel fully expanded with content
     opening        -- panel geometry animating circle → full (no inner content)
     populating     -- panel fully expanded, content fading in / typewriting
     closing        -- content sliding off, label fading
     collapsing     -- panel geometry animating full → circle (no inner content)

   Open choreography (Luca, 2026-05-11):
     0   ms  click circle, circle fades out (140 ms)
     0   ms  panel mounts at width 0 height 48, begins growing
     0   ms  height: 48 → calc(100% - 48px), 260 ms ease-out
     140 ms  width: 0 → 420, 360 ms cubic-bezier(0.16,1,0.3,1)  ← overlap, drift
     500 ms  panel at full size, content mounts:
     500 ms    header fades in (260 ms ease-out)
     560 ms    composer fades in (260 ms ease-out)
     620 ms    typewriter begins on first AI message (~25 ms per char)

   Close choreography:
     0   ms  click ×, content begins exit:
     0   ms    thread + chips translate X-off and fade (220 ms ease-in)
     80  ms    header label "Your guide / Maria" fades (200 ms)
     80  ms    composer translates Y-down and fades (220 ms ease-in)
     320 ms  content unmounts, panel begins collapsing:
     320 ms    width: 420 → 0 (260 ms cubic-bezier(0.4,0,0.2,1))
     580 ms    height: calc → 48 + radius → 50% (200 ms ease-in)
     780 ms  panel unmounts, circle fades in (240 ms cubic-bezier(0.34,1.56,0.64,1))
*/

const CHAT_SCRIPT_MARIA = [
  { role: 'guide', text: "Hi, I'm your guide. I help you make sense of your situation, get your story and evidence organized, and find your clearest next step, all in plain language. I'm not a lawyer and I won't give legal advice, but I'll walk you through every step at your pace.", chips: ["Okay, where do I start?", "How does this work?"] },
  { role: 'guide', text: "Now that you've seen the layout, let's start with your actual case. Tell me what happened. Even one sentence is enough.", chips: ["My landlord won't return my deposit", "I have documents you can read"] },
  { role: 'user',  text: "My landlord won't return my deposit" },
  { role: 'guide', text: "Got it. Utah deposit cases sit under section 57-17-3 -- the landlord owes you the balance plus an itemized accounting within 30 days of move-out. Three quick checks:", bullets: ["When did you move out?", "Did you get a written accounting?", "How much was the deposit?"] },
  { role: 'user',  text: "Moved out March 15. No accounting. Deposit was $1,800. They kept all of it." },
  { role: 'tool',  text: "Scanning Downloads folder for case documents...", files: [{n:"lease_signed_2023-09-01.pdf", s:"412 KB"},{n:"move-out_inspection.pdf", s:"1.2 MB"},{n:"deposit_demand_letter.docx", s:"28 KB"}] },
  { role: 'guide', text: "Found three documents that look relevant. Your lease confirms the deposit. The move-out inspection is your evidence. The demand letter is what you sent the landlord. Want me to draft your complaint using these?", chips: ["Yes, draft the complaint", "Show me what you found first"] },
];

const EASE_OUT   = 'cubic-bezier(0.22, 1, 0.36, 1)';
const EASE_DRIFT = 'cubic-bezier(0.16, 1, 0.3, 1)';
const EASE_IN    = 'cubic-bezier(0.4, 0, 1, 1)';
const EASE_IO    = 'cubic-bezier(0.4, 0, 0.2, 1)';
const EASE_POP   = 'cubic-bezier(0.34, 1.56, 0.64, 1)';

// Durations now match the @keyframes in demo.html (chatPanelOpen 620 ms,
// chatPanelClose 460 ms). OPEN_TOTAL = duration of the open keyframe — when
// it finishes, stage advances from 'opening' → 'populating' so content
// mounts only after geometry is settled.
const OPEN_TOTAL = 620;
const CLOSE_CONTENT_MS = 220;   // content slides off / label fades
const CLOSE_GEOMETRY_MS = 460;  // @keyframes chatPanelClose duration
const CLOSE_TOTAL_BEFORE_CIRCLE = CLOSE_CONTENT_MS + CLOSE_GEOMETRY_MS;  // ~680 ms
const TYPEWRITER_MS_PER_CHAR = 22;

function ChatGuide(props) {
  const targetState = props.state;
  const onStateChange = props.onStateChange;
  const isDark = THEME.mode === 'dark';

  // Walkthrough mode: when an interactive walkthrough is active, the chat
  // is driven by walkthrough step changes instead of user input. Each step's
  // chatMessage is appended to the thread as a typewriting guide message.
  // Maria's existing CHAT_SCRIPT path is used only when no walkthrough is
  // active (or when the active walkthrough isn't interactive).
  const walkthroughActive = !!(props.walkthroughScript && props.walkthroughScript.mode === 'interactive');
  const walkthroughStep = props.walkthroughStep || 0;

  // Source script: walkthrough-derived when interactive, else Maria-default.
  const sourceScript = React.useMemo(function () {
    if (walkthroughActive) {
      return props.walkthroughScript.steps.map(function (s) {
        return { role: 'guide', text: s.chatMessage || s.body || '' };
      });
    }
    return CHAT_SCRIPT_MARIA;
  }, [walkthroughActive, props.walkthroughScript]);

  // stage: 'circle' | 'opening' | 'populating' | 'expanded' | 'closing' | 'collapsing'
  // Fullscreen dropped 2026-05-11.
  const [stage, setStage] = React.useState(targetState === 'circle' ? 'circle' : 'expanded');

  // Drive transitions from external target.
  React.useEffect(function () {
    if (targetState === stage) return;
    // Open: circle → opening → populating → expanded
    if (stage === 'circle' && targetState === 'expanded') {
      setStage('opening');
      const t1 = setTimeout(function () { setStage('populating'); }, OPEN_TOTAL);
      const t2 = setTimeout(function () { setStage('expanded'); }, OPEN_TOTAL + 600);
      return function () { clearTimeout(t1); clearTimeout(t2); };
    }
    // Close: expanded → closing → collapsing → circle
    if ((stage === 'expanded' || stage === 'populating') && targetState === 'circle') {
      setStage('closing');
      const t1 = setTimeout(function () { setStage('collapsing'); }, CLOSE_CONTENT_MS);
      const t2 = setTimeout(function () { setStage('circle'); }, CLOSE_TOTAL_BEFORE_CIRCLE);
      return function () { clearTimeout(t1); clearTimeout(t2); };
    }
  }, [targetState]);

  // ─── Chat state ────────────────────────────────────────────────────────
  const [count, setCount] = React.useState(1);
  const [input, setInput] = React.useState('');
  const [attached, setAttached] = React.useState([]);
  const [scanning, setScanning] = React.useState(false);
  // Typewriter index for the currently-revealing AI message.
  const [typedChars, setTypedChars] = React.useState(0);
  // Track which message index is currently being typewritten.
  const [typingMsgIdx, setTypingMsgIdx] = React.useState(0);
  const threadRef = React.useRef(null);
  // "Not a lawyer" disclosure popover. Opens when user clicks the header pill.
  const [disclosureOpen, setDisclosureOpen] = React.useState(false);
  // Bottom-right floating panel (home redesign 2026-06-05). Resizable via the
  // invisible top-left grip; width 400-600, height floor min(500,50vh) up to
  // 50vh. listening drives the mic button visual + placeholder swap.
  const [size, setSize] = React.useState({ w: 440, h: null });
  const [listening, setListening] = React.useState(false);
  const resizeRef = React.useRef(null);

  // Start typewriter when entering 'populating' for the first time.
  React.useEffect(function () {
    if (stage !== 'populating') return;
    // Begin typing the first guide message after the populate fade-in delay (~120 ms).
    const startDelay = 120;
    const msg = sourceScript[typingMsgIdx];
    if (!msg || msg.role !== 'guide') return;
    setTypedChars(0);
    const startTimer = setTimeout(function () {
      const interval = setInterval(function () {
        setTypedChars(function (n) {
          if (n >= msg.text.length) { clearInterval(interval); return n; }
          return n + 1;
        });
      }, TYPEWRITER_MS_PER_CHAR);
    }, startDelay);
    return function () { clearTimeout(startTimer); };
  }, [stage]);

  // Walkthrough mode: auto-open the panel when an interactive walkthrough
  // starts. Parent (App) is the source of truth for chat state; we ask it
  // to set expanded. No-op if the panel is already expanded.
  React.useEffect(function () {
    if (!walkthroughActive) return;
    if (targetState !== 'expanded' && typeof onStateChange === 'function') {
      onStateChange('expanded');
    }
  }, [walkthroughActive]);

  // Walkthrough mode: maintain a dynamic message thread. Step messages
  // append on walkthroughStep transitions; per-choice reaction messages
  // append when the user picks a cause / evidence / service option in the
  // underlying UI (dispatched by the coachmark engine as a
  // hc-walkthrough-choice event). Thread is the source of truth for
  // `visible` in walkthrough mode below.
  const [reactionMessages, setReactionMessages] = React.useState([]);
  React.useEffect(function () {
    if (!walkthroughActive) return;
    const targetCount = walkthroughStep + 1;
    if (count === targetCount) return;
    // Delay the chat append so the coachmark spotlight has time to visually
    // transition to the new target before the next message lands. Without
    // this, the chat appears "ahead" of the spotlight: user sees the new
    // step's chatMessage while the spotlight is still mid-transition from
    // the previous step. The CSS spotlight ease is 260 ms; we wait 320 ms
    // to be safely past it. First step (intro) appears immediately.
    const isFirstStep = walkthroughStep === 0 && count === 1;
    const appendDelay = isFirstStep ? 0 : 320;
    let interval = null;
    const appendT = setTimeout(function () {
      setCount(targetCount);
      const msg = sourceScript[walkthroughStep];
      if (!msg || msg.role !== 'guide') return;
      // The new message's actual position in the visible array. visible
      // interleaves reaction messages between sourceScript messages, so
      // when reactions exist, the new step's message is NOT at index
      // walkthroughStep. It's at (targetCount - 1) + reactionMessages.length
      // because every reaction-to-date is inserted before this new message.
      // Without this correction, typingMsgIdx pointed at a reaction message
      // and the typewriter "blank" effect appeared to be retypewriting the
      // wrong line.
      const newMsgVisibleIdx = (targetCount - 1) + reactionMessages.length;
      setTypingMsgIdx(newMsgVisibleIdx);
      setTypedChars(0);
      interval = setInterval(function () {
        setTypedChars(function (n) {
          if (n >= msg.text.length) { clearInterval(interval); return n; }
          return n + 1;
        });
      }, TYPEWRITER_MS_PER_CHAR);
    }, appendDelay);
    return function () { clearTimeout(appendT); if (interval) clearInterval(interval); };
  }, [walkthroughActive, walkthroughStep, sourceScript]);
  // Reset reaction messages when the walkthrough resets (id changes / ends).
  React.useEffect(function () {
    if (!walkthroughActive) setReactionMessages([]);
  }, [walkthroughActive, props.walkthroughScript && props.walkthroughScript.id]);

  // Listen for user-choice events from the coachmark engine. Look up the
  // matching reaction text on the step that was active when the click
  // happened, then append it as an additional guide message tagged with
  // the source stepIdx so render order interleaves it correctly.
  React.useEffect(function () {
    if (!walkthroughActive || !props.walkthroughScript) return;
    const onChoice = function (e) {
      if (!e || !e.detail) return;
      const { stepId, choiceKey } = e.detail;
      const idx = props.walkthroughScript.steps.findIndex(function (s) { return s.id === stepId; });
      if (idx < 0) return;
      const step = props.walkthroughScript.steps[idx];
      const text = step.chatReactions && step.chatReactions[choiceKey];
      if (!text) return;
      // Avoid duplicate dispatches on the same step+key.
      setReactionMessages(function (prev) {
        if (prev.some(function (r) { return r.stepIdx === idx && r.key === choiceKey; })) return prev;
        return prev.concat([{ stepIdx: idx, key: choiceKey, role: 'guide', text: text, kind: 'reaction' }]);
      });
    };
    window.addEventListener('hc-walkthrough-choice', onChoice);
    return function () { window.removeEventListener('hc-walkthrough-choice', onChoice); };
  }, [walkthroughActive, props.walkthroughScript]);

  React.useEffect(function () {
    if (threadRef.current) threadRef.current.scrollTop = threadRef.current.scrollHeight;
  }, [count, scanning, typedChars]);

  // In walkthrough mode, interleave reaction messages immediately AFTER
  // their source step's message. Outside walkthrough mode, just slice the
  // sourceScript by count (the existing Maria-flow behavior).
  const visible = React.useMemo(function () {
    if (!walkthroughActive) return sourceScript.slice(0, count);
    const out = [];
    const upto = Math.min(count, sourceScript.length);
    for (let i = 0; i < upto; i++) {
      out.push(sourceScript[i]);
      reactionMessages.forEach(function (r) {
        if (r.stepIdx === i) out.push(r);
      });
    }
    return out;
  }, [walkthroughActive, sourceScript, count, reactionMessages]);
  const last = visible[visible.length - 1];
  const next = sourceScript[count];

  const panelBg = isDark ? 'rgba(28,28,32,0.55)' : 'rgba(252,248,238,0.42)';
  const panelBorder = isDark ? 'rgba(255,255,255,0.12)' : 'rgba(20,20,30,0.10)';
  const accent = isDark ? '#93A4CD' : '#4E6DA8';
  const textColor = isDark ? '#F2F4F8' : '#1A232C';
  const textDim = isDark ? 'rgba(242,244,248,0.65)' : 'rgba(26,35,44,0.65)';

  function advance() {
    if (next && next.role === 'tool' && next.files) {
      setScanning(true);
      setCount(count + 1);
      setTimeout(function () { setScanning(false); }, 1800);
    } else {
      setCount(count + 1);
      // If the next visible message is a guide message, start typewriting it.
      const nextMsg = sourceScript[count];
      if (nextMsg && nextMsg.role === 'guide') {
        setTypingMsgIdx(count);
        setTypedChars(0);
        const interval = setInterval(function () {
          setTypedChars(function (n) {
            if (n >= nextMsg.text.length) { clearInterval(interval); return n; }
            return n + 1;
          });
        }, TYPEWRITER_MS_PER_CHAR);
      }
    }
  }
  function onChip() {
    if (next && next.role === 'user') {
      setCount(count + 1);
      setTimeout(advance, 600);
    }
  }
  function onSend() {
    if (!input.trim() && attached.length === 0) return;
    setInput('');
    setAttached([]);
    if (next && next.role === 'user') {
      setCount(count + 1);
      setTimeout(advance, 600);
    } else {
      advance();
    }
  }
  function onAttach() { setAttached(attached.concat([{n:"rent_history.pdf", s:"84 KB"}])); }
  function onScan() { if (next && next.role === 'tool') advance(); }

  // Resize via the invisible top-left grip. Width 400-600; height floor
  // min(500,50vh) up to 50vh. Panel is anchored bottom-right, so dragging the
  // top-left corner grows it up and to the left.
  function onResizeDown(e) {
    e.preventDefault(); e.stopPropagation();
    const panel = e.currentTarget.parentNode;
    const r = panel.getBoundingClientRect();
    resizeRef.current = { sx: e.clientX, sy: e.clientY, sw: r.width, sh: r.height };
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {}
  }
  function onResizeMove(e) {
    const r = resizeRef.current; if (!r) return;
    const maxH = Math.round(window.innerHeight * 0.5);
    const minH = Math.min(500, maxH);
    const w = Math.min(600, Math.max(400, r.sw + (r.sx - e.clientX)));
    const h = Math.min(maxH, Math.max(minH, r.sh + (r.sy - e.clientY)));
    setSize({ w: w, h: h });
  }
  function onResizeUp() { resizeRef.current = null; }

  // ─── Geometry per stage ──────────────────────────────────────────────
  // The panel always renders as a flex member when state !== 'circle'.
  // When state === 'circle', we render only the absolute-positioned circle FAB
  // and the panel doesn't exist in flex flow (zero real estate cost).

  const isCircle    = stage === 'circle';
  const isOpening   = stage === 'opening';
  const isPopulating = stage === 'populating';
  const isClosing   = stage === 'closing';
  const isCollapsing = stage === 'collapsing';
  // Fullscreen dropped 2026-05-11 per James -- only circle ↔ expanded now.
  const isExpanded  = stage === 'expanded' || stage === 'populating' || stage === 'closing';

  // Geometry per stage. For 'opening' and 'collapsing' we use @keyframes
  // (chatPanelOpen / chatPanelClose) because CSS transitions don't always
  // fire on first mount, which was creating the rigid jump James saw. The
  // keyframe handles width + height + margin + radius in one timeline so
  // the case content doesn't shift until the panel actually grows.
  const PANEL_W = 404;  // 420 → 404 (− 16 px) per James's tightening pass.

  let panelWidth;
  if (isExpanded || isPopulating) panelWidth = PANEL_W;
  else                            panelWidth = 0;

  const panelHeight = (isExpanded || isPopulating) ? 'calc(100% - 48px)' : 0;
  const panelRadius = (isExpanded || isPopulating) ? 16 : '50%';

  // Animation: drive the geometry curve via keyframes during opening/closing
  // stages. Static values otherwise.
  let panelAnimation = 'none';
  // Open: ease-in-out (symmetric) -- James asked for smoothness on both
  // ends of the open curve, not just the landing.
  if (isOpening)     panelAnimation = 'chatPanelOpen 620ms ' + EASE_IO   + ' forwards';
  // Close stays ease-in: confirms intent, leaves on purpose.
  if (isCollapsing)  panelAnimation = 'chatPanelClose 460ms ' + EASE_IN  + ' forwards';

  return (
    <React.Fragment>
      {/* Circle FAB -- absolute-positioned, no flex footprint.
          top:28 (was 36) bumps the circle up 8 px per James's preference.
          During an interactive walkthrough the FAB is lifted above the
          coachmark scrim (zIndex 9000) so the user can see it. */}
      {isCircle && (
        <button
          data-coach="m-guide-rail"
          onClick={function () { onStateChange('expanded'); }}
          title="Open your guide"
          style={{
            position: 'fixed', bottom: 28, right: 36,
            zIndex: walkthroughActive ? 9500 : 55,
            width: 52, height: 52, borderRadius: '50%',
            background: isDark ? 'linear-gradient(150deg, #4E6DA8, #34507C)' : 'linear-gradient(150deg, #5E7BB6, #46659E)',
            WebkitBackdropFilter: 'blur(26px) saturate(1.7)',
            backdropFilter: 'blur(26px) saturate(1.7)',
            border: '1px solid rgba(143,164,208,0.45)',
            color: '#EAF0FF', fontSize: 24, lineHeight: 1,
            cursor: 'pointer', fontFamily: 'inherit',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            animation: 'chatGlyphIn 240ms ' + EASE_POP + ' both',
            boxShadow: '0 14px 36px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.22)',
          }}>✦</button>
      )}

      {/* Panel -- flex member when state !== 'circle'. During 'opening' and
          'collapsing' the geometry animates via @keyframes (chatPanelOpen /
          chatPanelClose) so width + height + margins all evolve on one
          curve. Inner content mounts only at 'populating' / 'expanded' so
          users never see jumbled words mid-transition. */}
      {!isCircle && (
        <div data-coach="m-guide-rail" style={{
          /* Home redesign 2026-06-05: the panel is now a FIXED bottom-right
             floating surface (was a flex member that pushed case content
             left). It unfolds up + to the left from the FAB corner via the
             chatPanelOpen/Close keyframes (transform + opacity, origin
             bottom-right). Width is user-resizable (400-600) via the invisible
             top-left grip; height auto-grows with the conversation, floored at
             min(500,50vh) and hard-capped at 50vh. Lifts above the coachmark
             scrim (zIndex 9000) during an interactive walkthrough. */
          position: 'fixed', bottom: 24, right: 36,
          width: Math.min(600, Math.max(400, size.w)),
          height: size.h ? size.h : 'auto',
          minWidth: 400, maxWidth: 600,
          minHeight: 'min(500px, 50vh)', maxHeight: '50vh',
          transformOrigin: 'bottom right',
          zIndex: walkthroughActive ? 9500 : 54,
          background: panelBg,
          WebkitBackdropFilter: 'blur(34px) saturate(1.7)',
          backdropFilter: 'blur(34px) saturate(1.7)',
          border: '1px solid ' + panelBorder,
          borderRadius: 16,
          overflow: 'hidden',
          animation: panelAnimation,
          boxShadow: isDark
            ? '0 30px 70px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.14)'
            : '0 30px 70px rgba(20,30,50,0.28), inset 0 1px 0 rgba(255,255,255,0.6)',
          display: 'flex', flexDirection: 'column',
        }}>
          {/* Invisible top-left resize grip (no visible mark; the resize
              cursor on hover is the only hint). */}
          <div onPointerDown={onResizeDown} onPointerMove={onResizeMove} onPointerUp={onResizeUp} onPointerCancel={onResizeUp}
            title="Drag to resize"
            style={{ position: 'absolute', top: 0, left: 0, width: 28, height: 28, cursor: 'nwse-resize', zIndex: 6, touchAction: 'none' }}/>
          {/* Content mounts only when the panel is at full size.
              During 'opening' and 'collapsing' the panel renders as an empty
              shell so users never see chrome mid-transition. */}
          {(isPopulating || isExpanded) && (
            <React.Fragment>
              {/* Header -- "Your guide / Maria - Deposit recovery" + window controls */}
              <div style={{
                display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                padding: '12px 16px', borderBottom: '1px solid ' + panelBorder, flexShrink: 0,
                animation: isClosing
                  ? 'chatLabelOut 200ms ' + EASE_IN + ' 80ms both'
                  : 'chatContentIn 260ms ' + EASE_OUT + ' 0ms both',
              }}>
                <div style={{display: 'flex', alignItems: 'center', gap: 10}}>
                  <div style={{
                    width: 26, height: 26, borderRadius: 7,
                    background: isDark ? 'rgba(143,164,208,0.18)' : 'rgba(78,109,168,0.12)',
                    color: accent, textAlign: 'center', lineHeight: '26px', fontSize: 15,
                  }}>✦</div>
                  <div>
                    {/* "Your AI guide" carries the AI-disclosure element of the
                        UPL "clear and conspicuous" duty permanently (Atticus
                        memo, 2026-05-12). The "Not a lawyer" pill carries the
                        entity-not-a-lawyer element. Subtitle is dynamic per
                        active walkthrough so the header matches the persona. */}
                    <div style={{fontSize: 13.5, fontWeight: 600, color: textColor, lineHeight: 1.1}}>Your AI guide</div>
                    <div style={{fontSize: 10.5, color: textDim, letterSpacing: 0.6, textTransform: 'uppercase', fontFamily: "'JetBrains Mono', monospace", marginTop: 2}}>
                      {(function () {
                        const matter = props.walkthroughScript ? props.walkthroughScript.matter : null;
                        if (matter === 'patel')   return 'Sarah · Wage theft';
                        if (matter === 'maria')   return 'Maria · Deposit recovery';
                        if (matter === 'daniel')  return 'Daniel · Contract dispute';
                        return 'Walking your case';
                      })()}
                    </div>
                  </div>
                </div>
                <div style={{display: 'flex', alignItems: 'center', gap: 8, position: 'relative'}}>
                  {/* "Not a lawyer" disclosure pill. Replaces the persistent
                      footer disclaimer (removed 2026-05-12). UPL-defensible
                      per Atticus memo: initial conspicuous disclosure in the
                      first chat message + ongoing one-click accessibility
                      via this pill. Click opens a popover with the full
                      disclosure text. */}
                  <button
                    onClick={function () { setDisclosureOpen(function (v) { return !v; }); }}
                    title="Important. What I am and what I am not."
                    style={{
                      display: 'flex', alignItems: 'center', gap: 4,
                      padding: '4px 9px 4px 8px',
                      fontSize: 10.5, fontWeight: 600, letterSpacing: 0.3,
                      color: textDim,
                      background: 'transparent',
                      border: '1px solid ' + panelBorder,
                      borderRadius: 999,
                      cursor: 'pointer', fontFamily: 'inherit',
                      lineHeight: 1,
                    }}>
                    <span style={{
                      display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                      width: 13, height: 13, borderRadius: '50%',
                      border: '1px solid ' + textDim,
                      fontSize: 9, fontWeight: 700, color: textDim,
                      fontFamily: "'Inter', system-ui, sans-serif",
                    }}>i</span>
                    Not a lawyer
                  </button>
                  <button title="Close" onClick={function () {
                    onStateChange('circle');
                    // If a walkthrough is active, closing the chat exits it.
                    // Per James 2026-05-12: chat X = stop the walkthrough,
                    // no matter where in the script the user happens to be.
                    if (walkthroughActive) {
                      window.dispatchEvent(new CustomEvent('hc-walkthrough-end'));
                    }
                  }} style={iconBtnStyle(textDim)}>×</button>

                  <Popover
                    open={disclosureOpen}
                    onClose={function () { setDisclosureOpen(false); }}
                    placement="bottom-right"
                    width={320}>
                    <div style={{
                      fontSize: 13.5, fontWeight: 600, marginBottom: 10, color: textColor,
                      fontFamily: "'Fraunces', Georgia, serif", letterSpacing: -0.2,
                    }}>Hello Court is not a law firm.</div>
                    <div style={{ marginBottom: 10, color: textDim }}>
                      Your guide is an AI agent. It can help you organize evidence, draft documents, and explain how the law generally works, but it cannot give you legal advice, predict outcomes, or represent you in any court.
                    </div>
                    <div style={{ color: textDim }}>
                      Every decision about your case, what to file, what to claim, what to settle for, is yours. We give you the structure; you supply the judgment.
                    </div>
                  </Popover>
                </div>
              </div>

              {/* Thread -- AI / user messages + chips */}
              <div ref={threadRef} style={{
                flex: 1, overflowY: 'auto', padding: '20px 16px', display: 'flex', flexDirection: 'column', gap: 14,
                animation: isClosing
                  ? 'chatThreadOut 220ms ' + EASE_IN + ' 0ms both'
                  : 'chatThreadIn 300ms ' + EASE_OUT + ' 100ms both',
              }}>
                {visible.map(function (msg, i) {
                  // Typewriter applies only to the currently-revealing guide message.
                  const isTypingThis = msg.role === 'guide' && i === typingMsgIdx && typedChars < msg.text.length;
                  return <ChatMsg key={i}
                    msg={msg} isDark={isDark}
                    scanning={scanning && i === visible.length - 1}
                    typedText={isTypingThis ? msg.text.slice(0, typedChars) : null}/>;
                })}
                {last && last.role === 'guide' && last.chips && (typedChars >= last.text.length) && (
                  <div style={{
                    display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 4,
                    animation: 'chatContentIn 200ms ' + EASE_OUT + ' both',
                  }}>
                    {last.chips.map(function (s, i) {
                      return (
                        <button key={i} onClick={onChip} style={{
                          padding: '7px 12px', fontSize: 12, fontWeight: 500, color: accent,
                          background: isDark ? 'rgba(143,164,208,0.10)' : 'rgba(78,109,168,0.08)',
                          border: '1px solid ' + (isDark ? 'rgba(143,164,208,0.30)' : 'rgba(78,109,168,0.30)'),
                          borderRadius: 999, cursor: 'pointer', fontFamily: 'inherit', whiteSpace: 'nowrap',
                        }}>{s}</button>
                      );
                    })}
                  </div>
                )}
              </div>

              {attached.length > 0 && (
                <div style={{padding: '8px 16px', borderTop: '1px solid ' + panelBorder, display: 'flex', flexWrap: 'wrap', gap: 6, flexShrink: 0}}>
                  {attached.map(function (f, i) { return <FileChip key={i} f={f} isDark={isDark}/>; })}
                </div>
              )}

              {/* Composer */}
              <div style={{
                padding: '10px 12px 12px', borderTop: '1px solid ' + panelBorder, flexShrink: 0,
                animation: isClosing
                  ? 'chatComposerOut 220ms ' + EASE_IN + ' 80ms both'
                  : 'chatContentIn 260ms ' + EASE_OUT + ' 60ms both',
              }}>
                <div style={{
                  display: 'flex', alignItems: 'flex-end', gap: 8, padding: '8px 10px',
                  background: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(20,20,30,0.04)',
                  border: '1px solid ' + (isDark ? 'rgba(255,255,255,0.10)' : 'rgba(20,20,30,0.10)'),
                  borderRadius: 12,
                }}>
                  <textarea value={input}
                    onChange={function (e) { setInput(e.target.value); }}
                    onKeyDown={function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSend(); } }}
                    placeholder={listening ? 'Listening... speak now' : 'Type your reply, or paste a document...'}
                    rows={1}
                    style={{
                      flex: 1, resize: 'none', border: 'none', outline: 'none', background: 'transparent',
                      color: textColor, fontFamily: 'inherit', fontSize: 13.5, lineHeight: 1.45,
                      padding: '4px 0', minHeight: 22, maxHeight: 120,
                    }}/>
                  <div style={{display: 'flex', gap: 4}}>
                    <button onClick={onAttach} title="Attach file" style={composerBtnStyle(isDark, false, textDim, null)}>+</button>
                    <button onClick={onScan} title="Scan computer for case docs" style={composerBtnStyle(isDark, true, accent, null)}>✦</button>
                    <button onClick={function () { setListening(function (v) { return !v; }); }} title="Speak instead of typing"
                      style={Object.assign({}, composerBtnStyle(isDark, listening, listening ? '#fff' : textDim, listening ? '#D9576B' : null), listening ? { animation: 'micPulse 1.2s ease-in-out infinite', borderColor: '#D9576B' } : {})}>
                      <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="8" y1="22" x2="16" y2="22"/></svg>
                    </button>
                    <button onClick={onSend} disabled={!input.trim() && attached.length === 0} title="Send"
                      style={composerBtnStyle(isDark, false, '#D7DEE2', (input.trim() || attached.length > 0) ? accent : null)}>
                      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
                    </button>
                  </div>
                </div>
                {/* Persistent footer disclaimer removed 2026-05-12. Disclosure
                    moved to (a) first-message acknowledgment-gated copy in the
                    chat thread, (b) "Not a lawyer" pill in the header opening
                    a popover with the full text. UPL-defensible per Atticus
                    memo (Dacey educational posture + WCAG 1.4.3; no sandbox authorization claimed). */}
              </div>
            </React.Fragment>
          )}
        </div>
      )}
    </React.Fragment>
  );
}

function ChatMsg(props) {
  const msg = props.msg;
  const isDark = props.isDark;
  const scanning = props.scanning;
  const typedText = props.typedText;  // string or null
  const textColor = isDark ? '#F2F4F8' : '#1A232C';
  const accent = isDark ? '#93A4CD' : '#4E6DA8';
  if (msg.role === 'tool') {
    return (
      <div style={{
        padding: '10px 12px',
        background: isDark ? 'rgba(143,164,208,0.08)' : 'rgba(78,109,168,0.06)',
        border: '1px dashed ' + (isDark ? 'rgba(143,164,208,0.32)' : 'rgba(78,109,168,0.32)'),
        borderRadius: 10,
      }}>
        <div style={{display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: accent, fontWeight: 500}}>
          <span style={{
            display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
            background: accent, opacity: scanning ? 1 : 0.5,
            animation: scanning ? 'chatScanPulse 1s ease-in-out infinite' : 'none',
          }}/>
          <span>{scanning ? msg.text : msg.text.replace('...', ' -- done.')}</span>
        </div>
        {!scanning && msg.files && (
          <div style={{display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 8}}>
            {msg.files.map(function (f, i) { return <FileChip key={i} f={f} isDark={isDark}/>; })}
          </div>
        )}
      </div>
    );
  }
  const isGuide = msg.role === 'guide';
  // If typedText is set, we're mid-typewriter -- render the partial.
  const displayText = (typedText !== null && typedText !== undefined) ? typedText : msg.text;
  const showCaret = typedText !== null && typedText !== undefined;
  return (
    <div style={{display: 'flex', justifyContent: isGuide ? 'flex-start' : 'flex-end'}}>
      <div style={{
        maxWidth: '85%', padding: '10px 14px', borderRadius: 14,
        borderTopLeftRadius: isGuide ? 4 : 14,
        borderTopRightRadius: isGuide ? 14 : 4,
        background: isGuide
          ? (isDark ? 'rgba(255,255,255,0.04)' : 'rgba(20,20,30,0.04)')
          : (isDark ? 'rgba(143,164,208,0.18)' : 'rgba(78,109,168,0.10)'),
        border: '1px solid ' + (isGuide
          ? (isDark ? 'rgba(255,255,255,0.08)' : 'rgba(20,20,30,0.08)')
          : (isDark ? 'rgba(143,164,208,0.28)' : 'rgba(78,109,168,0.22)')),
        color: textColor, fontSize: 13.5, lineHeight: 1.5,
      }}>
        <div>
          {displayText}
          {showCaret && <span style={{display: 'inline-block', width: 6, marginLeft: 2, color: accent, animation: 'chatCaretBlink 0.9s steps(1) infinite'}}>▋</span>}
        </div>
        {msg.bullets && !showCaret && (
          <ul style={{margin: '6px 0 0', paddingLeft: 18, fontSize: 13}}>
            {msg.bullets.map(function (b, i) { return <li key={i} style={{marginTop: 3}}>{b}</li>; })}
          </ul>
        )}
      </div>
    </div>
  );
}

function FileChip(props) {
  const f = props.f;
  const isDark = props.isDark;
  const textColor = isDark ? '#F2F4F8' : '#1A232C';
  const textDim = isDark ? 'rgba(242,244,248,0.65)' : 'rgba(26,35,44,0.65)';
  return (
    <div style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      padding: '5px 10px', fontSize: 11.5, color: textColor,
      background: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.55)',
      border: '1px solid ' + (isDark ? 'rgba(255,255,255,0.10)' : 'rgba(20,20,30,0.10)'),
      borderRadius: 6,
    }}>
      <span style={{fontSize: 12}}>📄</span>
      <span>{f.n}</span>
      <span style={{color: textDim, fontSize: 10.5}}>· {f.s}</span>
    </div>
  );
}

function iconBtnStyle(color) {
  return {
    width: 28, height: 28, borderRadius: 7,
    background: 'transparent', border: 'none',
    color: color, cursor: 'pointer', fontFamily: 'inherit', fontSize: 14, lineHeight: 1,
    display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
  };
}

function composerBtnStyle(isDark, isAccent, color, bg) {
  return {
    width: 30, height: 30, borderRadius: 8,
    border: '1px solid ' + (isDark ? 'rgba(255,255,255,0.10)' : 'rgba(20,20,30,0.10)'),
    background: bg || (isAccent
      ? (isDark ? 'rgba(143,164,208,0.18)' : 'rgba(78,109,168,0.12)')
      : (isDark ? 'rgba(255,255,255,0.06)' : 'rgba(20,20,30,0.06)')),
    color: color, cursor: 'pointer', fontFamily: 'inherit', fontSize: 15, lineHeight: 1, flexShrink: 0,
    display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
  };
}

window.ChatGuide = ChatGuide;
