/* v2 Triage / Intake — 5-step flow per Story 0.17 spec.
   0: Welcome (expectation-set)
   1: Tell your story (textarea + voice + AI assist; voice/AI inert in Phase A)
   2: Sounds like this (state + category + deadline confirm; inference in Phase D)
   3: Both sides (favorable + against, canned content in Phase A)
   4: Path forward (verdict + CTA)
   Output unchanged: Self-Service / Hybrid / Attorney (soft routing, never gates). */

const TRIAGE_CATEGORIES = [
  { id: 'small-claims', label: 'Small claims', icon: Icons.scale, hint: 'Under $10,000 (NY) or $15,000 (UT)' },
  { id: 'landlord', label: 'Landlord / tenant', icon: Icons.folder, hint: 'Lease, security deposit, eviction' },
  { id: 'employment', label: 'Employment', icon: Icons.user, hint: 'Wages, wrongful termination' },
  { id: 'consumer', label: 'Consumer', icon: Icons.shield, hint: 'Refunds, fraud, debt collection' },
  { id: 'family', label: 'Family', icon: Icons.bell, hint: 'Divorce, custody, support' },
  { id: 'business', label: 'Small business', icon: Icons.doc, hint: 'Vendor disputes, contracts' },
  { id: 'traffic', label: 'Traffic / vehicle', icon: Icons.flag, hint: 'Tickets, accidents under $5K' },
  { id: 'other', label: 'Something else', icon: Icons.book, hint: "We'll route you to a consult" },
];

const DEADLINE_OPTIONS = [
  { id: 'urgent', label: 'Within 7 days', tone: '#E06464', sub: "We'll flag this as urgent and recommend you talk to an attorney first." },
  { id: '14days', label: 'Within 14 days', tone: '#F2C53D', sub: "We'll recommend at least a document review before filing." },
  { id: 'soon', label: 'Within 30 days', tone: '#7BCA9E', sub: 'You have time. Self-service workflow fits.' },
  { id: 'none', label: 'No deadline pressing', tone: '#8B9BAE', sub: 'No urgency. Full self-paced workflow.' },
  { id: 'unsure', label: "I'm not sure", tone: '#8B9BAE', sub: "We'll suggest checking with an attorney before you file." },
];

const TriageIntake = ({ jx, onClose, onFinish }) => {
  const [step, setStep] = React.useState(0);
  // Step 2 (the basics) has 3 internal sub-steps: state → category → deadline.
  // Each sub-step is its own focused screen with description, sliding in from
  // the right. Reduces cognitive load vs the prior all-three-on-one-screen.
  const [subStep, setSubStep] = React.useState(0);
  const [description, setDescription] = React.useState('');
  const [state, setState] = React.useState(jx === 'utah' ? 'UT' : 'NY');
  const [category, setCategory] = React.useState(null);
  const [deadline, setDeadline] = React.useState(null);
  // selectedPath lets the user pick a path different from the recommendation
  // on step 4. null = inherit recommendation; otherwise overrides.
  const [selectedPath, setSelectedPath] = React.useState(null);
  // Out-of-area terminal state (Gap 1). When the user says their case isn't in
  // a supported jurisdiction, we route to an honest "not yet" screen instead of
  // forcing them into the wrong state's framework. Set on Continue from the
  // "where did this happen?" sub-step; cleared by Back.
  const [outOfArea, setOutOfArea] = React.useState(false);
  // Inline waitlist on the out-of-area screen. "Join the waitlist" opens an
  // email field (the footer buttons slide left to make room); submitting
  // confirms via a toast and flips to a done state. Mocked — no network in the
  // static demo, and per Atticus we deliberately capture nothing else from an
  // unsupported-state matter.
  const [waitlistOpen, setWaitlistOpen] = React.useState(false);
  const [waitlistEmail, setWaitlistEmail] = React.useState('');
  const [waitlistDone, setWaitlistDone] = React.useState(false);
  const [toast, setToast] = React.useState(null);
  const [toastLeaving, setToastLeaving] = React.useState(false);
  // Language gate state (tier-1 i18n). null = chooser not answered yet; 'en'
  // continues the flow; 'es' shows the Spanish "coming soon" + waitlist.
  const [lang, setLang] = React.useState(null);
  const langPicker = lang === null;
  const spanishSoon = lang === 'es';
  // Tiny localizer for the shared waitlist footer — Spanish only on the
  // Spanish-coming-soon screen; English everywhere else (incl. out-of-area).
  const t = (en, es) => (spanishSoon ? es : en);

  // Outcome logic unchanged from prior 3-step flow.
  // effectivePath is what drives the verdict CTA. Defaults to the recommended
  // outcome but the user can override on step 4 by clicking a different card.
  const effectivePath = selectedPath || (() => {
    // Inline duplicate of outcome logic so effectivePath has a value before
    // outcome is declared below.
    if (deadline === 'urgent' || category === 'family' || category === 'other') return 'attorney';
    if (deadline === '14days' || category === 'employment' || category === 'consumer') return 'hybrid';
    return 'self';
  })();
  const outcome = (() => {
    if (deadline === 'urgent' || category === 'family' || category === 'other') return 'attorney';
    if (deadline === '14days' || category === 'employment' || category === 'consumer') return 'hybrid';
    return 'self';
  })();

  const OUTCOMES = {
    self: {
      label: 'Self-service fits', tone: '#7BCA9E',
      title: "Looks like a great fit for the guided path. We've got you.",
      body: "Your situation looks like a good fit for the self-service workflow. We'll walk through your facts, help you pick the legal framework that fits, organize your evidence, and draft your court-ready documents. You can ask for an attorney consult or document review any time you want a second opinion.",
      cta: "Start the guided workflow",
    },
    hybrid: {
      label: 'Better with an attorney touchpoint', tone: '#F2C53D',
      title: "We can do most of this together. A check-in with an attorney would help.",
      body: "You can use the guided workflow to organize your matter, and we'll surface a couple of moments where an attorney consult or document review would make a real difference. You decide whether to take them. We'll just make sure you see them.",
      cta: "Start, with attorney touchpoints",
    },
    attorney: {
      label: 'Talk to an attorney first', tone: '#E06464',
      title: "This one calls for a real attorney in your corner.",
      body: "Based on what you've told us, we'd feel better if you talked to a verified attorney first. You can keep going on your own, but we'd recommend starting with a free 15-minute consult.",
      cta: "Find a verified attorney",
    },
  };

  // Field updates accept null to clear.
  const choose = (field, value) => {
    if (field === 'state') setState(value);
    if (field === 'category') setCategory(value);
    if (field === 'deadline') setDeadline(value);
  };

  // Toast lifecycle: ease in, stay fully present for 3 seconds, then ease out
  // (0.5s, matching the toastOut keyframe) and unmount.
  React.useEffect(() => {
    if (!toast) return;
    setToastLeaving(false);
    const leave = setTimeout(() => setToastLeaving(true), 3000);
    const remove = setTimeout(() => { setToast(null); setToastLeaving(false); }, 3500);
    return () => { clearTimeout(leave); clearTimeout(remove); };
  }, [toast]);
  // Manual dismiss (the toast X) eases out the same way rather than vanishing.
  const dismissToast = () => {
    setToastLeaving(true);
    setTimeout(() => { setToast(null); setToastLeaving(false); }, 500);
  };

  // Waitlist submit (mocked). Basic email sanity check, then confirm via toast
  // and collapse the field into a done state so it can't be re-submitted.
  const waitlistValid = /.+@.+\..+/.test(waitlistEmail.trim());
  const submitWaitlist = () => {
    if (!waitlistValid) return;
    setToast(spanishSoon
      ? "¡Listo! Te avisaremos en cuanto el español esté disponible."
      : "You're on the list. We'll email you the moment we reach your state.");
    setWaitlistOpen(false);
    setWaitlistDone(true);
    setWaitlistEmail('');
  };

  // ── Phase B: Voice input via Web Speech API ───────────────────────────
  // Chrome + Safari ship the Recognition API under the webkit prefix; Firefox
  // doesn't ship it at all. We feature-detect and gracefully disable the mic
  // button if the browser can't deliver. While recording, final transcript
  // chunks are appended to the description textarea (interim results are not
  // surfaced to keep the UI calm).
  const recognitionRef = React.useRef(null);
  const [isRecording, setIsRecording] = React.useState(false);
  const [voiceSupported, setVoiceSupported] = React.useState(false);

  React.useEffect(() => {
    if (typeof window === 'undefined') return;
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) { setVoiceSupported(false); return; }
    setVoiceSupported(true);
    const recognition = new SR();
    recognition.continuous = true;
    recognition.interimResults = false;
    recognition.lang = 'en-US';
    recognition.onresult = (e) => {
      let final = '';
      for (let i = e.resultIndex; i < e.results.length; i++) {
        if (e.results[i].isFinal) final += e.results[i][0].transcript;
      }
      const trimmed = final.trim();
      if (trimmed) {
        setDescription(prev => (prev && !prev.endsWith(' ') ? prev + ' ' : prev) + trimmed);
      }
    };
    recognition.onend = () => setIsRecording(false);
    recognition.onerror = () => setIsRecording(false);
    recognitionRef.current = recognition;
    return () => { try { recognition.stop(); } catch (err) {} };
  }, []);

  const handleVoice = () => {
    const r = recognitionRef.current;
    if (!r) return;
    if (isRecording) {
      try { r.stop(); } catch (err) {}
      setIsRecording(false);
    } else {
      try { r.start(); setIsRecording(true); } catch (err) { setIsRecording(false); }
    }
  };

  // ── Phase D: canned inference from description ────────────────────────
  // Reads the textarea content for known phrases and pre-fills state /
  // category / deadline on the read-back step. Only sets fields the user
  // hasn't manually picked yet (won't override an explicit choice). Runs
  // on every description change, regex matches are cheap.
  React.useEffect(() => {
    if (!description) return;
    const d = description.toLowerCase();
    if (/salt lake|\butah\b|provo|ogden|orem\b|sandy\b/.test(d)) setState('UT');
    else if (/new york|\bnyc\b|brooklyn|manhattan|queens|bronx/.test(d)) setState('NY');
    setCategory(prev => {
      if (prev) return prev;
      if (/deposit|landlord|tenant|\blease\b|evict/.test(d)) return 'landlord';
      if (/repair|auto|car|mechanic|fraud|refund|consumer/.test(d)) return 'consumer';
      if (/wage|overtime|\bfired\b|terminated|employer/.test(d)) return 'employment';
      if (/contract|business|vendor/.test(d)) return 'business';
      if (/divorce|custody|family/.test(d)) return 'family';
      if (/traffic|ticket|accident/.test(d)) return 'traffic';
      if (/small.?claim/.test(d)) return 'small-claims';
      return prev;
    });
    setDeadline(prev => {
      if (prev) return prev;
      if (/within (a |one )?week|7 days|next week/.test(d)) return 'urgent';
      if (/within 14|two weeks|14 days/.test(d)) return '14days';
      if (/within 30|month|30 days/.test(d)) return 'soon';
      if (/no.{0,20}(deadline|rush|pressing|hurry)/.test(d)) return 'none';
      return prev;
    });
  }, [description]);

  // Both-sides content per category. Drives step 3. Each item can carry an
  // optional `laws` array surfaced via expand toggle, so the user can see the
  // actual statutes / citations behind the headline.
  const BOTH_SIDES = {
    landlord: {
      favor: [
        { t: 'Wrongful retention of deposit', b: 'Utah § 57-17-3 requires landlords to return your deposit or send an itemized statement of deductions within 30 days. Missing that window may make double damages available.', laws: [
          { cite: 'Utah Code § 57-17-3', text: 'Landlord must return the deposit, plus interest, and any prepaid rent, within 30 days of vacancy. If the landlord retains any portion, an itemized statement of deductions and the balance owed must be sent within the same window.' },
          { cite: 'Utah Code § 57-17-4', text: 'A landlord who fails to comply is liable for the full deposit plus a civil penalty of $100 and the renter\'s court costs.' },
        ] },
        { t: 'Move-out documentation', b: 'Photos with original capture dates, the certified-mail demand, and your signed lease together cover all four elements a court looks for in a deposit case.', laws: [
          { cite: 'Utah R. Evid. 901', text: 'Authentication of digital photos with embedded EXIF metadata is generally sufficient under the rules of evidence; original capture date and GPS support a chain-of-custody argument.' },
        ] },
        { t: 'Statutory penalty available', b: 'If the court finds wrongful retention, you can recover up to 2x the deposit, plus filing fees and reasonable attorney costs.', laws: [
          { cite: 'Utah Code § 57-17-4(1)(b)', text: 'On finding of bad-faith retention, court may award the renter the full deposit plus an additional sum equal to twice the amount of the deposit wrongfully withheld.' },
        ] },
      ],
      against: [
        { t: 'Damage claims', b: "The landlord may say the unit had damage that justified keeping the deposit. Have your move-out photos and condition checklist ready so you can address that head on.", laws: [
          { cite: 'Utah Code § 57-17-3(2)', text: 'Burden of proving damages remains on the landlord. Itemized statement must specify each deduction with a corresponding amount.' },
        ] },
        { t: 'Lease-break disputes', b: 'If they argue you broke the lease early, expect them to bring move-in and move-out date records. Match yours against theirs in advance.', laws: [
          { cite: 'Utah Code § 57-17-2', text: 'Notice and termination provisions; default written notice period is 15 days unless the lease specifies otherwise.' },
        ] },
        { t: 'Service of process', b: "Bigger landlords often have a registered agent that won't accept regular mail. Make sure your service method matches what your court allows so the case isn't dismissed on a technicality.", laws: [
          { cite: 'Utah R. Civ. P. 4(d)', text: 'Service on a corporate or LLC defendant must be made on the registered agent or an officer authorized to accept service. Service by certified mail accepted with proof of delivery in small-claims matters.' },
        ] },
      ],
    },
    consumer: {
      favor: [
        { t: 'Statutory consumer protections', b: 'Most states give consumers strong rights when a service exceeds the agreed estimate or when a written authorization was missing. Many of these statutes allow treble damages.', laws: [
          { cite: 'NY Gen. Bus. Law § 198-a', text: 'Auto repair shops must provide a written estimate before performing work; charges may not exceed 110% of the estimate without prior written authorization. Treble damages plus attorney fees on violation.' },
        ] },
        { t: 'Paper trail favors you', b: 'When the disagreement is about price or scope, the original estimate, the invoice, and any text or email exchange usually do most of the work.' },
      ],
      against: [
        { t: 'Verbal authorization claims', b: "The other side may say you verbally agreed to the additional work. If you have any contrary text or email, surface it early." },
        { t: 'Time-barred claims', b: "Consumer claims usually have a 1 to 3 year statute of limitations depending on the state. Confirm your dates fit the window.", laws: [
          { cite: 'NY CPLR § 213', text: 'Most contract-based consumer claims carry a 6-year statute of limitations. Statutory consumer-protection claims may have shorter windows (3 years under GBL § 349).' },
        ] },
      ],
    },
    employment: {
      favor: [
        { t: 'Wage statutes are strict', b: 'State wage-payment laws usually allow recovery of unpaid amounts plus liquidated or treble damages, plus attorney fees if you have to file.', laws: [
          { cite: 'NY Labor Law § 198(1-a)', text: 'On a finding of unpaid wages, court must award the wages, plus liquidated damages equal to 100% of the unpaid amount, prejudgment interest, and reasonable attorney fees and costs.' },
          { cite: '29 U.S.C. § 207', text: 'Federal Fair Labor Standards Act: nonexempt employees must receive overtime at 1.5× regular rate for hours worked over 40 in a workweek.' },
          { cite: '12 NYCRR § 142-2.2', text: 'New York overtime regulation extends FLSA protections; some industries (residential employees, agricultural) have separate thresholds.' },
        ] },
        { t: 'Paystubs and timecards', b: "Records the employer is legally required to keep work in your favor. If they can't produce them, courts often credit your numbers.", laws: [
          { cite: 'NY Labor Law § 195(4)', text: 'Employers must keep payroll records (hours worked, wages paid, pay rate, deductions) for at least 6 years and provide employees with a wage statement each pay period.' },
          { cite: '29 C.F.R. § 516.5', text: 'Federal recordkeeping: name, address, occupation, hourly rate, hours worked each day and week, total wages, additions and deductions. Records retained for 3 years.' },
          { cite: 'Anderson v. Mt. Clemens Pottery Co., 328 U.S. 680 (1946)', text: 'When employer fails to keep accurate records, the employee\'s reasonable estimate of unpaid hours shifts the burden to the employer to negate it.' },
        ] },
      ],
      against: [
        { t: 'At-will employment', b: 'Most states are at-will, which limits wrongful-termination claims unless a protected reason is involved. Be ready to identify the protected basis.', laws: [
          { cite: 'NY Common Law (Murphy v. American Home Products, 58 N.Y.2d 293)', text: 'NY follows the at-will doctrine: employer may terminate for any reason or no reason, except where a statutory or contractual exception applies (discrimination, retaliation, public policy).' },
          { cite: 'NY Labor Law § 215', text: 'Anti-retaliation: employer may not discharge or discriminate against employee for filing a wage complaint or cooperating with an investigation. Penalties include reinstatement, back pay, liquidated damages.' },
        ] },
        { t: 'Counterclaims', b: 'Employers sometimes raise breach of contract or alleged policy violations as defenses. Have your employment agreement and policy acknowledgments ready.', laws: [
          { cite: 'NY CPLR § 3019', text: 'Counterclaims may be asserted against any party. The court will hear them in the same proceeding unless severed for case management.' },
        ] },
      ],
    },
    default: {
      favor: [
        { t: 'Your records are the case', b: "What you've already kept (dates, names, dollar amounts, written communications) does most of the work. Bringing real evidence to court is rarer than you'd think." },
        { t: 'Self-file is a real option', b: "For most claims under the small-claims cap, you can file yourself. The platform walks you through every form, every fee, and every filing deadline." },
      ],
      against: [
        { t: 'Procedural traps', b: "Filing in the wrong court, serving the wrong way, or missing a deadline can dismiss a strong case. We'll flag these as we go." },
        { t: 'Counterclaims', b: "Once you file, the other side can file a counterclaim. We'll prep you for what they're likely to raise so nothing is a surprise." },
      ],
    },
  };
  const bothSides = BOTH_SIDES[category] || BOTH_SIDES.default;
  const [expandedLaws, setExpandedLaws] = React.useState({});
  const toggleLaw = (key) => setExpandedLaws(prev => ({ ...prev, [key]: !prev[key] }));

  // ── Phase C: AI assist (mock for demo) ────────────────────────────────
  // Click "Help me say this" → typewriter reveal of a canned Maria-style
  // scenario that mentions Utah (state), security deposit / landlord (category),
  // and "no court deadline pressing" (deadline). Gives the rest of the flow
  // clean data to demo against without needing real voice or a real LLM call.
  // Original case for the demo, intentionally not overlapping with any of the
  // pre-built matters (Maria deposit, Daniel contractor, Patel auto-repair).
  // NY restaurant wage/overtime case. Picked so the canned inference lands on
  // state=NY, category=employment, deadline=none, and Phase D's employment
  // both-sides content has something real to render.
  const MOCK_SCENARIO = "I work as a server at a restaurant in Brooklyn. My employer has been keeping me 30 to 45 minutes after every shift to clean up and roll silverware, but clocking me out before any of that work. That's about 6 weeks of unpaid overtime, around 40 hours total. When I asked for back pay, the owner said that was just part of the job and threatened to cut my shifts. I have my schedule notes, copies of my timecards, and texts from coworkers saying the same thing happens to all of us. No court date pressing right now. I just want to know what my options are without losing the job.";
  const [isAssisting, setIsAssisting] = React.useState(false);
  const assistTimerRef = React.useRef(null);
  const handleAssist = () => {
    if (isAssisting) return;
    setIsAssisting(true);
    setDescription('');
    if (assistTimerRef.current) { clearInterval(assistTimerRef.current); assistTimerRef.current = null; }
    let idx = 0;
    setTimeout(() => {
      assistTimerRef.current = setInterval(() => {
        idx += 4;
        if (idx >= MOCK_SCENARIO.length) {
          setDescription(MOCK_SCENARIO);
          setIsAssisting(false);
          clearInterval(assistTimerRef.current);
          assistTimerRef.current = null;
        } else {
          setDescription(MOCK_SCENARIO.substring(0, idx));
        }
      }, 18);
    }, 220);
  };
  React.useEffect(() => () => {
    if (assistTimerRef.current) clearInterval(assistTimerRef.current);
  }, []);

  const isWelcome = step === 0;
  const isOutcome = step === 4;
  const showProgress = step >= 1 && step <= 3 && !outOfArea && !langPicker && !spanishSoon;
  const totalWorkSteps = 3; // story, read-back, both-sides (verdict is outcome screen)

  /* ─── Step content renderers ─── */

  const stepWelcome = () => (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <p style={{ fontSize: 13.5, color: THEME.textDim, lineHeight: 1.6, margin: 0 }}>
        Building a case takes time. The good news, you're not doing this alone. We'll go through every piece of your story together. Your facts. Our framework. Your call, every step. There's no rush. We're not going anywhere.
      </p>
      <p style={{ fontSize: 13.5, color: THEME.textDim, lineHeight: 1.6, margin: 0 }}>
        Three quick screens after this, and then we map out where you're strong, where you'll want to be ready, and how to take the case on together.
      </p>
    </div>
  );

  const stepStory = () => (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }} data-coach="intake-story">
      <textarea
        value={description}
        onChange={(e) => setDescription(e.target.value)}
        placeholder="Just talk it out. What's going on, when did it start, who's the other side, what have you tried so far. No legal words required. We'll take care of those."
        rows={7}
        style={{
          width: '100%', boxSizing: 'border-box',
          padding: '14px 16px', borderRadius: 12,
          background: THEME.mode === 'dark' ? 'rgba(255,255,255,0.025)' : 'rgba(255,255,255,0.65)',
          border: `1.5px solid ${THEME.line}`,
          color: THEME.text, fontSize: 13.5, lineHeight: 1.6,
          fontFamily: 'inherit', resize: 'vertical',
          outline: 'none',
        }}
      />
      <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
        <button
          type="button"
          onClick={handleVoice}
          disabled={!voiceSupported}
          data-coach="intake-voice"
          title={!voiceSupported ? "Voice not supported in this browser. Try Chrome or Safari." : (isRecording ? "Stop recording" : "Speak instead of typing")}
          style={{
            padding: '9px 14px', borderRadius: 8,
            background: isRecording ? 'rgba(224, 100, 100, 0.20)' : 'rgba(20, 28, 40, 0.55)',
            border: `1px solid ${isRecording ? '#E06464' : 'rgba(255,255,255,0.10)'}`,
            color: isRecording ? '#FF8A8A' : '#F2F4F8',
            fontSize: 12.5, fontWeight: 500,
            cursor: voiceSupported ? 'pointer' : 'not-allowed',
            opacity: voiceSupported ? 1 : 0.5,
            display: 'inline-flex', alignItems: 'center', gap: 8,
            fontFamily: 'inherit',
          }}>
          <Icon d={Icons.mic} size={14} stroke="currentColor" sw={1.8}/>
          {isRecording ? 'Listening, tap to stop' : 'Speak it instead'}
        </button>
        <button
          type="button"
          onClick={handleAssist}
          disabled={isAssisting}
          data-coach="intake-assist"
          style={{
            padding: '9px 14px', borderRadius: 8,
            background: 'rgba(20, 28, 40, 0.55)',
            border: `1px solid rgba(255,255,255,0.10)`,
            color: '#F2F4F8',
            fontSize: 12.5, fontWeight: 500,
            cursor: isAssisting ? 'wait' : 'pointer',
            display: 'inline-flex', alignItems: 'center', gap: 8,
            fontFamily: 'inherit',
          }}>
          <Icon d={Icons.sparkle} size={14} stroke="currentColor" fill="currentColor"/>
          {isAssisting ? 'Drafting your story' : 'Help me say this'}
        </button>
      </div>
      <div style={{ fontSize: 11.5, color: THEME.textDim, lineHeight: 1.5, padding: '4px 4px 0' }}>
        You can skip this and we'll just ask a few questions instead. Sharing in your own words helps us get the bigger picture.
      </div>
    </div>
  );

  const chip = (selected, onClick, label, sub) => (
    <div
      onClick={onClick}
      role="button"
      tabIndex={0}
      aria-pressed={selected}
      className="intake-focusable"
      onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(); } }}
      style={{
      padding: '9px 12px', borderRadius: 10, cursor: 'pointer',
      background: selected ? (THEME.mode === 'dark' ? 'rgba(105,134,191,0.12)' : 'rgba(105,134,191,0.08)') : THEME.surface,
      border: `1.5px solid ${selected ? THEME.blue : THEME.line}`,
      display: 'flex', alignItems: 'center', gap: 10,
      transition: 'background 0.18s ease, border-color 0.18s ease',
    }}>
      {/* Canonical radio (Target 11) — dark navy Btn-primary aesthetic when
         selected, white inner dot. Matches the radio recipe across the rest
         of the demo (service-method, etc.). */}
      <div style={{
        width: 16, height: 16, borderRadius: 8,
        border: `1.5px solid ${selected ? 'rgba(242,244,248,0.18)' : THEME.lineStrong}`,
        background: selected ? (THEME.mode === 'dark' ? '#1A2230' : '#2A3640') : 'transparent',
        flexShrink: 0,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        transition: 'background 0.18s ease, border-color 0.18s ease',
      }}>
        {selected && <div style={{ width: 5, height: 5, borderRadius: 3, background: '#F2F4F8' }}/>}
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 12.5, color: THEME.text, fontWeight: 600 }}>{label}</div>
        {sub && <div style={{ fontSize: 11, color: THEME.textDim, marginTop: 1 }}>{sub}</div>}
      </div>
    </div>
  );

  // Standard section-header style for the intake modal. Uses the new
  // THEME.textHeader token so dark + light mode stay consistent.
  const sectionHeader = {
    fontSize: 11.5, color: THEME.textHeader,
    letterSpacing: 1.1, textTransform: 'uppercase',
    fontWeight: 600, marginBottom: 10,
  };
  /* Step 2 (The basics) — split into 3 focused sub-steps that slide in.
     Sub-step 0: where it happened · 1: closest fit · 2: deadline pressure.
     Each sub-step has its own description so users aren't asked to make
     three decisions on one screen. The chip→tile→Continue pattern is the
     same across all three. */
  const READBACK_SUBSTEPS = [
    {
      eyebrow: 'Where did this happen?',
      desc: 'Different states have different rules. We use this to surface the right framework. Once we add more jurisdictions this becomes a dropdown.',
      content: (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 8 }}>
          {Object.values(JURISDICTIONS).map(jj => chip(
            state === jj.code,
            () => choose('state', jj.code),
            jj.name,
            jj.posture === 'sandbox' ? 'Sandbox-authorized' : 'Educational framework',
          ))}
          {/* Out-of-area option (Gap 1). Someone whose case isn't in a
              supported state needs an honest answer, not a forced wrong pick.
              Selecting this routes to the "not yet" screen on Continue. Spans
              the full grid width so it reads as a distinct, separate choice. */}
          <div style={{ gridColumn: '1 / -1' }}>
            {chip(
              state === 'OTHER',
              () => choose('state', 'OTHER'),
              'My case is somewhere else',
              "A different state, or you're not sure",
            )}
          </div>
        </div>
      ),
    },
    {
      eyebrow: 'What kind of case is it?',
      desc: 'Closest fit. You can change this later. None of these locks you into a path.',
      content: (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 8 }}>
          {TRIAGE_CATEGORIES.map(c => chip(
            category === c.id,
            () => choose('category', c.id),
            c.label,
            c.hint,
          ))}
        </div>
      ),
    },
    {
      eyebrow: 'Anything pressing on a deadline?',
      desc: 'If something is time-sensitive we route differently. No deadline is fine. The self-paced workflow fits perfectly.',
      content: (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
          {DEADLINE_OPTIONS.map(d => chip(
            deadline === d.id,
            () => choose('deadline', d.id),
            d.label,
            d.sub,
          ))}
        </div>
      ),
    },
  ];

  const stepReadback = () => {
    const cur = READBACK_SUBSTEPS[subStep];
    return (
      <div key={subStep} style={{ display: 'flex', flexDirection: 'column', gap: 14, animation: 'intakeStepIn 0.32s cubic-bezier(0.22, 1, 0.36, 1) both' }} data-coach="intake-readback">
        <div>
          <div style={{ ...sectionHeader, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
            <span>{cur.eyebrow}</span>
            <span style={{ fontSize: 10, color: THEME.textDim, letterSpacing: 0.6, fontWeight: 600 }}>
              · {subStep + 1} of {READBACK_SUBSTEPS.length}
            </span>
          </div>
          <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.55, marginTop: 4, marginBottom: 8 }}>
            {cur.desc}
          </div>
        </div>
        {cur.content}
      </div>
    );
  };

  const renderItem = (item, key, accentColor) => {
    const isOpen = !!expandedLaws[key];
    const hasLaws = !!(item.laws && item.laws.length);
    return (
      <div>
        <div
          onClick={() => hasLaws && toggleLaw(key)}
          style={{ cursor: hasLaws ? 'pointer' : 'default' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 2 }}>
            <span style={{ fontSize: 12, color: THEME.text, fontWeight: 600, flex: 1 }}>{item.t}</span>
            {hasLaws && (
              <span style={{
                fontSize: 9.5, color: accentColor, letterSpacing: 0.4, fontWeight: 700,
                textTransform: 'uppercase',
                display: 'inline-flex', alignItems: 'center', gap: 3,
              }}>
                {isOpen ? 'Hide laws' : 'Show laws'}
                <span style={{ display: 'inline-block', transition: 'transform 0.2s ease', transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)', fontSize: 8, lineHeight: 1 }}>▾</span>
              </span>
            )}
          </div>
          <div style={{ fontSize: 11.5, color: THEME.textDim, lineHeight: 1.55 }}>{item.b}</div>
        </div>
        {hasLaws && (
          <div style={{
            display: 'grid',
            gridTemplateRows: isOpen ? '1fr' : '0fr',
            transition: 'grid-template-rows 0.32s cubic-bezier(0.22, 1, 0.36, 1)',
          }}>
            <div style={{ minHeight: 0, overflow: 'hidden' }}>
              <div style={{
                marginTop: 8, padding: '10px 12px', borderRadius: 8,
                background: 'rgba(255,255,255,0.03)',
                border: `1px solid ${accentColor}26`,
                display: 'flex', flexDirection: 'column', gap: 8,
                opacity: isOpen ? 1 : 0,
                transition: 'opacity 0.25s ease',
              }}>
                {item.laws.map((law, j) => (
                  <div key={j}>
                    <div style={{
                      fontFamily: 'var(--font-mono, "SF Mono", monospace)',
                      fontSize: 10.5, color: accentColor, letterSpacing: 0.3, fontWeight: 600, marginBottom: 3,
                    }}>{law.cite}</div>
                    <div style={{ fontSize: 11, color: THEME.textDim, lineHeight: 1.55 }}>{law.text}</div>
                  </div>
                ))}
              </div>
            </div>
          </div>
        )}
      </div>
    );
  };

  const stepBothSides = () => (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 22 }} data-coach="intake-bothsides">
      <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.55 }}>
        We owe you the full picture, not just the good news. Here's what looks like it works in your favor, and here's what the other side might bring up. Knowing both is how you walk in confident.
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <div style={{
          padding: '14px 14px', borderRadius: 12,
          background: 'rgba(123, 202, 158, 0.08)',
          border: '1px solid rgba(123, 202, 158, 0.30)',
        }}>
          <div style={{ fontSize: 10.5, color: '#7BCA9E', letterSpacing: 1.1, textTransform: 'uppercase', fontWeight: 700, marginBottom: 10 }}>
            In your favor
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
            {bothSides.favor.map((item, i) => (
              <div key={i} style={{ animation: `intakeStepIn 0.32s ease-out ${0.05 + i * 0.07}s both` }}>
                {renderItem(item, `f${i}`, '#7BCA9E')}
              </div>
            ))}
          </div>
        </div>
        <div style={{
          padding: '14px 14px', borderRadius: 12,
          background: 'rgba(242, 197, 61, 0.06)',
          border: '1px solid rgba(242, 197, 61, 0.28)',
        }}>
          <div style={{ fontSize: 10.5, color: '#F2C53D', letterSpacing: 1.1, textTransform: 'uppercase', fontWeight: 700, marginBottom: 10 }}>
            What to be ready for
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
            {bothSides.against.map((item, i) => (
              <div key={i} style={{ animation: `intakeStepIn 0.32s ease-out ${0.18 + i * 0.07}s both` }}>
                {renderItem(item, `a${i}`, '#F2C53D')}
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );

  const stepOutcome = () => (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }} data-coach="intake-outcome">
      <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.55 }}>
        Here's what we'd recommend based on what you told us. Pick the one that feels right today. You can change your mind any time. Any path can switch to any other later.
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        {['self', 'hybrid', 'attorney'].map(key => {
          const o = OUTCOMES[key];
          const isSelected = effectivePath === key;
          const isRecommended = outcome === key;
          return (
            <div key={key} onClick={() => setSelectedPath(key)} style={{
              padding: '14px 16px', borderRadius: 12, cursor: 'pointer',
              // Flat subtle background + 1px border, mirrors form-field card
              // surface language. Selection reads via slightly stronger bg tint,
              // accent border tone, and the existing accent left-bar pill.
              background: isSelected
                ? `${o.tone}1C`
                : 'rgba(255,255,255,0.025)',
              border: isSelected
                ? `1px solid ${o.tone}66`
                : `1px solid ${THEME.line}`,
              opacity: isSelected ? 1 : 0.78,
              position: 'relative',
              transition: 'opacity 0.18s ease, background 0.22s ease, border-color 0.22s ease',
            }}>
              {isSelected && (
                <span aria-hidden="true" style={{
                  position: 'absolute', top: 8, bottom: 8, left: 4,
                  width: 4, borderRadius: '6px 0 0 6px',
                  background: o.tone,
                }}/>
              )}
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8, flexWrap: 'wrap' }}>
                <div style={{
                  fontSize: 9.5, fontWeight: 700, padding: '3px 8px', borderRadius: 4,
                  background: o.tone, color: '#0F1419', letterSpacing: 0.6,
                }}>{o.label.toUpperCase()}</div>
                {isRecommended && (
                  // Mirrors the canonical form-field helper text (.ds-fld__helper in
                  // form-fields.jsx): 11.5px, line-height 1.4, textDim color, regular
                  // weight, no italic, no letter-spacing. Reads as ambient guidance
                  // in the exact same visual language as the form active-state hint.
                  <div style={{
                    fontSize: 11.5, lineHeight: 1.4, fontWeight: 400,
                    color: THEME.textDim,
                  }}>recommended for your case</div>
                )}
              </div>
              <div style={{ fontFamily: 'Fraunces', fontSize: 16, color: THEME.text, fontWeight: 500, marginBottom: 6, lineHeight: 1.35 }}>
                {o.title}
              </div>
              <div style={{ fontSize: 12, color: THEME.textDim, lineHeight: 1.55 }}>
                {o.body}
              </div>
            </div>
          );
        })}
      </div>
      <NonAttorneyDisclosure jx={state.toLowerCase() === 'ut' ? 'utah' : 'ny'} compact/>
    </div>
  );

  /* ─── Step routing ─── */

  const STEP_TITLES = [
    { eyebrow: 'Welcome', title: "Hey. Glad you're here.", sub: null },
    { eyebrow: 'Step 1 of 3 · Your story', title: "What's going on? Tell us in your own words.", sub: "Just talk it out. Don't worry about legal words. That's our job." },
    { eyebrow: 'Step 2 of 3 · The basics', title: "Sounds like this is what we're working with.", sub: "Tell us if any of this is off. You're the one who knows your story." },
    { eyebrow: 'Step 3 of 3 · Both sides', title: "Where you're strong. Where you'll want to be ready.", sub: null },
    { eyebrow: 'Path forward', title: 'Pick the path that feels right.', sub: null },
  ];

  // Out-of-area terminal screen (Gap 1). Honest scoping, no legal advice and
  // no characterization of their matter: we only operate where we're
  // authorized, the right court + rules differ by state, and we won't point
  // someone down the wrong path for a place we don't cover yet. Offers a
  // waitlist + a neutral pointer to local help (bar referral / legal aid).
  const stepOutOfArea = () => {
    const supported = Object.values(JURISDICTIONS).map(jj => jj.name).join(' and ');
    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 14, animation: 'intakeStepIn 0.32s cubic-bezier(0.22, 1, 0.36, 1) both' }}>
        <div style={{ fontSize: 13.5, color: THEME.text, lineHeight: 1.6 }}>
          Right now Hello Court can only help with cases in {supported}. The
          right court and the rules are different in every state, and we won't
          point you down the wrong path for somewhere we don't cover yet.
        </div>
        <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.6 }}>
          We're adding new states. Join the waitlist and we'll let you know the
          moment we reach yours. In the meantime, two kinds of public resources
          exist in most states and can help you find local options: the{' '}
          <strong style={{ color: THEME.text, fontWeight: 600 }}>state bar association</strong>{' '}
          (most run a lawyer-referral service) and{' '}
          <strong style={{ color: THEME.text, fontWeight: 600 }}>legal-aid organizations</strong>{' '}
          (free or low-cost help if you qualify). Hello Court isn't affiliated
          with these and doesn't vet them. They're a starting point you can look
          up for your state.
        </div>
      </div>
    );
  };

  // Language gate (tier-1 i18n). The very first screen, before Welcome. Picking
  // a language advances on click — English continues the flow; Spanish routes to
  // an honest "coming soon" + waitlist (full Spanish is a v2 effort: UI +
  // legal-content translation under Atticus/Quentin review). Bilingual labels.
  const langTile = (code, label, sub) => (
    <div
      onClick={() => setLang(code)}
      role="button"
      tabIndex={0}
      aria-label={label}
      className="intake-focusable"
      onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setLang(code); } }}
      onMouseEnter={(e) => { e.currentTarget.style.borderColor = THEME.blue; }}
      onMouseLeave={(e) => { e.currentTarget.style.borderColor = THEME.line; }}
      style={{
        padding: '18px 18px', borderRadius: 12, cursor: 'pointer',
        background: THEME.surface, border: `1.5px solid ${THEME.line}`,
        transition: 'border-color 0.18s ease',
      }}>
      <div style={{ fontFamily: 'Fraunces', fontSize: 18, color: THEME.text, fontWeight: 500 }}>{label}</div>
      <div style={{ fontSize: 12, color: THEME.textDim, marginTop: 3 }}>{sub}</div>
    </div>
  );
  const stepLanguage = () => (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14, animation: 'intakeStepIn 0.32s cubic-bezier(0.22, 1, 0.36, 1) both' }}>
      <div style={{ fontSize: 13, color: THEME.textDim, lineHeight: 1.6 }}>
        We'll guide you in your language. / Te guiaremos en tu idioma.
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 10 }}>
        {langTile('en', 'English', 'Continue in English')}
        {langTile('es', 'Español', 'Continuar en español')}
      </div>
    </div>
  );
  const stepSpanishSoon = () => (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14, animation: 'intakeStepIn 0.32s cubic-bezier(0.22, 1, 0.36, 1) both' }}>
      <div style={{ fontSize: 13.5, color: THEME.text, lineHeight: 1.6 }}>
        Estamos creando la versión en español de Hello Court con el mismo cuidado
        que la versión en inglés. Todavía no está lista, y preferimos esperar a
        hacerlo bien antes de guiarte en un asunto legal.
      </div>
      <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.6 }}>
        Apúntate a la lista y te avisaremos en cuanto el español esté disponible.
        Mientras tanto, puedes volver y continuar en inglés.
      </div>
    </div>
  );

  const renderStep = () => {
    if (langPicker) return stepLanguage();
    if (spanishSoon) return stepSpanishSoon();
    if (outOfArea) return stepOutOfArea();
    if (step === 0) return stepWelcome();
    if (step === 1) return stepStory();
    if (step === 2) return stepReadback();
    if (step === 3) return stepBothSides();
    return stepOutcome();
  };

  // Header copy. Language picker + Spanish-coming-soon + out-of-area each
  // override the normal step header so the framing matches the screen.
  const cur = langPicker
    ? { eyebrow: 'Language · Idioma', title: 'First, choose your language.', sub: 'Primero, elige tu idioma.' }
    : spanishSoon
    ? { eyebrow: 'Español', title: 'El español llegará pronto.', sub: null }
    : outOfArea
    ? { eyebrow: 'Before we go further', title: "Let's make sure we can help.", sub: null }
    : STEP_TITLES[step];
  // Step 2 advance gating depends on which sub-step (state/category/deadline)
  // the user is on. Each sub-step requires its own selection before continuing.
  const canAdvance = (
    step === 0 ? true :
    step === 1 ? true : // description optional
    step === 2 ? (
      subStep === 0 ? !!state :
      subStep === 1 ? !!category :
      !!deadline
    ) :
    step === 3 ? true :
    true
  );
  const continueLabel = step === 3 ? 'See where this lands' : 'Continue';

  // Continue handler — advances sub-step within step 2, then advances to next
  // main step. When entering step 2 from step 1, reset subStep to 0.
  const advanceForward = () => {
    if (!canAdvance) return;
    // Out-of-area branch (Gap 1): if they picked "somewhere else" on the
    // where-did-this-happen sub-step, divert to the honest "not yet" screen
    // instead of continuing into category/deadline/verdict.
    if (step === 2 && subStep === 0 && state === 'OTHER') {
      setOutOfArea(true);
      return;
    }
    if (step === 2 && subStep < READBACK_SUBSTEPS.length - 1) {
      setSubStep(subStep + 1);
    } else {
      if (step === 1) setSubStep(0); // entering step 2 fresh
      setStep(step + 1);
    }
  };

  // Back handler — decrements sub-step within step 2, then goes to prev step.
  // When re-entering step 2 from step 3, restore the last sub-step (deadline).
  const advanceBack = () => {
    // From the out-of-area screen, Back returns to the where-did-this-happen
    // picker (clearing the diversion) rather than stepping back a main step.
    if (spanishSoon) { setLang(null); setWaitlistOpen(false); return; }
    if (outOfArea) { setOutOfArea(false); setWaitlistOpen(false); return; }
    if (step === 2 && subStep > 0) {
      setSubStep(subStep - 1);
    } else {
      if (step === 3) setSubStep(READBACK_SUBSTEPS.length - 1);
      setStep(step - 1);
    }
  };

  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 90,
      background: THEME.mode === 'dark' ? 'rgba(8,12,18,0.78)' : 'rgba(20,28,38,0.32)',
      WebkitBackdropFilter: 'blur(20px) saturate(140%)', backdropFilter: 'blur(20px) saturate(140%)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 40,
      animation: 'intakeFadeIn 0.28s ease-out',
    }}>
      <style>{`
        @keyframes intakeFadeIn { from { opacity: 0; } to { opacity: 1; } }
        @keyframes intakeRiseIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
        @keyframes intakeStepIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
        @keyframes waitlistSlideIn { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: translateX(0); } }
        @keyframes toastIn { from { opacity: 0; transform: translate(-50%, -20px); } to { opacity: 1; transform: translate(-50%, 0); } }
        @keyframes toastOut { from { opacity: 1; transform: translate(-50%, 0); } to { opacity: 0; transform: translate(-50%, -20px); } }
        /* Keyboard focus ring (WCAG 2.4.7) for the custom clickable tiles +
           the icon close button. :focus-visible = keyboard only, no mouse halo.
           #9FB3FF reads AA against both the dark and light modal backdrops. */
        .intake-focusable:focus-visible { outline: 2px solid #9FB3FF; outline-offset: 2px; border-radius: 12px; }
      `}</style>

      {/* Brand lockup — pinned to the top-left of the flow (moved out of the
          centered card 2026-06-02). Theme-aware; only renders while triage is
          open. The global bottom-left page-brand pill was removed. */}
      <img
        src={THEME.mode === 'dark'
          ? 'assets/hellocourt-home-logo-dark.svg?v=20260608'
          : 'assets/hellocourt-home-logo-light.svg?v=20260608'}
        alt="Hello Court"
        style={{ position: 'absolute', top: 28, left: 32, height: 22, width: 'auto', zIndex: 2 }}
      />
      {/* Close — pinned to the top-right (moved out of the footer 2026-06-02),
          mirroring the top-left logo. Closes/saves via onClose on every step. */}
      <button
        onClick={onClose}
        className="intake-focusable"
        onMouseEnter={(e) => { e.currentTarget.style.background = THEME.mode === 'dark' ? '#2E3C4B' : '#D7DEE2'; }}
        onMouseLeave={(e) => { e.currentTarget.style.background = THEME.bgElev; }}
        style={{
          position: 'absolute', top: 24, right: 28, zIndex: 3,
          padding: '7px 16px', borderRadius: 999, cursor: 'pointer',
          border: `1px solid ${THEME.lineStrong}`, background: THEME.bgElev, color: THEME.text,
          fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit', letterSpacing: 0.2,
          display: 'inline-flex', alignItems: 'center',
          transition: 'background 0.18s ease',
        }}>
        {t('Close', 'Cerrar')}
      </button>
      <div style={{
        width: 'min(640px, 100%)', maxHeight: '100%',
        display: 'flex', flexDirection: 'column',
        animation: 'intakeRiseIn 0.42s cubic-bezier(0.22, 1, 0.36, 1) both',
      }}>
        {/* Header — no inner card chrome; sits directly on the blurred backdrop */}
        <div style={{ padding: '20px 24px 14px' }}>
          <div style={{ fontSize: 10.5, color: THEME.textDim, letterSpacing: 1.4, textTransform: 'uppercase', fontWeight: 600, marginBottom: 4 }}>
            {cur.eyebrow}
          </div>
          <div style={{ fontFamily: 'Fraunces', fontSize: 21, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, lineHeight: 1.25 }}>
            {cur.title}
          </div>
          {cur.sub && <div style={{ fontSize: 12.5, color: THEME.textDim, marginTop: 6, lineHeight: 1.55 }}>{cur.sub}</div>}
        </div>

        {/* Progress bar — only visible during the 3 working steps */}
        {showProgress && (
          <div style={{ padding: '0 24px', marginTop: 12 }}>
            <div style={{ display: 'flex', gap: 5 }}>
              {[1, 2, 3].map(i => (
                <div key={i} style={{
                  flex: 1, height: 3, borderRadius: 2,
                  background: i <= step ? THEME.blue : THEME.lineSoft,
                }}/>
              ))}
            </div>
          </div>
        )}

        {/* Content — keyed by step so transitions animate fresh each navigation.
            Scrollable so the footer (CTA buttons) stays visible regardless of
            content height. */}
        <div key={langPicker ? 'lang' : spanishSoon ? 'es' : outOfArea ? 'ooa' : step} style={{ padding: '20px 24px', flex: 1, minHeight: 0, overflow: 'auto', animation: 'intakeStepIn 0.30s ease-out both' }}>
          {renderStep()}
        </div>

        {/* Footer — canonical Btn (pill, real <button>, AA focus rings).
            Back/Continue routes through advanceBack/advanceForward so step 2
            sub-step navigation works without leaking sub-step state into the
            button handlers. */}
        <div style={{ padding: '14px 24px 20px', display: 'flex', alignItems: 'center', gap: 12 }}>
          {(!langPicker && !isOutcome && (spanishSoon || step > 0 || (step === 2 && subStep > 0))) && (
            <Btn kind="secondary" onClick={advanceBack}>{t('Back', 'Atrás')}</Btn>
          )}
          {/* Save & quit hint — visual stub. Tells the user that closing
              doesn't lose progress; matches the resume-banner story on the
              welcome landing. Real persistence wired later. */}
          {!isWelcome && !isOutcome && !outOfArea && !langPicker && !spanishSoon && (
            <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: THEME.textDim, marginLeft: step > 0 ? 8 : 0 }}>
              <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 14, color: THEME.success, fontVariationSettings: "'FILL' 1" }}>cloud_done</span>
              Saved · resume any time
            </div>
          )}
          <div style={{ flex: 1 }}/>
          {langPicker ? null : (outOfArea || spanishSoon) ? (
            waitlistDone ? (
              // Confirmed state — the toast already fired; this is the durable cue.
              <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12.5, color: THEME.success, fontWeight: 600 }}>
                <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 16, fontVariationSettings: "'FILL' 1" }}>check_circle</span>
                {t("You're on the list", 'Estás en la lista')}
              </div>
            ) : waitlistOpen ? (
              // Email field slides in; as it takes layout width the Close button
              // is pushed left — the "buttons slide left" effect.
              <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, animation: 'waitlistSlideIn 0.46s cubic-bezier(0.65, 0, 0.35, 1) both' }}>
                {/* Canonical design-system field (form-fields.jsx TextField). */}
                <div style={{ width: 220 }}>
                  <TextField
                    type="email"
                    size="sm"
                    autoFocus
                    value={waitlistEmail}
                    onChange={(e) => setWaitlistEmail(e.target.value)}
                    onKeyDown={(e) => { if (e.key === 'Enter') submitWaitlist(); }}
                    label={t('Email', 'Correo')}
                    aria-label={t('Email for the waitlist', 'Correo para la lista')}
                  />
                </div>
                <Btn kind="primary" disabled={!waitlistValid} onClick={submitWaitlist}>{t('Sign up', 'Enviar')}</Btn>
              </div>
            ) : (
              <Btn kind="primary" onClick={() => setWaitlistOpen(true)}>{t('Join the waitlist', 'Unirme a la lista')}</Btn>
            )
          ) : !isOutcome ? (
            <Btn kind="primary" disabled={!canAdvance} onClick={advanceForward}>
              {isWelcome ? "Let's get started" : continueLabel}
            </Btn>
          ) : (
            <>
              <Btn kind="secondary" onClick={() => setStep(0)}>Start over</Btn>
              <Btn kind="primary" onClick={() => onFinish && onFinish({ description, state, category, deadline, outcome: effectivePath, recommended: outcome })}>
                {OUTCOMES[effectivePath].cta}
              </Btn>
            </>
          )}
        </div>
      </div>

      {/* Confirmation toast — fires on waitlist signup, auto-dismisses. Scoped
          to the modal overlay (a positioned ancestor), bottom-center. */}
      {toast && (
        <div role="status" aria-live="polite" style={{
          position: 'absolute', top: 24, left: '50%', transform: 'translateX(-50%)',
          zIndex: 120, display: 'inline-flex', alignItems: 'center', gap: 8,
          padding: '7px 7px 7px 12px', borderRadius: 8,
          background: THEME.mode === 'dark' ? 'rgba(26,34,48,0.97)' : 'rgba(255,255,255,0.98)',
          border: `1px solid ${THEME.line}`,
          boxShadow: '0 8px 24px rgba(0,0,0,0.26)',
          animation: toastLeaving
            ? 'toastOut 0.5s cubic-bezier(0.65, 0, 0.35, 1) both'
            : 'toastIn 0.5s cubic-bezier(0.65, 0, 0.35, 1) both',
          maxWidth: 'min(360px, 92%)',
        }}>
          <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 16, color: THEME.success, fontVariationSettings: "'FILL' 1" }}>check_circle</span>
          <span style={{ fontSize: 12, color: THEME.text, fontWeight: 500, lineHeight: 1.4 }}>{toast}</span>
          <button
            onClick={dismissToast}
            aria-label={t('Dismiss', 'Cerrar')}
            className="intake-focusable"
            onMouseEnter={(e) => { e.currentTarget.style.color = THEME.text; }}
            onMouseLeave={(e) => { e.currentTarget.style.color = THEME.textDim; }}
            style={{
              flexShrink: 0, width: 22, height: 22, borderRadius: 4, marginLeft: 2,
              border: 'none', background: 'transparent', cursor: 'pointer', color: THEME.textDim,
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            }}>
            <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 16 }}>close</span>
          </button>
        </div>
      )}
    </div>
  );
};

window.TriageIntake = TriageIntake;
