/* Hello Court v3. Maria Vargas demo arc.
   Pro-se only · Utah + NY · UPL-discipline patterns · Atticus framing · Tier 1/2/3 architecture.
   Single self-contained module: data + screens + chrome.
*/

/* ================================ DATA ================================ */

const V3_MATTERS = {
  maria: {
    id: 'maria', tier: 1, jurisdiction: 'utah',
    title: 'Vargas v. Westhaven Properties',
    short: 'Vargas v. Westhaven',
    client: { name: 'Maria Vargas', initials: 'MV', city: 'Salt Lake City, UT', occupation: 'Restaurant server' },
    opposing: { name: 'Westhaven Properties LLC', initials: 'WP' },
    type: 'Security deposit recovery',
    statute: 'Utah § 57-17-3',
    statuteNote: '2× wrongful retention available',
    court: 'Salt Lake County Justice Court',
    amount: 1800, amountLabel: '$1,800',
    potentialAmount: 3860, potentialAmountLabel: '$3,860 total recovery sought',
    stage: 'Preparation', stageIdx: 1,
    readiness: 87,
    readinessBreakdown: {
      elements: { score: 36, max: 40, label: 'Element completeness', missing: 'All 4 statutory elements have at least one affirmed evidence item. Slight deduction because Element 4 (wrongful retention) is supported by only one document. Courts prefer redundancy on the headline element.' },
      mapping: { score: 28, max: 30, label: 'Evidence-element mapping', missing: '5 of 7 evidence items are affirmed against an element. The 30-day timeline event and the second walkthrough-photo set have not yet been confirmed in the affirmation gate.' },
      statutory: { score: 13, max: 15, label: 'Statutory accuracy', missing: 'Utah § 57-17-3 cite verified current as of 2026-04-01 by Atticus. Semi-annual re-verification scheduled (next: 2026-10-01).' },
      procedural: { score: 10, max: 15, label: 'Procedural prep', missing: 'Service-of-process method not yet selected. Fee-waiver eligibility not yet determined. Both can finalize in about 5 minutes from the Filing screen.' },
    },
    summary: 'Maria moved out of her apartment on March 15. She gave proper notice, returned keys, and left the unit clean. Westhaven did not return her $1,800 deposit or send an itemized statement within 30 days as Utah § 57-17-3 requires.',

    /* ── What's next: V3Home action items derived from open readiness factors.
       Per-matter so Daniel/Sarah don't see Maria's "landlord" copy (audit 0.40.2). ── */
    whatsNextIntro: 'Two small things, then your packet is ready to file.',
    whatsNext: [
      { label: 'Choose how you want to serve the landlord', sub: 'Certified mail or process server', screen: 'filing' },
      { label: 'Read and affirm your final document set', sub: 'Statement of Claim · Exhibit Index · Damages Worksheet', screen: 'documents' },
    ],

    /* ── Case caption: court-form header used at top of every filing-packet page ── */
    caption: {
      caseNumber: 'UT-SLC-2026-04772',
      court: 'Salt Lake County Justice Court',
      filingDate: 'April 28, 2026',
      plaintiff: 'Maria Vargas',
      plaintiffPhone: '(801) 555-0142',
      plaintiffEmail: 'maria.vargas.case@example.com',
      defendant: 'Westhaven Properties, LLC',
    },

    /* ── Case Snapshot: at-a-glance overview ── */
    snapshot: {
      natureOfClaim: 'Wrongful retention of security deposit and failure to provide itemized statement under Utah Code § 57-17-3.',
      keyIssues: [
        'Westhaven kept Maria\'s $1,800 deposit after she vacated and returned keys.',
        'No itemized written statement of deductions was sent within the 30-day statutory window.',
        'Move-out walkthrough photos with EXIF dates show the unit was clean and undamaged.',
        'Certified-mail demand was sent April 14; no response received.',
      ],
      evidenceHighlights: [
        { label: 'Signed lease with $1,800 deposit clause', exhibit: 1 },
        { label: 'Move-out walkthrough photos (12, EXIF March 15 11:42)', exhibit: 3 },
        { label: 'Certified-mail receipt for demand letter (April 14)', exhibit: 5 },
        { label: '30-day timeline calculation (move-out → demand)', exhibit: 6 },
      ],
      reliefSought: '$3,860. Breakdown: $1,800 deposit + $1,800 statutory 2× penalty + $200 documented filing time + $60 court costs.',
      maxBalanceOwed: 'None. Maria owed no balance at move-out (rent paid in full through term).',
    },

    /* ── Statement of Claim sections, generated from her affirmed facts ── */
    statementSections: [
      { id: 'background', n: 'I', title: 'Background and Tenancy', body: 'On May 1, 2024, Plaintiff entered into a written lease with Defendant for the residential unit located at 1247 Sugar House Ave, Apt 3B, Salt Lake City, Utah. Lease term: 12 months. Monthly rent: $1,450. Security deposit: $1,800, paid in full at signing.' },
      { id: 'moveout', n: 'II', title: 'Move-Out and Return of Possession', body: 'Plaintiff provided 30-day written notice on February 13, 2026. Move-out completed on March 15, 2026. Keys returned to Defendant\'s on-site office at 12:14 PM. Plaintiff and a witness conducted a self-walkthrough; 12 timestamped photographs were taken, EXIF metadata preserved. Unit was left clean and undamaged. (Exhibit 3)' },
      { id: 'thirty-day', n: 'III', title: 'Statutory 30-Day Window', body: 'Under Utah Code § 57-17-3, the landlord must, within 30 days of the tenant vacating and returning possession, either return the security deposit in full or provide a written itemized statement of deductions and return any remaining balance. As of April 14, 2026 (30 days after move-out), Plaintiff had received neither.' },
      { id: 'demand', n: 'IV', title: 'Written Demand and Refusal', body: 'On April 14, 2026, Plaintiff sent a written demand for return of the deposit via certified mail (Exhibit 5). Tracking confirms delivery on April 16, 2026. As of the filing date of this action, Defendant has provided no response, no return of the deposit, and no itemized statement.' },
      { id: 'wrongful', n: 'V', title: 'Wrongful Retention', body: 'Defendant\'s failure to return the deposit or provide an itemized statement within 30 days constitutes wrongful retention under Utah Code § 57-17-3. The statute provides for recovery of the wrongfully withheld deposit plus an additional sum equal to twice the wrongfully withheld amount. (Utah Code § 57-17-3(5))' },
      { id: 'damages', n: 'VI', title: 'Damages Requested', body: 'Plaintiff seeks judgment in the amount of $3,860, calculated as set forth in the Damages Summary. Plaintiff has incurred documented lost paid time off addressing this matter and seeks reimbursement of court costs.' },
    ],

    /* ── Key Admissions and Undisputed Facts: distilled strongest facts ── */
    keyAdmissions: [
      { fact: 'The lease established a $1,800 security deposit, paid at signing.', source: 'Signed lease (Exhibit 1)' },
      { fact: 'Plaintiff provided 30-day written notice to vacate.', source: 'Notice with delivery confirmation (Exhibit 2)' },
      { fact: 'Plaintiff returned possession of the unit on March 15, 2026.', source: 'Key-return receipt signed by property manager (Exhibit 4)' },
      { fact: 'Defendant received Plaintiff\'s certified-mail demand on April 16, 2026.', source: 'USPS tracking confirmation (Exhibit 5)' },
      { fact: 'No itemized statement of deductions has been provided to Plaintiff.', source: 'Plaintiff testimony; no document in the record reflects one' },
      { fact: 'No portion of the $1,800 deposit has been returned.', source: 'Bank records; no incoming transfer from Defendant' },
    ],

    /* ── Multi-category damages with offset ── */
    damages: {
      lineItems: [
        { id: 'deposit', label: 'Security deposit (wrongfully retained)', calc: 'Per signed lease', amount: 1800 },
        { id: 'statutory', label: 'Statutory 2× penalty', calc: 'Utah § 57-17-3(5): 2 × $1,800 deposit', amount: 1800 },
        { id: 'lostTime', label: 'Documented lost paid time off', calc: '8 hrs × $25/hr (employer records)', amount: 200 },
        { id: 'courtCosts', label: 'Estimated court costs', calc: 'Filing + service fees', amount: 60 },
      ],
      subtotal: 3860,
      offset: { label: 'Reasonable balance owed by Plaintiff', amount: 0, note: 'Rent paid in full through end of term. No outstanding obligations.' },
      net: 3860,
      netLabel: '$3,860',
    },

    /* ── Reference Packet (confidential, plaintiff use only) ── */
    referencePacket: {
      caseNumber: 'UT-SLC-2026-04772',
      figures: { claimed: '$3,860', alreadyPaid: '$1,800 deposit (held by defendant)', maxBalance: 'None', net: '$3,860' },
      sections: [
        { n: 1, title: 'Opening Statement', use: 'Read aloud word-for-word' },
        { n: 2, title: 'Quick Oral Outline', use: 'One-page reference during testimony' },
        { n: 3, title: 'Exhibit Reference Guide', use: 'What to say for each exhibit' },
        { n: 4, title: 'Handling Their Arguments', use: 'Scripted rebuttals to common defenses' },
        { n: 5, title: 'Cross-Examination Prep', use: 'Questions from defense + your answers' },
        { n: 6, title: 'Questions From the Judge', use: 'Anticipate and answer confidently' },
        { n: 7, title: 'Legal Rules & Key Points', use: 'Utah law reminders, what to say if challenged' },
        { n: 8, title: 'Math & Damages Summary', use: 'All figures verified, hand to judge if asked' },
        { n: 9, title: 'What to Expect in Court', use: 'Step-by-step courtroom procedure' },
      ],
      opening: {
        deliveryNote: 'Estimated delivery: 2–3 minutes. Speak slowly. Pause after each section. Look up at the judge.',
        sections: [
          { label: 'Introduction', body: '"Your Honor, my name is Maria Vargas. I am the plaintiff in this matter, and I am representing myself. I am asking this Court for judgment in the amount of $3,860 based on Westhaven Properties\' wrongful retention of my security deposit under Utah Code § 57-17-3."' },
          { label: 'What happened', body: '"On May 1, 2024, I signed a lease with Westhaven and paid an $1,800 security deposit. On February 13, 2026, I gave proper 30-day written notice. On March 15, 2026, I moved out. I returned the keys, took photographs of the unit\'s condition, and left it clean and undamaged. I have those photographs with their original timestamps preserved."' },
          { label: 'The 30-day window', body: '"Utah Code § 57-17-3 gives a landlord exactly 30 days from when a tenant vacates and returns possession. Within those 30 days the landlord must either return the deposit in full or send a written itemized statement of any deductions. Westhaven did neither."' },
          { label: 'My demand', body: '"On April 14, 30 days after I moved out, I sent a written demand by certified mail. USPS tracking confirms it was delivered on April 16. To this day, I have received no response, no statement, and no return of any portion of my deposit."', stageNote: '[Hold up Exhibit 5, the certified-mail receipt. Pause.]' },
          { label: 'What the statute provides', body: '"Utah Code § 57-17-3 subsection 5 provides that when a landlord wrongfully retains a deposit, the tenant may recover the deposit plus an additional sum equal to twice the wrongfully retained amount."' },
          { label: 'Closing', body: '"I paid the deposit in good faith, lived in the unit for nearly two years, and left it in good condition. The statute is clear, my evidence is documented, and Westhaven has had every opportunity to comply. I respectfully ask this Court to award $3,860: my deposit, the statutory penalty, my documented filing time, and court costs. Thank you."', stageNote: '[Look up. Nod. You\'re done with opening.]' },
        ],
      },
      oralOutline: [
        { n: 1, title: 'OPENING', bullets: ['Lease started May 2024, $1,800 deposit', 'Moved out March 15, 2026 with proper notice', 'Westhaven kept the deposit, sent no itemized statement', 'Requesting judgment of $3,860'] },
        { n: 2, title: 'THE LEASE IS THE CONTRACT', bullets: ['Signed May 1, 2024', '$1,800 deposit clause, paid at signing', '12-month term, renewed; no balance owed at move-out'], exhibitRef: 'Exhibit 1' },
        { n: 3, title: 'NOTICE & MOVE-OUT', bullets: ['30-day written notice Feb 13', 'Move-out March 15, 12:14 PM', 'Keys returned to property manager', '12 photographs with EXIF metadata'], exhibitRef: 'Exhibits 2, 3, 4' },
        { n: 4, title: 'THE 30-DAY WINDOW', bullets: ['Utah § 57-17-3: 30 days to return deposit OR send itemized statement', 'Window expired April 14', 'Neither was sent'] },
        { n: 5, title: 'CERTIFIED-MAIL DEMAND', bullets: ['Sent April 14 via certified mail', 'USPS tracking confirms delivery April 16', 'No response received as of filing'], exhibitRef: 'Exhibit 5' },
        { n: 6, title: 'STATUTORY PENALTY', bullets: ['§ 57-17-3(5): wrongful retention triggers 2× damages', '$1,800 deposit + $1,800 statutory = $3,600 base', 'Plus filing time and court costs = $3,860'] },
        { n: 7, title: 'DAMAGES: WHAT I AM REQUESTING', bullets: ['Deposit: $1,800', 'Statutory 2× penalty: $1,800', 'Documented lost time (8 hrs @ $25/hr): $200', 'Court costs: $60', 'Total: $3,860'] },
        { n: 8, title: 'CLOSING', bullets: ['I paid the deposit in good faith.', 'I left the unit clean and on time.', 'Westhaven had 30 days. They did nothing.', 'The statute is clear. I respectfully request $3,860.'] },
      ],
      exhibitGuide: [
        { exhibit: 1, title: 'Signed lease', proves: '$1,800 deposit clause, 12-month term, both parties\' signatures', say: '"This is the operative agreement, Your Honor. The deposit clause is on page 4."' },
        { exhibit: 2, title: '30-day notice with delivery confirmation', proves: 'Plaintiff provided proper notice to vacate on February 13, 2026', say: '"This is my written notice, sent and confirmed delivered."' },
        { exhibit: 3, title: 'Move-out photographs (12 images, EXIF preserved)', proves: 'Unit was clean and undamaged at the time of move-out, March 15, 11:42 AM', say: '"These photos are timestamped on the move-out date. Original EXIF preserved."' },
        { exhibit: 4, title: 'Key-return receipt', proves: 'Keys were returned to Westhaven\'s on-site property manager at 12:14 PM on move-out day', say: '"Property manager signed for keys at the time of return."' },
        { exhibit: 5, title: 'Certified-mail demand letter + USPS tracking', proves: 'Plaintiff sent written demand on April 14; tracking shows delivered April 16', say: '"This is my demand letter and the USPS proof of delivery."' },
        { exhibit: 6, title: '30-day timeline calculation', proves: 'Move-out (March 15) plus 30 days falls on April 14, the date the demand was sent', say: '"This shows the statutory window calculation, Your Honor."' },
      ],
      rebuttals: [
        { theySay: '"The unit had damages, so we kept the deposit to cover them."', youSay: '"Your Honor, Utah Code § 57-17-3 requires a written itemized statement of any deductions, sent within 30 days. No such statement was ever sent. Without it, the law treats the deposit as wrongfully retained regardless of any later-claimed damages. Exhibit 3 also shows the unit\'s condition at move-out."' },
        { theySay: '"We never received the demand letter."', youSay: '"Exhibit 5 includes USPS certified-mail tracking confirming delivery on April 16, 2026. Delivery is documented."' },
        { theySay: '"Plaintiff didn\'t give proper notice."', youSay: '"Exhibit 2 contains my 30-day written notice dated February 13, 2026, with delivery confirmation. The notice complies with the lease and exceeds the statutory minimum."' },
        { theySay: '"We sent an itemized statement, the plaintiff didn\'t receive it."', youSay: '"Your Honor, the statute requires the landlord to deliver the statement within 30 days. The burden is on the landlord to produce it. I respectfully ask the Court to require Defendant to produce a copy with proof of mailing dated within the statutory window."' },
      ],
      crossExam: [
        { group: 'Lease & Notice', items: [
          { q: '"You agreed to the deposit terms when you signed the lease, correct?"', a: '"Yes. The lease set a $1,800 deposit. Utah law also governs how that deposit must be returned, regardless of the lease language."' },
          { q: '"Did you give exactly 30 days\' notice?"', a: '"I gave more than 30 days. My notice was dated February 13, 2026; I moved out March 15, 2026. That\'s 30 days, exactly as required."' },
        ]},
        { group: 'Move-Out Condition', items: [
          { q: '"You took those photos yourself. How do we know they\'re accurate?"', a: '"The photos retain their original EXIF metadata, including timestamp and GPS coordinates. Exhibit 3 shows them taken at 11:42 AM on March 15 at the unit\'s address. The metadata is verifiable."' },
          { q: '"Was a representative of Westhaven present at your walkthrough?"', a: '"I returned keys to your on-site property manager at 12:14 PM. No formal walkthrough was offered or scheduled."' },
        ]},
        { group: 'The Demand Letter', items: [
          { q: '"Did you send the demand letter to the correct address?"', a: '"Exhibit 5 is addressed to Westhaven Properties at the same address listed on the lease. USPS confirms delivery to that address on April 16."' },
        ]},
      ],
      judgeQuestions: [
        { q: '"What exactly are you asking me to award you today?"', a: '"$3,860, Your Honor. $1,800 for the deposit, $1,800 statutory penalty under § 57-17-3, $200 in documented lost time, and $60 in court costs."' },
        { q: '"Did you receive any written itemized statement at any point?"', a: '"No, Your Honor. None: before, during, or after the 30-day statutory window."' },
        { q: '"Was the unit returned in the same condition you received it?"', a: '"Yes, Your Honor. Exhibit 3 contains 12 photographs with EXIF metadata showing the unit\'s condition at the time of move-out."' },
        { q: '"Did you have any outstanding rent balance at move-out?"', a: '"No, Your Honor. Rent was paid in full through the end of the term."' },
        { q: '"Why is the demand letter dated April 14?"', a: '"Move-out was March 15. The 30-day statutory window expired on April 14. I sent the demand on the day the window expired."' },
        { q: '"Have you had any communication with Defendant since the move-out?"', a: '"No substantive response, Your Honor. No itemized statement, no return of the deposit, no acknowledgment of the demand letter."' },
      ],
      legalRules: {
        dos: [
          'Stay calm. Composure helps the judge focus on the law.',
          'Address the judge as "Your Honor" every single time.',
          'Refer to exhibits by number: "As shown in Exhibit [#], Your Honor..."',
          'If you don\'t know the answer, say: "I don\'t have that in front of me, Your Honor."',
        ],
        donts: [
          'Don\'t interrupt the defendant or the judge.',
          'Don\'t argue with the judge. If overruled, say "Yes, Your Honor" and move on.',
          'Don\'t bring up anything outside your filed claim.',
          'Don\'t react visibly if Defendant says something inaccurate. Note it; address it on your turn.',
        ],
        keyPoints: [
          { title: 'Utah § 57-17-3: The 30-day rule', body: 'Within 30 days after the tenant vacates and returns possession, the landlord must (a) return the security deposit, or (b) deliver a written itemized statement of deductions and return any remaining balance. Failing both triggers § 57-17-3(5).' },
          { title: '§ 57-17-3(5): 2× recovery', body: 'When a landlord fails to comply, the court may award the wrongfully retained amount plus an additional sum equal to twice the wrongfully retained amount, plus court costs.' },
          { title: 'Burden of proof on the landlord', body: 'Once the tenant establishes the deposit was paid and not returned, and that no itemized statement was sent within 30 days, the burden shifts to the landlord to produce the statement and any underlying documentation.' },
        ],
      },
      whatToExpect: [
        { n: 1, title: 'Arrive Early', body: 'Get to the courthouse 30 minutes early. Tell the clerk you are the plaintiff in Vargas v. Westhaven Properties. Confirm the judge has your filed documents.' },
        { n: 2, title: 'Case Is Called', body: 'Both sides approach. The judge runs the proceeding (there is no jury). Address the judge as "Your Honor" at all times.' },
        { n: 3, title: 'You Speak First', body: 'As plaintiff, you present first. Deliver your opening statement from Section 1. Then walk the judge through your key exhibits in order. Pause after key points.' },
        { n: 4, title: 'Photo Evidence', body: 'Have your move-out photo binder ready. Hand the judge the printed copies if asked. Keep originals as backup.' },
        { n: 5, title: 'Defendant Responds', body: 'They will present their side. Do NOT interrupt. Take notes on anything inaccurate so you can address it calmly when it is your turn.' },
        { n: 6, title: 'Judge\'s Questions', body: 'The judge may ask either party questions at any time. Answer directly and briefly. "Yes, Your Honor" / "No, Your Honor." See Section 6.' },
        { n: 7, title: 'Judgment', body: 'The judge may rule immediately or mail you the decision within a few days. Either is normal. Stay calm and professional regardless of the outcome.' },
      ],
    },
  },
  daniel: {
    id: 'daniel', tier: 2, jurisdiction: 'utah',
    title: 'Romero v. Mountain View Homeowners',
    short: 'Romero v. Mountain View',
    client: { name: 'Daniel Romero', initials: 'DR', city: 'Provo, UT', occupation: 'Independent contractor' },
    opposing: { name: 'Mountain View Homeowners Assoc.', initials: 'MV' },
    type: 'Contract dispute (above small claims cap)',
    statute: 'Utah contract law',
    court: 'Utah District Court · 4th District',
    amount: 24000, amountLabel: '$24,000',
    stage: 'Triage', stageIdx: 0,
    readiness: 41,
    summary: 'Daniel completed a $24,000 kitchen remodel for an HOA-managed property. Final payment withheld; HOA threatening fraud counterclaim. Above $15K small-claims cap → routes to Tier 2 lawyer-handoff packet.',

    /* What's next — Tier 2 lawyer-handoff flow. Primary surface is V3LawyerPitch;
       V3Home is a secondary fallback so this copy stays generic to the handoff. */
    whatsNextIntro: 'Two small steps to a lawyer-ready packet.',
    whatsNext: [
      { label: 'Review your evidence summary for the lawyer', sub: 'Photos, contracts, payment records, all in one place', screen: 'evidence' },
      { label: 'Open the LawyerMatchPacket preview', sub: 'What a lawyer sees when you book a fixed-fee consult', screen: 'pitch' },
    ],
  },
  patel: {
    /* The key remains 'patel' for router compatibility, but the persona has
       been repurposed from Sarah Patel (NY auto-repair) to Sarah Chen (NY
       wage theft). Surface fields + readiness match Brooklyn Bites scenario
       wired up in V3_DAMAGES_*_WAGE_NY + V3_EVIDENCE_WAGE_NY + V3_DOCS_WAGE_NY. */
    id: 'patel', tier: 1, jurisdiction: 'ny',
    title: 'Chen v. Brooklyn Bites',
    short: 'Chen v. Brooklyn Bites',
    client: { name: 'Sarah Chen', initials: 'SC', city: 'Brooklyn, NY', occupation: 'Restaurant server' },
    opposing: { name: 'Brooklyn Bites LLC', initials: 'BB' },
    type: 'Wage theft · Unpaid overtime',
    statute: 'NY Lab. Law § 198 + FLSA § 207',
    statuteNote: '100% liquidated damages; attorney fees authorized (pro-se = $0)',
    court: 'NYC Civil Court · Kings County (small claims)',
    amount: 1040, amountLabel: '$1,040 (unpaid wages + overtime)',
    potentialAmount: 2100, potentialAmountLabel: '$2,100 (with 100% liquidated + filing fee)',
    stage: 'Preparation', stageIdx: 1,
    readiness: 76,
    readinessBreakdown: {
      elements: { score: 33, max: 40, label: 'Element completeness', missing: 'All 4 elements have supporting evidence. Element 2 (off-the-clock hours) leans on contemporaneous notes plus coworker texts; a sworn coworker statement at trial would strengthen it.' },
      mapping: { score: 22, max: 30, label: 'Evidence-element mapping', missing: '5 of 6 evidence items affirmed. The manager-text screenshot is uploaded but not yet affirmed against Element 2 (off-the-clock hours).' },
      statutory: { score: 13, max: 15, label: 'Statutory accuracy', missing: 'NY Lab. Law § 198 + FLSA § 207 + 12 NYCRR § 142-2.2 cites are current as of 2026-04-01 by Atticus.' },
      procedural: { score: 8, max: 15, label: 'Procedural prep', missing: 'Service method not chosen yet. For NYC Civil Court small-claims under $5K, court-mailed service is the cheap default ($10). Pick from the Filing screen.' },
    },
    summary: 'Sarah worked as a server at Brooklyn Bites for 8 weeks. She was paid straight time only despite working 50 hours each week. She also performed 12 hours of off-the-clock prep work the manager required but never recorded. NY Lab. Law § 198 + FLSA § 207 entitle her to the unpaid premium plus 100% liquidated damages.',

    /* What's next. NYC small-claims wage flow. Court-mailed service is the cheap default. */
    whatsNextIntro: 'Two small things, then your packet is ready to file.',
    whatsNext: [
      { label: 'Choose how you want to serve Brooklyn Bites', sub: 'Court-mailed service ($10) or licensed process server', screen: 'filing' },
      { label: 'Read and affirm your final document set', sub: 'Statement of Claim · Exhibit Index · Damages Worksheet', screen: 'documents' },
    ],

    caption: {
      caseNumber: 'NY-KC-2026-12388',
      court: 'NYC Civil Court · Kings County (Small-Claims Part)',
      filingDate: 'April 29, 2026',
      plaintiff: 'Sarah Chen',
      plaintiffPhone: '(917) 555-0184',
      plaintiffEmail: 'sarah.chen.case@example.com',
      defendant: 'Brooklyn Bites LLC',
    },

    snapshot: {
      natureOfClaim: 'Unpaid wages and overtime premium under NY Labor Law § 198 and FLSA § 207, plus 100% liquidated damages under NY Labor Law § 198(1-a).',
      keyIssues: [
        'Defendant scheduled Plaintiff for 50 hours per week for 8 consecutive weeks but paid straight time only for hours over 40.',
        'Plaintiff performed 12 hours of pre-shift prep work that the manager required but was never recorded on her timecards.',
        'Defendant cannot produce complete timekeeping records for these weeks. Under Anderson v. Mt. Clemens Pottery, Plaintiff\'s reasonable estimate of unpaid hours stands.',
        'Contemporaneous notes plus coworker texts plus a manager text instructing Plaintiff to "clock in after setup" corroborate the off-the-clock requirement.',
      ],
      evidenceHighlights: [
        { label: 'Brooklyn Bites timecards (Mar 11–17 and Mar 25–31, 2026, 50 hrs each)', exhibit: 2 },
        { label: 'Paystubs across 6 pay periods (straight-time pay only)', exhibit: 3 },
        { label: 'Manager text: "clock in after setup"', exhibit: 5 },
        { label: 'Contemporaneous Apple Notes schedule log (Mar–Apr 2026)', exhibit: 4 },
      ],
      reliefSought: '$2,100. Breakdown: $240 unpaid off-the-clock regular wages + $800 overtime premium owed + $1,040 NY 100% liquidated damages + $20 court filing fee.',
      maxBalanceOwed: 'None. Plaintiff has no outstanding obligations to Defendant.',
    },

    statementSections: [
      { id: 'employment', n: 'I', title: 'Employment and Pay', body: 'On January 6, 2026, Plaintiff was hired by Defendant as a server at its Manhattan restaurant. Plaintiff\'s offer letter set her regular rate at $20.00 per hour (Exhibit 1). Plaintiff\'s employment was non-exempt under both NY Labor Law and FLSA.' },
      { id: 'hours', n: 'II', title: 'Hours Worked and Pay Provided', body: 'During each of the 8 workweeks between January 6, 2026 and February 28, 2026, Plaintiff was scheduled for and worked 50 hours, as reflected in Defendant\'s own timecards (Exhibit 2). Plaintiff\'s paystubs for those weeks show payment at her regular rate of $20.00 per hour for all 50 hours (Exhibit 3). No overtime premium was paid for the 10 hours per week worked in excess of 40.' },
      { id: 'statute', n: 'III', title: 'The Statutory Framework', body: 'Under FLSA § 207 and 12 NYCRR § 142-2.2, an employer must pay a non-exempt employee one-and-one-half times the regular rate for all hours worked over 40 in a workweek. Defendant\'s payment of straight time only for those overtime hours is a per-hour underpayment of $10.00 (one-half of the regular rate), totaling $800 (10 hrs/week × 8 weeks × $10/hr) in unpaid overtime premium.' },
      { id: 'off-clock', n: 'IV', title: 'Required Off-the-Clock Prep Work', body: 'In addition to her scheduled hours, Plaintiff was required by the floor manager to arrive 30 minutes before her shift to perform prep work (rolling silverware, setting tables, stocking the service station). Plaintiff was instructed by text on January 8, 2026 to "clock in after setup" (Exhibit 5). Across the same 8 workweeks Plaintiff performed approximately 12 hours of pre-shift work that was required but never recorded on her timecards (Exhibit 4. Contemporaneous schedule notes). At Plaintiff\'s regular rate of $20.00/hr, this is $240 in unpaid regular wages.' },
      { id: 'recordkeeping', n: 'V', title: 'Recordkeeping Failure and Burden-Shift', body: 'NY Labor Law § 195(4) and 29 CFR § 516.5 require an employer to maintain complete and accurate records of hours worked. Defendant\'s timekeeping records do not include the required pre-shift prep work, despite that work being performed at the manager\'s direction. Under Anderson v. Mt. Clemens Pottery, 328 U.S. 680 (1946), where an employer\'s records are incomplete or inaccurate, a non-exempt employee may meet her burden of proof by producing sufficient evidence to show as a matter of just and reasonable inference the amount and extent of the unpaid work. Plaintiff\'s contemporaneous notes, the manager\'s "clock in after setup" text, and corroborating texts with three coworkers satisfy that standard.' },
      { id: 'damages', n: 'VI', title: 'Damages Requested', body: 'Plaintiff seeks judgment in the amount of $2,100, calculated as set forth in the Damages Summary: $240 in unpaid regular wages, $800 in overtime premium owed, $1,040 in liquidated damages under NY Labor Law § 198(1-a) (100% of the unpaid actual amount, awarded by default unless Defendant proves a good-faith reasonable basis for the underpayment), and $20 in court filing fees recoverable upon prevailing.' },
    ],

    keyAdmissions: [
      { fact: 'Plaintiff was employed by Defendant from January 6, 2026 through February 28, 2026 as a non-exempt server.', source: 'Offer letter (Exhibit 1); paystubs (Exhibit 3)' },
      { fact: 'Plaintiff\'s regular rate was $20.00 per hour.', source: 'Offer letter (Exhibit 1); paystubs (Exhibit 3)' },
      { fact: 'Defendant\'s own timecards show Plaintiff worked 50 scheduled hours per week.', source: 'Timecards for weeks of Mar 11–17 and Mar 25–31, 2026 (Exhibit 2)' },
      { fact: 'Defendant paid Plaintiff straight time only for those 50 hours; no overtime premium was applied.', source: 'Paystubs across 6 pay periods (Exhibit 3)' },
      { fact: 'The floor manager instructed Plaintiff in writing to "clock in after setup."', source: 'Manager text dated January 8, 2026 (Exhibit 5)' },
      { fact: 'Plaintiff has no outstanding wage advances, deductions, or obligations to Defendant.', source: 'Paystubs show net pay with no offsets; Plaintiff testimony' },
    ],

    damages: {
      lineItems: [
        { id: 'unpaid-regular', label: 'Unpaid off-the-clock regular wages', calc: '12 hrs × $20/hr regular rate', amount: 240 },
        { id: 'ot-premium', label: 'Overtime premium owed', calc: '10 hrs/wk × 8 wks × $10/hr (half-time premium per FLSA § 207)', amount: 800 },
        { id: 'liquidated', label: 'Liquidated damages (NY 100%)', calc: '($240 + $800) × 100% per NY Lab. Law § 198(1-a)', amount: 1040 },
        { id: 'filing-fee', label: 'Court filing fee (recoverable upon prevailing)', calc: 'NYC Civil Court small-claims filing fee', amount: 20 },
      ],
      subtotal: 2100,
      offset: { label: 'Outstanding obligations Plaintiff owes Defendant', amount: 0, note: 'None. No wage advances, deductions, or open balances against Plaintiff in the record.' },
      net: 2100,
      netLabel: '$2,100',
    },

    referencePacket: {
      caseNumber: 'NY-KC-2026-12388',
      figures: { claimed: '$2,100', alreadyPaid: '$0 (Defendant has paid no portion of the unpaid amount)', maxBalance: 'None', net: '$2,100' },
      sections: [
        { n: 1, title: 'Opening Statement', use: 'Read aloud word-for-word' },
        { n: 2, title: 'Quick Oral Outline', use: 'One-page reference during testimony' },
        { n: 3, title: 'Exhibit Reference Guide', use: 'What to say for each exhibit' },
        { n: 4, title: 'Handling Their Arguments', use: 'Scripted rebuttals to common defenses' },
        { n: 5, title: 'Cross-Examination Prep', use: 'Questions from defense + your answers' },
        { n: 6, title: 'Questions From the Judge', use: 'Anticipate and answer confidently' },
        { n: 7, title: 'Legal Rules & Key Points', use: 'NY wage-law reminders, what to say if challenged' },
        { n: 8, title: 'Math & Damages Summary', use: 'All figures verified, hand to judge if asked' },
        { n: 9, title: 'What to Expect in Court', use: 'Step-by-step courtroom procedure' },
      ],
      opening: {
        deliveryNote: 'Estimated delivery: 2–3 minutes. Speak slowly. Pause after each section. Look up at the judge.',
        sections: [
          { label: 'Introduction', body: '"Your Honor, my name is Sarah Chen. I am the plaintiff in this matter and I am representing myself. I am asking this Court for judgment in the amount of $2,100 based on unpaid wages and overtime owed to me by Brooklyn Bites LLC under New York Labor Law § 198 and the federal Fair Labor Standards Act."' },
          { label: 'What happened', body: '"I worked as a server at Brooklyn Bites for eight weeks beginning January 6, 2026. My regular hourly rate was $20.00. Brooklyn Bites scheduled me for 50 hours each of those weeks, and their own timecards confirm I worked those hours. But my paystubs show I was paid straight time only. No time-and-a-half premium for any of the 80 hours I worked over the 40-per-week federal threshold."' },
          { label: 'The off-the-clock work', body: '"On top of those 50 hours, my floor manager required me to arrive 30 minutes before each shift to roll silverware, set tables, and stock the service station. On January 8 the manager texted me to \'clock in after setup.\' I have that text. I performed approximately 12 hours of that pre-shift work that never appeared on a single timecard."', stageNote: '[Hold up Exhibit 5. The manager\'s text. Pause.]' },
          { label: 'The recordkeeping failure', body: '"New York Labor Law § 195 and the federal regulations require Brooklyn Bites to keep complete records of hours worked. Their records do not include that pre-shift work. Under Anderson v. Mt. Clemens Pottery, when an employer\'s records are incomplete, the worker\'s reasonable estimate stands. My contemporaneous notes and texts with three coworkers provide that estimate."' },
          { label: 'What the statute provides', body: '"Both NY Labor Law § 198 and FLSA § 207 require time-and-a-half over 40 hours. NY Labor Law § 198(1-a) adds liquidated damages equal to 100 percent of the unpaid amount unless the employer proves a good-faith reasonable basis for the underpayment. The employer carries that burden, not me."' },
          { label: 'Closing', body: '"I worked the hours. Brooklyn Bites\' own records confirm 50 hours a week. They paid straight time. They required prep work they never recorded. The statute is clear, my evidence is documented, and I respectfully ask this Court to award $2,100: $240 in off-the-clock wages, $800 in overtime premium, $1,040 in liquidated damages, and $20 in court costs. Thank you."', stageNote: '[Look up. Nod. You\'re done with opening.]' },
        ],
      },
      oralOutline: [
        { n: 1, title: 'OPENING', bullets: ['Hired Jan 6, 2026 as server at Brooklyn Bites', 'Regular rate $20/hr, 50 hours scheduled each week for 8 weeks', 'Paid straight time only; no overtime premium', 'Plus 12 hours off-the-clock prep, never recorded', 'Requesting $2,100 judgment'] },
        { n: 2, title: 'THE EMPLOYMENT', bullets: ['Non-exempt server position', 'Offer letter sets $20/hr rate', 'Paystubs confirm rate and hours paid'], exhibitRef: 'Exhibits 1, 3' },
        { n: 3, title: 'HOURS BROOKLYN BITES PAID FOR', bullets: ['Their own timecards: 50 scheduled hours per week', 'Paystubs: straight time at $20/hr for all hours', '10 hrs/week × 8 weeks = 80 unpaid overtime hours'], exhibitRef: 'Exhibits 2, 3' },
        { n: 4, title: 'HOURS BROOKLYN BITES DID NOT RECORD', bullets: ['Manager required pre-shift prep', 'Text: "clock in after setup" (Jan 8)', '~30 min × 5 shifts × 8 weeks = ~12 hrs unpaid regular', 'Contemporaneous notes + 3 coworker corroboration'], exhibitRef: 'Exhibits 4, 5, 6' },
        { n: 5, title: 'THE BURDEN-SHIFT', bullets: ['NY Lab Law § 195(4) + 29 CFR § 516.5 require complete records', 'Brooklyn Bites cannot produce them for the prep work', 'Anderson v. Mt. Clemens Pottery: reasonable estimate stands', 'My evidence satisfies that standard'] },
        { n: 6, title: 'STATUTORY FRAMEWORK', bullets: ['FLSA § 207 + 12 NYCRR § 142-2.2: time-and-a-half over 40/wk', 'NY Lab Law § 198: unpaid wages recoverable', 'NY Lab Law § 198(1-a): 100% liquidated by default'] },
        { n: 7, title: 'DAMAGES: WHAT I AM REQUESTING', bullets: ['Off-the-clock wages: $240', 'Overtime premium: $800', 'Liquidated 100%: $1,040', 'Court costs: $20', 'Total: $2,100'] },
        { n: 8, title: 'CLOSING', bullets: ['I worked the hours.', 'Brooklyn Bites\' own records prove most of them.', 'They paid straight time and required work they did not record.', 'The statute is clear. I respectfully request $2,100.'] },
      ],
      exhibitGuide: [
        { exhibit: 1, title: 'Offer letter from Brooklyn Bites (Jan 6, 2026)', proves: 'Plaintiff\'s regular rate of $20.00/hr and her non-exempt server role', say: '"This is my offer letter, Your Honor. It establishes my rate and role."' },
        { exhibit: 2, title: 'Brooklyn Bites timecards (weeks of Mar 11 and Mar 25, 2026)', proves: 'Plaintiff worked 50 scheduled hours each of those weeks (representative of all 8)', say: '"These are Brooklyn Bites\' own timecards, Your Honor. They show 50 hours each week."' },
        { exhibit: 3, title: 'Paystubs for 6 pay periods', proves: 'Plaintiff was paid at $20/hr straight time only; no overtime premium applied', say: '"My paystubs. Straight time only across every pay period."' },
        { exhibit: 4, title: 'Contemporaneous schedule notes (Apple Notes, Mar–Apr 2026)', proves: 'Plaintiff\'s own real-time record of off-the-clock prep minutes per shift', say: '"These are my notes taken at the time, Your Honor. They show the prep work I performed each shift."' },
        { exhibit: 5, title: 'Manager text: "clock in after setup" (Jan 8, 2026)', proves: 'Defendant required off-the-clock pre-shift work in writing', say: '"This is the floor manager\'s text instructing me to clock in after the setup work was done."' },
        { exhibit: 6, title: 'Text thread with 3 coworkers about closing/prep duties', proves: 'Off-the-clock prep was company practice, corroborating Plaintiff\'s account', say: '"These are texts with three coworkers describing the same off-the-clock practice."' },
      ],
      rebuttals: [
        { theySay: '"Plaintiff was paid for every hour she clocked in."', youSay: '"Your Honor, that\'s precisely the issue. Exhibit 5 shows the manager instructed me to clock in after the setup work was finished. Brooklyn Bites paid only for clocked hours, but they required work before the clock started. Under Anderson v. Mt. Clemens Pottery, when an employer\'s records are incomplete, my reasonable estimate of unpaid hours stands."' },
        { theySay: '"The 50-hour weeks were the employee\'s choice, not a requirement."', youSay: '"Exhibit 2 contains Brooklyn Bites\' own scheduling timecards showing 50 hours assigned per week. FLSA § 207 applies whenever a non-exempt employee works over 40 hours, regardless of whether the schedule was offered, requested, or mandated."' },
        { theySay: '"Plaintiff is a tipped worker, so tip-credit rules apply."', youSay: '"My offer letter (Exhibit 1) sets a flat $20.00/hr rate. Brooklyn Bites elected a non-tipped pay structure. Tip-credit rules under 12 NYCRR § 146 do not apply where the employer pays the full minimum and above, as the paystubs in Exhibit 3 confirm."' },
        { theySay: '"The contemporaneous notes are self-serving."', youSay: '"Under Anderson v. Mt. Clemens Pottery and consistent NY appellate decisions, an employee\'s contemporaneous records ARE the evidentiary baseline when the employer cannot produce complete records. Exhibits 4 and 6 (coworker texts) corroborate the pattern."' },
      ],
      crossExam: [
        { group: 'Employment and Rate', items: [
          { q: '"You agreed to the rate when you accepted the job, correct?"', a: '"Yes. The offer letter set my rate at $20.00/hr. The statutory overtime premium applies on top of any agreed rate; it cannot be waived by agreement under FLSA § 207."' },
          { q: '"You were free to leave Brooklyn Bites at any time, weren\'t you?"', a: '"Yes. That\'s irrelevant to whether the hours I worked were paid lawfully. The statute applies to the hours actually worked, not to whether the employment relationship continued."' },
        ]},
        { group: 'Hours Worked', items: [
          { q: '"You can\'t prove the prep work happened on every shift, can you?"', a: '"My contemporaneous notes record the date and minutes for each shift. Exhibit 4. Brooklyn Bites cannot produce records showing those hours either way, which under Anderson v. Mt. Clemens Pottery shifts the evidentiary burden."' },
          { q: '"Did anyone at Brooklyn Bites tell you the prep work was unpaid?"', a: '"The manager\'s text told me to clock in AFTER setup. Exhibit 5. That instruction made the unpaid status clear. I followed it because I was new and needed the job."' },
        ]},
        { group: 'The Manager Text', items: [
          { q: '"That text could mean anything. How do we know it refers to prep work?"', a: '"The text is dated January 8, two days after I started. The pattern of clocking in after setup is reflected in every timecard Brooklyn Bites produced thereafter. Exhibit 2 shows clock-in times consistently after the scheduled shift start."' },
        ]},
      ],
      judgeQuestions: [
        { q: '"What exactly are you asking me to award you today?"', a: '"$2,100, Your Honor. $240 in off-the-clock regular wages, $800 in overtime premium under FLSA § 207, $1,040 in liquidated damages under NY Lab. Law § 198(1-a), and $20 in court filing fees."' },
        { q: '"Did Brooklyn Bites ever pay you any portion of the amount you\'re seeking?"', a: '"No, Your Honor. Brooklyn Bites has paid none of the disputed amount, before or after my demand."' },
        { q: '"Are you a tipped employee under NY hospitality regulations?"', a: '"No, Your Honor. My offer letter sets a flat $20/hr rate and my paystubs in Exhibit 3 show no tip-credit applied. Brooklyn Bites elected a non-tipped pay structure."' },
        { q: '"Can you point me to the specific overtime statute that applies?"', a: '"FLSA § 207 and 12 NYCRR § 142-2.2, Your Honor. Both require time-and-a-half for hours over 40 in a workweek. Brooklyn Bites paid straight time for all 50 hours each week."' },
        { q: '"Why is liquidated damages 100% rather than something less?"', a: '"NY Labor Law § 198(1-a) sets liquidated damages at 100% of the unpaid amount by default. The employer must prove a good-faith reasonable basis to reduce it. Brooklyn Bites has offered none in the record."' },
        { q: '"Why didn\'t you complain at the time?"', a: '"I needed the job, Your Honor. I started keeping the schedule notes (Exhibit 4) when I realized the prep work was unpaid. I left the position at the end of February and filed this claim shortly after consulting Hello Court."' },
      ],
      legalRules: {
        dos: [
          'Stay calm. Composure helps the judge focus on the law and the numbers.',
          'Address the judge as "Your Honor" every single time.',
          'Refer to exhibits by number: "As shown in Exhibit [#], Your Honor..."',
          'If you don\'t know an answer, say: "I don\'t have that in front of me, Your Honor."',
        ],
        donts: [
          'Don\'t interrupt opposing counsel or the judge.',
          'Don\'t argue with the judge. If overruled, say "Yes, Your Honor" and move on.',
          'Don\'t volunteer testimony beyond your filed claim.',
          'Don\'t react visibly if opposing counsel says something inaccurate. Note it; address it on your turn.',
        ],
        keyPoints: [
          { title: 'FLSA § 207: the 40-hour overtime threshold', body: 'Non-exempt employees must be paid one-and-one-half times the regular rate for all hours worked over 40 in a single workweek. The threshold applies per workweek, not averaged across multiple weeks. The premium cannot be waived by agreement.' },
          { title: 'NY Lab. Law § 198: unpaid wages plus § 198(1-a) liquidated', body: 'NY law authorizes recovery of unpaid wages, plus liquidated damages equal to 100% of the unpaid amount. Liquidated damages are awarded by default unless the employer proves a good-faith reasonable basis for the underpayment. The employer carries that burden, not the worker.' },
          { title: 'Anderson v. Mt. Clemens Pottery: the burden-shift', body: 'When an employer\'s timekeeping records are incomplete or inaccurate, a non-exempt employee may meet her burden of proof by producing sufficient evidence to show as a matter of just and reasonable inference the amount and extent of the unpaid work. The employer then bears the burden of negating that reasonable inference.' },
        ],
      },
      whatToExpect: [
        { n: 1, title: 'Arrive Early', body: 'Get to NYC Civil Court 30 minutes early. Tell the clerk you are the plaintiff in Chen v. Brooklyn Bites. Confirm the judge has your filed documents.' },
        { n: 2, title: 'Case Is Called', body: 'Both sides approach. In small-claims part, a judge or court-appointed arbitrator runs the proceeding (there is no jury). Address them as "Your Honor" at all times.' },
        { n: 3, title: 'You Speak First', body: 'As plaintiff, you present first. Deliver your opening statement from Section 1. Then walk the judge through your key exhibits in order: offer letter, timecards, paystubs, manager text. Pause after key points.' },
        { n: 4, title: 'Documents Ready', body: 'Have your exhibit binder ready and tab-marked. Hand the judge printed copies if asked. Keep originals as backup.' },
        { n: 5, title: 'Defendant Responds', body: 'They will present their side. Do NOT interrupt. Take notes on anything inaccurate so you can address it calmly when it is your turn.' },
        { n: 6, title: 'Judge\'s Questions', body: 'The judge may ask either party questions at any time. Answer directly and briefly. "Yes, Your Honor" / "No, Your Honor." See Section 6.' },
        { n: 7, title: 'Judgment', body: 'The judge may rule immediately or mail you the decision within a few weeks. Either is normal. Stay calm and professional regardless of outcome.' },
      ],
    },

    /* === byCause: variant content per primary cause of action ===========
       When the user picks a different cause in V3Theory, V3Damages /
       V3Documents / V3Courtroom look up the variant here. Falls back to
       the default (unpaid-overtime) if the variant is missing.

       Pleading note: in practice a wage plaintiff pleads all viable causes
       together. "Primary" here is the headline framing the user is taking
       into court — affects damages requested, Statement of Claim emphasis,
       and the opening statement's leading argument.
    ===================================================================== */
    byCause: {
      'unpaid-overtime': {
        label: 'Unpaid overtime (NY Lab. Law § 198 + FLSA § 207)',
        damagesIntroLine: "Wage cases stack up fast. NY Labor Law § 198 and federal FLSA § 207 give you several layers (unpaid wages, overtime premium, and 100% liquidated damages on top). Pick what to claim. The math runs from your timecard data.",
        damagesLineItems: [
          { id: 'unpaid-regular', label: 'Unpaid off-the-clock regular wages', calc: '12 hrs × $20/hr regular rate', amount: 240 },
          { id: 'ot-premium', label: 'Overtime premium owed', calc: '10 hrs/wk × 8 wks × $10/hr (half-time per FLSA § 207)', amount: 800 },
          { id: 'liquidated', label: 'Liquidated damages (NY 100%)', calc: '($240 + $800) × 100% per NY Lab. Law § 198(1-a)', amount: 1040 },
          { id: 'filing-fee', label: 'Court filing fee (recoverable upon prevailing)', calc: 'NYC Civil Court small-claims', amount: 20 },
        ],
        damagesTotal: 2100,
        damagesTotalLabel: '$2,100',
        statementBody: 'Plaintiff Sarah Chen worked as a server for Defendant from January 6, 2026 through February 28, 2026 (8 weeks). Her regular rate was $20.00/hr (Exhibit 1). Defendant\'s own timecards confirm 50 scheduled hours each workweek (Exhibit 2), and Plaintiff\'s paystubs show payment at straight time only (Exhibit 3). FLSA § 207 and 12 NYCRR § 142-2.2 require time-and-a-half for hours over 40; the unpaid overtime premium totals $800 (10 hrs/wk × 8 wks × $10/hr half-time). Plaintiff also performed 12 hours of off-the-clock pre-shift prep work that the manager required in writing (Exhibit 5: "clock in after setup") but never recorded, totaling $240 in unpaid regular wages. Under NY Lab. Law § 198(1-a), Plaintiff seeks liquidated damages equal to 100% of the unpaid amount ($1,040), awarded by default unless Defendant proves a good-faith reasonable basis for the underpayment. Plaintiff seeks $2,100 total.',
        openingLeadIn: '"Your Honor, my name is Sarah Chen. I am the plaintiff in this matter and I am representing myself. I am asking this Court for judgment in the amount of $2,100 based on unpaid wages and overtime owed to me by Brooklyn Bites LLC under New York Labor Law § 198 and the federal Fair Labor Standards Act."',
      },
      'recordkeeping-violation': {
        label: 'Recordkeeping violation as primary (NY Lab. Law § 195(4) + Anderson)',
        damagesIntroLine: "Recordkeeping violation as your primary framing puts the burden-shift up front. Liquidated damages defer to discovery since they require an actual underpayment finding; what you ask for now is the unpaid amount your reasonable estimate establishes. Your numbers, supported by your notes plus coworker corroboration, stand under Anderson v. Mt. Clemens Pottery.",
        damagesLineItems: [
          { id: 'unpaid-regular', label: 'Unpaid off-the-clock regular wages (burden-shift estimate)', calc: '12 hrs × $20/hr; estimate stands under Anderson', amount: 240 },
          { id: 'ot-premium', label: 'Overtime premium owed (burden-shift estimate)', calc: '10 hrs/wk × 8 wks × $10/hr half-time', amount: 800 },
          { id: 'liquidated-deferred', label: 'Liquidated damages, deferred', calc: 'Reserved pending discovery confirming full extent of underpayment', amount: 0, deferred: true },
          { id: 'filing-fee', label: 'Court filing fee (recoverable upon prevailing)', calc: 'NYC Civil Court small-claims', amount: 20 },
        ],
        damagesTotal: 1060,
        damagesTotalLabel: '$1,060',
        statementBody: 'Plaintiff Sarah Chen worked as a server for Defendant from January 6, 2026 through February 28, 2026. Pursuant to NY Labor Law § 195(4) and 29 CFR § 516.5, Defendant was required to maintain complete and accurate records of all hours Plaintiff worked. Defendant has not produced records reflecting the off-the-clock prep work Plaintiff performed at the floor manager\'s written direction (Exhibit 5: "clock in after setup"). Under Anderson v. Mt. Clemens Pottery, 328 U.S. 680 (1946), where an employer\'s records are incomplete, a non-exempt employee may meet her burden of proof by producing sufficient evidence to show as a matter of just and reasonable inference the amount and extent of the unpaid work. Plaintiff\'s contemporaneous notes (Exhibit 4) plus corroborating texts with three coworkers (Exhibit 6) satisfy that standard. Plaintiff seeks $1,060 in actual unpaid wages and court costs. Liquidated damages under § 198(1-a) are reserved pending discovery confirming the full extent of underpayment.',
        openingLeadIn: '"Your Honor, my name is Sarah Chen, plaintiff in this matter, representing myself. I am asking this Court for judgment in the amount of $1,060 against Brooklyn Bites LLC. The legal foundation of my claim is New York Labor Law § 195(4) and the federal recordkeeping regulations — and the U.S. Supreme Court\'s decision in Anderson v. Mt. Clemens Pottery, which controls how this case is proved when the employer cannot produce complete records."',
      },
      'retaliation': {
        label: 'Anti-retaliation declaration (NY Lab. Law § 215), contingent claim',
        damagesIntroLine: "Anti-retaliation as your primary framing files the wage claim AND seeks the Court's declaration that no adverse action may be taken against you for exercising your right to file. Damages are largely contingent: if Brooklyn Bites retaliates, you can amend to add back pay, 100% liquidated, and reinstatement under § 215. The base filing recovers the unpaid wages alone.",
        damagesLineItems: [
          { id: 'unpaid-base', label: 'Base unpaid wages (off-the-clock + OT premium)', calc: '$240 + $800 (preserved from underlying wage claim)', amount: 1040 },
          { id: 'protective-decl', label: 'Court declaration: no retaliation under § 215', calc: 'Declaratory relief. No dollar value at filing', amount: 0, conditional: true },
          { id: 'backpay-contingent', label: 'Back pay (contingent on retaliation occurring)', calc: 'Activated if Defendant takes adverse action post-filing', amount: 0, conditional: true },
          { id: 'liq-contingent', label: 'Liquidated damages under § 215 (contingent)', calc: '100% of back pay if retaliation finding', amount: 0, conditional: true },
          { id: 'filing-fee', label: 'Court filing fee (recoverable upon prevailing)', calc: 'NYC Civil Court small-claims', amount: 20 },
        ],
        damagesTotal: 1060,
        damagesTotalLabel: '$1,060 (filing) + contingent § 215 damages if retaliation',
        statementBody: 'Plaintiff Sarah Chen is a former server at Defendant\'s Manhattan restaurant. Plaintiff brings this action to recover $1,040 in unpaid wages and overtime under NY Labor Law § 198 and FLSA § 207, plus $20 in court costs. Pursuant to NY Labor Law § 215, Plaintiff additionally requests the Court\'s declaration that no adverse employment action — including but not limited to interference with future employment references — may be taken against Plaintiff in retaliation for exercising the right to file this claim. Plaintiff reserves the right to amend pursuant to § 215 and to seek back pay, liquidated damages, and reinstatement remedies should Defendant subsequently retaliate.',
        openingLeadIn: '"Your Honor, my name is Sarah Chen. I am the plaintiff and I am representing myself. I am asking this Court for two things today. First: judgment of $1,060 in unpaid wages and court costs owed to me by Brooklyn Bites LLC under NY Labor Law § 198 and FLSA § 207. Second: this Court\'s declaration under NY Labor Law § 215 that no adverse action may be taken against me for exercising my right to bring this claim."',
      },
    },
  },
  /* === New-case placeholder (Story 0.49). Populated by TriageIntake's
        onFinish in demo.html. Fields below are mutated at runtime from
        {description, state, category, deadline}. The static defaults here
        let the matter render safely if someone lands on it without going
        through triage first, and they double as the guided-tour (Story 0.17)
        pre-seeded example state.
        __tourHome: true means the standard V3Home renders (readiness ring,
        stage rail, data-coach targets). Triage onFinish resets this to false
        so the onboarding "pick where to start" tiles show for fresh cases. === */
  newcase: {
    id: 'newcase', tier: 1, jurisdiction: 'ny',
    title: 'Your new case',
    short: 'Your new case',
    client: { name: 'You', initials: 'YO', city: 'Brooklyn, NY', occupation: '—' },
    opposing: { name: 'The other side', initials: '—' },
    type: 'Wage theft · Unpaid overtime',
    statute: 'NY Lab. Law § 198 + FLSA § 207',
    statuteNote: '100% liquidated damages available',
    court: 'NYC Civil Court · Kings County (small claims)',
    amount: 1040, amountLabel: '$1,040',
    potentialAmount: 2100, potentialAmountLabel: '$2,100 total recovery sought',
    stage: 'Preparation', stageIdx: 1,
    /* readiness: 35 = 14 (elements) + 9 (mapping) + 12 (statutory) + 0 (procedural).
       Reflects a tour case where theory is picked and some evidence is connected,
       but service method has not been confirmed yet. */
    readiness: 35,
    readinessBreakdown: {
      elements: { score: 14, max: 40, label: 'Element completeness', missing: 'Theory selected. 2 of 4 wage elements have at least one piece of supporting evidence. Connect your timecards and paystubs to the remaining elements on the Evidence screen.' },
      mapping: { score: 9, max: 30, label: 'Evidence-element mapping', missing: 'Timecards affirmed for Elements 1 and 3. Schedule notes and paystubs are uploaded but not yet matched to elements. Go to Evidence to confirm the remaining connections.' },
      statutory: { score: 12, max: 15, label: 'Statutory accuracy', missing: 'NY Lab. Law § 198 and FLSA § 207 verified current as of 2026-05-01 by Atticus. Semi-annual re-verification scheduled (next: 2026-11-01).' },
      procedural: { score: 0, max: 15, label: 'Procedural prep', missing: 'Court, fee, and service method not yet confirmed. These finalize in about 5 minutes on the Filing screen.' },
    },
    summary: "A new case you started today. Your description from intake gets captured here, and we'll build out the rest as you fill in the details.",
    caption: { caseNumber: '—', court: '—', filingDate: '—', plaintiff: 'You', plaintiffPhone: '—', plaintiffEmail: '—', defendant: 'The other side' },
    snapshot: { natureOfClaim: '—', keyIssues: [], evidenceHighlights: [] },
    whatsNextIntro: 'Two things to get this case to the filing threshold.',
    whatsNext: [
      { label: 'Connect your remaining evidence', sub: 'Timecards and schedule notes need to be matched to two more elements', screen: 'evidence' },
      { label: 'Confirm service method', sub: 'NYC Civil Court small-claims uses court-mailed service ($10)', screen: 'filing' },
    ],
    __tourHome: true,
  },
  /* === Tier 1 case-type variety stubs (UT). Appear in case selector;
        clicking renders the V3ComingSoon stub instead of full screens ===  */
  wage: {
    id: 'wage', tier: 1, jurisdiction: 'utah', comingSoon: true,
    title: 'Lopez v. Pinnacle Construction',
    short: 'Lopez v. Pinnacle',
    client: { name: 'Andres Lopez', initials: 'AL', city: 'Ogden, UT', occupation: 'Drywall installer' },
    opposing: { name: 'Pinnacle Construction LLC', initials: 'PC' },
    type: 'Wage theft · Unpaid overtime',
    statute: 'Utah § 34-28 (Wage Payment Act)',
    statuteNote: 'Treble damages + reasonable attorney\'s fees available',
    court: 'Utah Labor Commission → Salt Lake Justice Court',
    amount: 4280, amountLabel: '$4,280',
    stage: 'Intake', stageIdx: 0,
    readiness: 12,
    summary: '6 weeks of unpaid overtime ($4,280 at time-and-a-half). Employer paid straight-time only; refuses to issue corrected paystubs. Utah Wage Payment Act allows treble damages where wages are wrongfully withheld.',
  },
  fraud: {
    id: 'fraud', tier: 1, jurisdiction: 'utah', comingSoon: true,
    title: 'Tate v. Brightline Solar',
    short: 'Tate v. Brightline',
    client: { name: 'Reagan Tate', initials: 'RT', city: 'Provo, UT', occupation: 'High-school teacher' },
    opposing: { name: 'Brightline Solar Sales LLC', initials: 'BS' },
    type: 'Consumer fraud · Door-to-door misrepresentation',
    statute: 'Utah § 13-11 (Consumer Sales Practices Act)',
    statuteNote: 'Actual damages + statutory; up to $2,000 per violation',
    court: 'Salt Lake County Justice Court',
    amount: 6800, amountLabel: '$6,800',
    stage: 'Intake', stageIdx: 0,
    readiness: 8,
    summary: 'Door-to-door salesman promised 0% APR financing and 30-day cancellation. Contract had 24% APR and a 3-day cancellation window buried on page 7. Utah CSPA prohibits material misrepresentations in consumer sales.',
  },
  breach: {
    id: 'breach', tier: 1, jurisdiction: 'utah', comingSoon: true,
    title: 'Kim v. PetShack Boarding',
    short: 'Kim v. PetShack',
    client: { name: 'Joon Kim', initials: 'JK', city: 'Salt Lake City, UT', occupation: 'Software engineer' },
    opposing: { name: 'PetShack Boarding & Grooming', initials: 'PS' },
    type: 'Small breach of contract · Service refund',
    statute: 'Utah § 78A-8 (small-claims)',
    statuteNote: '$15K small-claims cap',
    court: 'Salt Lake County Justice Court',
    amount: 1240, amountLabel: '$1,240',
    stage: 'Intake', stageIdx: 0,
    readiness: 6,
    summary: 'Boarding facility lost dog\'s collar and harness, returned dog with skin infection from cage rash. Contract guarantees safe boarding. Refund of $1,240 (boarding fee + vet bill) refused.',
  },
};

/* Tier 1 statutory anchors per state */
const V3_STATUTES = {
  utah: [
    { area: 'Security-deposit recovery', cite: '§ 57-17-3', note: '2× wrongful retention' },
    { area: 'Unpaid wages', cite: '§ 34-28-1 et seq.', note: '' },
    { area: 'Consumer fraud', cite: '§ 13-11 et seq.', note: '' },
    { area: 'Auto repair / lemon', cite: '§ 13-44', note: '' },
    { area: 'Small breach of contract', cite: '§ 78A-8', note: '$15K small-claims cap' },
  ],
  ny: [
    { area: 'Security-deposit recovery', cite: 'GOL § 7-108', note: 'Post-2019 HSTPA' },
    { area: 'Unpaid wages', cite: 'Labor Law §§ 190 et seq. + § 198', note: '' },
    { area: 'Consumer fraud', cite: 'GBL § 349', note: '' },
    { area: 'Auto repair', cite: 'GBL § 198-a + § 396-p', note: '' },
    { area: 'Small breach of contract', cite: 'Civ. Court Act § 1801', note: '$10K NYC / $5K else' },
  ],
};

/* Tier 1 elements for security deposit */
const V3_ELEMENTS_DEPOSIT_UT = [
  { id: 'tenancy', label: 'A landlord-tenant relationship existed', explain: 'You rented the unit. We need a signed lease (or proof of rent payment if no written lease).', complete: true, evidence: 2 },
  { id: 'deposit-paid', label: 'You paid a security deposit', explain: 'The amount you paid the landlord at move-in to hold against future damages.', complete: true, evidence: 2 },
  { id: 'tenancy-ended', label: 'Your tenancy ended properly', explain: 'You gave notice (or your lease ended), returned the keys, and moved out by the agreed date.', complete: true, evidence: 3 },
  { id: 'no-return', label: 'Landlord did not return the deposit or itemize within 30 days', explain: 'Utah § 57-17-3 gives the landlord 30 days from the end of tenancy to return the deposit OR send a written explanation of any deductions.', complete: true, evidence: 1 },
];

/* Maria's evidence */
const V3_EVIDENCE_MARIA = [
  { id: 'lease', name: 'Lease agreement (signed 4/1/2024)', type: 'PDF', maps: ['tenancy', 'deposit-paid'] },
  { id: 'deposit-receipt', name: 'Deposit receipt. $1,800 · 4/1/2024', type: 'Image', exif: 'Photo · 3/27/2026', maps: ['deposit-paid'] },
  { id: 'notice', name: '30-day notice to vacate (texted 2/13/2026)', type: 'Screenshot', maps: ['tenancy-ended'] },
  { id: 'keys', name: 'Key return confirmation email · 3/15/2026', type: 'Email', maps: ['tenancy-ended'] },
  { id: 'walkthrough', name: 'Move-out walkthrough photos (12)', type: 'Image set', exif: 'EXIF dates: 3/15/2026', maps: ['tenancy-ended'] },
  { id: 'demand', name: 'Demand letter sent (certified) · 4/16/2026', type: 'PDF + USPS', maps: ['no-return'] },
  { id: 'silence', name: 'No response. 30 days elapsed', type: 'Timeline event', maps: ['no-return'] },
];

/* Damages decomposition for Maria */
const V3_DAMAGES_MARIA = [
  { label: 'Security deposit withheld', amount: 1800, basis: 'Original deposit paid 4/1/2024 (receipt provided)', user: true },
  { label: 'Statutory 2× damages (wrongful retention)', amount: 1800, basis: 'Utah § 57-17-3. Available when landlord neither returns deposit nor itemizes within 30 days', user: false, atticus: true },
  { label: 'Filing fee (recoverable if you prevail)', amount: 60, basis: 'Salt Lake County Justice Court small claims filing fee', user: false },
];

/* ============================================================
   Patel. NY auto-repair · Tier 1 NY parity demo arc
   ============================================================ */

/* Cause-of-action options for V3Theory (Maria) */
const V3_CAUSES_MARIA = [
  { id: 'wrongful-retention', label: 'Wrongful retention of security deposit', cite: 'Utah § 57-17-3', remedy: '2× damages possible', applies: 'The landlord did not return your deposit OR send a written itemization within 30 days after you give the landlord your forwarding address (or after your lease ends, whichever is later).', prove: ['A landlord-tenant relationship existed', 'You paid a deposit', 'Your tenancy ended properly', '30+ days passed after you gave the landlord your forwarding address (or after your lease ended, whichever is later) with no return and no itemization'], match: 'strong' },
  { id: 'breach-lease', label: 'Breach of lease agreement', cite: 'Utah contract law', remedy: 'Actual damages', applies: 'The landlord did something the lease prohibited, or did not do something the lease required.', prove: ['A valid lease', 'A specific lease term that was broken', 'You followed your obligations', 'You suffered measurable damages'], match: 'partial' },
];

/* Cause-of-action options for V3Theory (Patel) */
const V3_CAUSES_PATEL = [
  { id: 'gbl-349', label: 'Deceptive business practices', cite: 'NY GBL § 349', remedy: 'Actual + $50 statutory; treble if willful', applies: 'A business engaged in materially misleading consumer-oriented conduct that caused you injury. Like charging for unauthorized work after a written estimate.', prove: ['The conduct was consumer-oriented', 'The act or omission was materially misleading', 'You were injured as a result'], match: 'strong' },
  { id: 'gbl-198a', label: 'Auto-repair shop regulation violation', cite: 'NY GBL § 198-a', remedy: 'Actual damages', applies: 'An auto-repair shop performed work without a written estimate AND without your written authorization for any work over the estimate.', prove: ['The shop is a registered auto-repair facility', 'A written estimate was given', 'Additional work was performed beyond the estimate', 'You did not authorize the additional work in writing'], match: 'strong' },
];

/* === Story 0.49: New-case employment / NY wage data (matches MOCK_SCENARIO) === */
const V3_CAUSES_WAGE_NY = [
  { id: 'unpaid-overtime', label: 'Unpaid overtime / off-the-clock work', cite: 'NY Labor Law § 198 + 29 USC § 207', remedy: 'Unpaid wages + 100% liquidated damages + attorney fees', applies: 'A non-exempt employee was clocked out before completing required work, resulting in unpaid hours over the 40-per-week overtime threshold.', prove: ['You were a non-exempt employee', 'You worked hours that were not on your paid timecards', 'Your hours, including unpaid time, exceeded 40 in at least one workweek', 'The employer failed to pay overtime (1.5x) for those excess hours'], match: 'strong' },
  { id: 'recordkeeping-violation', label: 'Recordkeeping violation', cite: 'NY Labor Law § 195(4) + 29 CFR § 516.5', remedy: 'Burden-shift on damages calculation', applies: 'The employer is legally required to keep complete payroll and timekeeping records. Where they cannot produce them, the employee\'s reasonable estimate of unpaid hours stands.', prove: ['Employer failed to keep accurate hour-by-hour records', 'Your reasonable estimate of unpaid time is supported by other evidence (texts, schedules, coworker statements)'], match: 'strong' },
  { id: 'retaliation', label: 'Anti-retaliation protection', cite: 'NY Labor Law § 215', remedy: 'Reinstatement + back pay + liquidated damages', applies: 'If the employer fires, demotes, or cuts hours after you raise a wage complaint, that is a separate unlawful act.', prove: ['You engaged in protected activity (asked about back pay, filed a complaint)', 'The employer took an adverse action', 'The adverse action was causally connected to the protected activity'], match: 'partial' },
];

const V3_ELEMENTS_WAGE_NY = [
  { id: 'nonexempt', label: 'You were a non-exempt employee', explain: 'Servers and tipped workers are almost always non-exempt under NY law. Exempt categories are narrow (executive, administrative, professional, certain commission-based salespeople with minimum salary thresholds).', complete: false, evidence: 0 },
  { id: 'off-clock-hours', label: 'You worked hours not captured on your timecards', explain: 'The 30 to 45 minutes after each shift, cleaning up and rolling silverware while clocked out. Need a credible total: shift count × per-shift unpaid time.', complete: false, evidence: 0 },
  { id: 'overtime-threshold', label: 'Total hours exceeded 40 in at least one workweek', explain: 'NY follows the federal FLSA standard. Overtime = 1.5x for hours over 40 in a single workweek.', complete: false, evidence: 0 },
  { id: 'unpaid', label: 'You were not paid overtime for those hours', explain: 'Paystubs or check stubs confirming the unpaid amount. If employer cannot produce records, the burden shifts under Anderson v. Mt. Clemens Pottery.', complete: false, evidence: 0 },
];

/* Mock file-grab for new-case employment scenario. These are the "files we
   found in your Drive" when user clicks Connect Drive in V3Evidence's
   empty-state. Each item maps to one or more elements above. */
const V3_EVIDENCE_WAGE_NY = [
  { id: 'timecard-w12', name: 'Timecard · Week of Mar 11–17, 2026', type: 'PDF · timesheets@brooklynbites.com', source: 'Connected from Google Drive', maps: ['nonexempt', 'overtime-threshold'] },
  { id: 'timecard-w14', name: 'Timecard · Week of Mar 25–31, 2026', type: 'PDF · timesheets@brooklynbites.com', source: 'Connected from Google Drive', maps: ['nonexempt', 'overtime-threshold'] },
  { id: 'schedule-notes', name: 'Personal schedule notes (6 weeks)', type: 'Apple Notes · Mar–Apr 2026', source: 'Connected from iCloud', maps: ['off-clock-hours'] },
  { id: 'coworker-texts', name: 'Texts with 3 coworkers about closing duties', type: 'iMessage export', source: 'Connected from iCloud', maps: ['off-clock-hours'] },
  { id: 'paystubs', name: 'Paystubs · 6 pay periods', type: 'PDF · Brooklyn Bites payroll', source: 'Connected from Google Drive', maps: ['unpaid'] },
  { id: 'manager-text', name: 'Text from manager: "that\'s part of the job"', type: 'iMessage screenshot', source: 'Connected from iCloud', maps: ['off-clock-hours'] },
];

/* Per-evidence Atticus reads for Sarah Chen / wage-NY. Mirrors the per-item
   shape of V3_EVIDENCE_READS_PATEL but framed for unpaid-wage elements. */
const V3_EVIDENCE_READS_WAGE_NY = {
  'timecard-w12': {
    elements: ['nonexempt', 'overtime-threshold'],
    why: "This is a Brooklyn Bites timecard from the week of Mar 11. It shows 50 scheduled hours and the rate paid was straight time only. It supports Element 1 (you were a non-exempt employee) and Element 3 (your hours exceeded 40 in this workweek).",
  },
  'timecard-w14': {
    elements: ['nonexempt', 'overtime-threshold'],
    why: "Second timecard, same 50-hour scheduled week. The pattern matters: one outlier week looks like an exception, eight in a row shows a policy. Use both timecards plus your paystubs to establish that overtime was the norm, not the exception.",
  },
  'schedule-notes': {
    elements: ['off-clock-hours'],
    why: "Your contemporaneous notes (timestamps in Apple Notes from Mar to Apr) record the actual minutes worked each shift, including the off-the-clock prep. Under Anderson v. Mt. Clemens Pottery, when the employer doesn't keep accurate records, your reasonable estimate of unpaid hours is what the court uses.",
  },
  'coworker-texts': {
    elements: ['off-clock-hours'],
    why: "The text thread with three coworkers about closing duties corroborates that off-the-clock work was company practice, not just your situation. Use these as supporting evidence behind Element 2. They strengthen your reasonable-estimate position.",
  },
  'paystubs': {
    elements: ['unpaid'],
    why: "Six pay periods of paystubs from Brooklyn Bites payroll. The rate column shows your hourly wage; the hours column shows what they paid you for. The gap between hours-paid and hours-worked is the unpaid amount. This supports Element 4 directly.",
  },
  'manager-text': {
    elements: ['off-clock-hours'],
    why: "The manager's text saying \"that's part of the job\" is the closest thing to a written admission you have. It acknowledges off-the-clock duties were required without claiming they were paid for. Useful for Element 2 and as procedural posture.",
  },
};

/* Atticus messages for Sarah Chen / wage-NY case. Mirrors V3_ATTICUS_PATEL
   keys (intake, cause, damages, readiness, evidence, filing, etc.) but framed
   for NY Labor Law § 198 + FLSA § 207 rather than auto-repair regulation. */
const V3_ATTICUS_WAGE_NY = {
  intake: {
    topic: 'Plain & Kind orientation',
    userLabel: 'Reading your case',
    body: "This sounds like a NY wage-theft case. NY Labor Law § 198 and the federal FLSA § 207 require time-and-a-half for hours over 40 in a workweek. You said you worked 50 scheduled hours each week and your manager required off-the-clock prep that was never on your timecard. That fact pattern is exactly what these statutes were written for.",
  },
  cause: {
    topic: 'Procedural research',
    userLabel: 'Working out the court paperwork',
    body: "Atticus's read: under NY Lab. Law § 198 + FLSA § 207, your strongest claim is unpaid overtime plus the off-the-clock regular wages. NY adds a separate recordkeeping violation under § 195(4), which lets your reasonable estimate of unpaid hours stand when the employer can't produce complete records. Both can be pled in the same complaint when both apply. The choice is yours.",
  },
  damages: {
    topic: 'Damages framework',
    userLabel: 'Working out the damages',
    body: "Atticus's read: your starting amount is the actual unpaid wages: $240 for off-the-clock regular hours and $800 for the overtime premium owed. NY Lab. Law § 198(1-a) adds 100% liquidated damages on top of those, unless the employer proves a good-faith basis. Attorney's fees are statutorily authorized but evaluate to $0 for pro-se cases. The math runs from what you affirm.",
  },
  readiness: {
    topic: 'Case-readiness read',
    body: "Your case sits at 76 of 100. Element completeness is strong (4 of 4 elements have evidence). Procedural prep is the lightest area. You still need to choose service-of-process method for NYC Civil Court. That's the last thing standing between you and a fileable claim.",
  },
  evidence: {
    topic: 'Evidence-element mapping',
    userLabel: 'Connecting evidence to the law',
    body: "Each piece of evidence supports one or more elements (the things you need to prove). We'll suggest matches based on what we see, but you confirm each one. You're the one making the legal argument. We're just helping you organize it.",
  },
  filing: {
    topic: 'Filing path',
    userLabel: 'Mapping the procedural steps',
    body: "NYC Civil Court · Small Claims Part is the right venue for wage claims under $5,000. Filing fee is $20. Court-mailed service ($10) is the cheap default for sub-$5K claims. Hearings typically scheduled 4 to 8 weeks out.",
  },
};

/* Audit-trail events for Sarah Chen / wage-NY case. Mirrors V3_AUDIT_EVENTS_PATEL
   shape with NY Lab. Law cite decisions instead of NY GBL decisions. */
const V3_AUDIT_EVENTS_WAGE_NY = [
  { ts: 'Apr 28, 6:14 PM', kind: 'atticus', label: 'Opened Atticus on intake summary', sys: 'Atticus offered Plain & Kind orientation', user: 'You read the orientation; no decision logged' },
  { ts: 'Apr 28, 6:42 PM', kind: 'cause-of-action', label: 'Selected NY Lab. Law § 198 + FLSA § 207 (unpaid overtime)', sys: 'System presented 3 NY wage options · Atticus recommended Card 1', user: 'You chose: unpaid overtime + off-the-clock regular wages' },
  { ts: 'Apr 28, 7:01 PM', kind: 'evidence', label: 'Affirmed: timecards support Elements 1 + 3', sys: 'Atticus read: 50 scheduled hrs/wk at straight-time rate', user: 'You affirmed the mapping' },
  { ts: 'Apr 28, 7:08 PM', kind: 'evidence', label: 'Affirmed: schedule notes support Element 2', sys: 'Atticus read: contemporaneous notes record actual minutes worked', user: 'You affirmed: notes establish off-the-clock hours' },
  { ts: 'Apr 28, 7:14 PM', kind: 'evidence', label: 'Affirmed: paystubs support Element 4', sys: 'Atticus read: paystubs show straight-time pay only across 6 pay periods', user: 'You affirmed the mapping' },
  { ts: 'Apr 28, 7:22 PM', kind: 'damages', label: 'Affirmed unpaid wages + overtime premium', sys: 'Atticus suggested: $240 off-clock + $800 OT premium per NY Lab. Law § 198', user: 'You affirmed: pursue $1,040 unpaid' },
  { ts: 'Apr 28, 7:23 PM', kind: 'damages', label: 'Affirmed 100% liquidated damages (NY)', sys: 'Atticus flagged § 198(1-a) liquidated as default unless employer rebuts', user: 'You affirmed: ask for the additional $1,040' },
  { ts: 'Apr 28, 7:24 PM', kind: 'damages', label: "Attorney's fees: not applicable (pro-se)", sys: 'Atticus marked card grayed (pro-se = $0 fees)', user: 'No affirmation needed. System-locked' },
  { ts: 'Apr 29, 9:08 AM', kind: 'atticus', label: 'Voice mode: Plain & Kind 12th-grade', sys: 'Default at MVP · NY-reviewed Atticus content', user: 'No change' },
  { ts: 'Apr 29, 9:12 AM', kind: 'filing', label: 'Selected court-mailed service', sys: 'Atticus recommended NYC Civ. Ct. clerk-mailing for under-$5K claims', user: 'You chose: court-mailed certified ($10)' },
  { ts: 'Apr 29, 9:16 AM', kind: 'filing', label: 'Affirmed claims for packet generation', sys: 'Affirmation gate shown · 3 claims (NY wage-specific)', user: 'You ticked all 3 affirmations' },
];

/* Tier 1 elements for NY auto-repair (Patel) */
const V3_ELEMENTS_AUTOREPAIR_NY = [
  { id: 'estimate', label: 'You received a written estimate for specific repair work', explain: 'NY GBL § 198-a requires the shop to give a written estimate before starting work. We need the original estimate document with the dollar amount and scope.', complete: true, evidence: 1 },
  { id: 'unauth-work', label: 'The shop performed work beyond the estimate', explain: 'Work that was added to the final invoice but was not in the original written estimate.', complete: true, evidence: 2 },
  { id: 'no-auth', label: 'You did not authorize the additional work in writing', explain: 'NY GBL § 198-a requires written authorization for any work over the estimate. Verbal "okay" by phone or text without acknowledging the new dollar amount is not enough.', complete: true, evidence: 2 },
  { id: 'damages', label: 'You paid for the unauthorized work', explain: 'Bank statement, credit card receipt, or paid invoice showing the amount you actually paid above the original estimate.', complete: true, evidence: 1 },
];

/* Patel's evidence */
const V3_EVIDENCE_PATEL = [
  { id: 'estimate', name: 'Written estimate · $400 brake job · 2/22/2026', type: 'PDF', maps: ['estimate'] },
  { id: 'invoice', name: 'Final invoice · $2,840 · 2/24/2026', type: 'PDF', maps: ['unauth-work', 'damages'] },
  { id: 'photos', name: 'Photos of work performed (8)', type: 'Image set', exif: 'EXIF dates: 2/24–2/26/2026', maps: ['unauth-work'] },
  { id: 'texts', name: 'Text messages with shop · no $ confirmation', type: 'Screenshot', maps: ['no-auth'] },
  { id: 'bank', name: 'Bank statement · $2,840 ACH · 2/26/2026', type: 'PDF', maps: ['damages'] },
  { id: 'demand', name: 'Demand letter sent (certified) · 3/15/2026', type: 'PDF + USPS', maps: ['no-auth'] },
];

/* Damages decomposition for Patel (data only. V3Damages reads from getMatterDamagesConfig) */
const V3_DAMAGES_PATEL = [
  { label: 'Unauthorized charges', amount: 2440, basis: '$2,840 final invoice − $400 authorized estimate = $2,440 over estimate', user: true },
  { label: 'GBL § 349 statutory damages', amount: 50, basis: 'NY GBL § 349(h). $50 statutory OR actual, whichever is greater', user: false, atticus: true },
  { label: 'Treble (if willful)', amount: 4880, basis: 'NY GBL § 349 court discretion to treble actual where conduct is willful or knowing', user: false, atticus: true, conditional: true },
  { label: 'Filing fee', amount: 25, basis: 'NYC Civil Court · small-claims filing fee', user: false },
];

/* Per-evidence Atticus reads for Patel */
const V3_EVIDENCE_READS_PATEL = {
  'estimate': {
    elements: ['estimate'],
    why: "This is the foundation document. The written $400 estimate dated 2/22/2026 establishes Element 1 (you received a written estimate). It also defines the boundary. Anything above this dollar amount required your separate written authorization under GBL § 198-a.",
  },
  'invoice': {
    elements: ['unauth-work', 'damages'],
    why: "The final invoice supports two elements. It shows work was performed above the estimate (Element 2) and the $2,440 differential between authorized $400 and final $2,840 is the unauthorized-work damages amount (Element 4).",
  },
  'photos': {
    elements: ['unauth-work'],
    why: "The 8 photos with EXIF dates 2/24–2/26 show the actual work performed beyond the brake job. Useful corroboration for Element 2. They prove the additional work was real, not just a billing error.",
  },
  'texts': {
    elements: ['no-auth'],
    why: "The text thread shows the shop never sent you a written authorization request for the additional work, and you never confirmed a new dollar amount. GBL § 198-a is strict. Verbal okays don't satisfy the written-authorization requirement. This supports Element 3.",
  },
  'bank': {
    elements: ['damages'],
    why: "The ACH on 2/26 shows you actually paid the $2,840. Without proof of payment, even a winning case has no recoverable damages. This locks in Element 4.",
  },
  'demand': {
    elements: ['no-auth'],
    why: "The certified-mail demand letter establishes your good-faith pre-litigation effort and dates the dispute. It also captures your written position that you did not authorize the additional work. Useful for Element 3 and as procedural posture.",
  },
};

/* Atticus messages for Patel (NY GBL framing. No sandbox cover language) */
const V3_ATTICUS_PATEL = {
  intake: {
    topic: 'Plain & Kind orientation',
    userLabel: 'Reading your case',
    body: "This sounds like a NY auto-repair case. Under NY GBL § 198-a, a repair shop must give you a written estimate AND obtain your written authorization for any work above that estimate. You said the original estimate was $400 and the final bill was $2,840. That's $2,440 in additional work. If you didn't sign off on that in writing, the shop's conduct may not satisfy the statute's authorization requirement.",
  },
  cause: {
    topic: 'Procedural research',
    userLabel: 'Working out the court paperwork',
    body: "Atticus's read: GBL § 349 (deceptive business practices) and GBL § 198-a (auto-repair regulation) are both potentially applicable here. GBL § 349 carries broader remedies: actual damages plus $50 statutory, plus the court can choose to triple the damages where conduct is found willful. GBL § 198-a covers actual damages. Both can be pled in the same complaint when both apply. The choice of which to pursue is yours.",
  },
  damages: {
    topic: 'Damages framework',
    userLabel: 'Working out the damages',
    body: "Atticus's read: the starting amount you can ask for is the $2,440 in unauthorized charges. NY GBL § 349 includes $50 statutory damages (awarded automatically when liability is established). The court can choose to triple the damages where conduct is found willful. The third damages line is conditioned on a willfulness finding. You affirm what you want to pursue.",
  },
  readiness: {
    topic: 'Case-readiness check',
    body: "Your case sits at 79 of 100. Element completeness is strong (4 of 4 elements have evidence). Procedural prep is the lightest area. You still need to choose service-of-process method for NYC Civil Court. That's the last thing standing between you and a fileable claim.",
  },
  evidence: {
    topic: 'Evidence-element mapping',
    userLabel: 'Connecting evidence to the law',
    body: "Each piece of evidence supports one or more 'elements'. The things you need to prove. We'll suggest matches based on what we see, but you confirm each one. You're the one making the legal argument; we're just helping you organize it.",
  },
  filing: {
    topic: 'Where and how to file',
    body: "You'll file at NYC Civil Court small-claims part. Kings County (Brooklyn). Filing fee is $25 for claims up to $5,000. After filing, you'll need to serve East Side Auto Repair. Certified mail is acceptable for amounts under $5,000 in NYC small-claims. The court will set a hearing 4–8 weeks out.",
  },
  courtroom: {
    topic: 'Courtroom prep',
    body: "Your hearing is in 4–6 weeks. NYC Civil Court small-claims hearings run 15–20 minutes. The judge will have read your packet. Per NY UPL discipline, we don't write scripted lines for you to recite. Instead, you'll see procedural framing: what judges typically ask in auto-repair cases, how to present your evidence, and courtroom etiquette.",
  },
  documents: {
    topic: 'Final document review',
    body: "Read each document carefully. We've drafted everything from your affirmed claims and evidence. But you're the one signing. If any fact reads wrong, edit it before you affirm. Once you affirm, the packet is locked and ready to file.",
  },
};

/* Theory cards for V3Damages (per matter) */
const V3_DAMAGES_THEORY_CARDS_MARIA = [
  {
    id: 'wrongful', recommended: true, eligible: true,
    icon: 'check',
    title: '2× wrongful retention',
    cite: 'Utah § 57-17-3(c)',
    headline: '$3,600',
    body: "If a court finds the landlord wrongfully retained your deposit, Utah law lets you recover double the wrongfully-retained amount. For a $1,800 deposit, that's the original $1,800 plus $1,800 statutory penalty: $3,600 from this theory. Combined with lost time and court costs the total recovery sought is $3,860.",
    trigger: "Trigger: court finds wrongful retention occurred. Default: yes for cases like yours.",
  },
  {
    id: 'costs', recommended: false, eligible: true,
    icon: 'warn',
    title: 'Lost time + court costs',
    cite: 'Utah § 78A-8 (small-claims)',
    headline: '$260',
    body: 'Documented lost paid time off addressing this matter (8 hrs × $25/hr from your employer records = $200), plus filing and service fees ($60). Both are recoverable as part of the judgment if you prevail.',
    trigger: 'Trigger: you prevail on the underlying claim. Costs and documented lost time are then included in the judgment.',
  },
  {
    id: 'attyfees', recommended: false, eligible: false,
    icon: 'info',
    title: "Attorney's fees",
    cite: 'Not authorized for this case-type',
    headline: 'N/A',
    body: "Utah § 57-17-3 doesn't authorize attorney's fees for security-deposit recovery in small-claims court. We're not including this. It would be unsupported.",
    trigger: null,
    readWhy: "Why this is grayed: Utah's fee-shifting statutes (§ 78B-5-826) don't extend to § 57-17-3 small-claims actions. Without statutory authorization or a fee-shifting clause in your lease, both parties bear their own attorney's fees by default.",
  },
];

const V3_DAMAGES_THEORY_CARDS_PATEL = [
  {
    id: 'deceptive', recommended: true, eligible: true,
    icon: 'check',
    title: 'Actual damages + statutory $50',
    cite: 'NY GBL § 349',
    headline: '$2,490',
    body: "GBL § 349 lets you recover actual damages PLUS $50 statutory, OR statutory alone if actuals are smaller. Your $2,440 in unauthorized charges + $50 statutory = $2,490 baseline.",
    trigger: 'Trigger: court finds deceptive consumer-oriented conduct. Default: yes for repair-shop overcharges without written authorization.',
  },
  {
    id: 'treble', recommended: false, eligible: true,
    icon: 'warn',
    title: 'Treble damages (if willful)',
    cite: 'NY GBL § 349. Court discretion',
    headline: '+$4,880',
    body: 'If the court finds the deception was willful or knowing, it can treble actual damages. That would add $4,880 (the additional 2× of $2,440) on top of baseline. Discretionary. Not automatic.',
    trigger: 'Trigger: court finds the conduct was knowing or willful. Atticus suggests this is plausible given the shop never sent a written authorization request.',
  },
  {
    id: 'attyfees-ny', recommended: false, eligible: false,
    icon: 'info',
    title: "Reasonable attorney's fees",
    cite: 'NY GBL § 349(h)',
    headline: 'N/A (pro-se)',
    body: "GBL § 349(h) authorizes reasonable attorney's fees for prevailing plaintiffs. Pro-se claims don't generate attorney's fees because there's no attorney. We're not including this.",
    trigger: null,
    readWhy: "Why this is grayed: GBL § 349(h) requires a prevailing plaintiff with reasonable attorney's fees actually incurred. Pro-se litigants by definition have no attorney's fees to recover. If you later retain counsel, this category becomes available.",
  },
];

const V3_DAMAGES_ITEMS_MARIA = (theories) => [
  { label: 'Wrongfully retained deposit', calc: '(user-affirmed)', amount: 1800, include: true },
  { label: 'Statutory 2× multiplier', calc: '$1,800 × 2 (per § 57-17-3(c))', amount: 1800, include: theories.wrongful, theory: 'wrongful' },
  { label: 'Documented lost paid time off', calc: '8 hrs × $25/hr (employer records)', amount: 200, include: theories.costs, theory: 'costs' },
  { label: 'Court costs', calc: 'Filing + service fees (Salt Lake County)', amount: 60, include: theories.costs, theory: 'costs' },
];

const V3_DAMAGES_ITEMS_PATEL = (theories) => [
  { label: 'Unauthorized charges', calc: '$2,840 invoice − $400 estimate', amount: 2440, include: true },
  { label: 'GBL § 349 statutory damages', calc: '$50 (per § 349(h))', amount: 50, include: theories.deceptive, theory: 'deceptive' },
  { label: 'Treble (if willful, court discretion)', calc: '$2,440 × 2 incremental ($7,320 total)', amount: 4880, include: theories.treble, theory: 'treble' },
  { label: 'Filing fee', calc: 'NYC Civil Court · small-claims', amount: 25, include: true },
];

const V3_DAMAGES_INITIAL_MARIA = { wrongful: true, costs: true };
const V3_DAMAGES_INITIAL_PATEL = { deceptive: true, treble: false };

/* Story 0.49 — NY wage/overtime case (newcase scenario). Matches the canned
   restaurant wage scenario: 8 weeks at 50 hrs/wk paid straight time only
   ($20/hr regular rate), plus 12 hrs of off-the-clock prep. Statutory cites
   verified by Atticus 2026-05-09. */
const V3_DAMAGES_THEORY_CARDS_WAGE_NY = [
  {
    id: 'unpaid', recommended: true, eligible: true,
    icon: 'check',
    title: 'Unpaid regular wages (off-the-clock)',
    cite: 'NY Lab Law § 198',
    headline: '$240',
    body: '12 hours of pre-shift prep work that you weren\'t paid for, at your regular rate of $20/hr. Off-the-clock work is wages owed under New York Labor Law.',
    trigger: 'Trigger: employer required or suffered the work. Default: yes if it was part of your regular duties.',
  },
  {
    id: 'overtime', recommended: true, eligible: true,
    icon: 'check',
    title: 'Overtime premium owed',
    cite: 'FLSA § 207 + 12 NYCRR § 142-2.2',
    headline: '$800',
    body: 'You worked 50 hrs/week for 8 weeks but were paid straight time. Federal + NY law require time-and-a-half over 40 hrs/week. The half-time premium owed: 80 hrs × $10/hr = $800.',
    trigger: 'Trigger: hours over 40 in a workweek. Default: yes per your timecard records.',
  },
  {
    id: 'liquidated', recommended: true, eligible: true,
    icon: 'warn',
    title: 'Liquidated damages (NY)',
    cite: 'NY Lab Law § 198(1-a)',
    headline: '+$1,040',
    body: 'NY law adds 100% liquidated damages on top of unpaid wages and overtime, unless the employer proves a good-faith basis. Equal to your actual damages: $240 + $800 = $1,040.',
    trigger: 'Trigger: employer cannot show a good-faith reasonable basis for the underpayment. Court awards by default; employer must rebut.',
  },
  {
    id: 'attyfees-wage', recommended: false, eligible: false,
    icon: 'info',
    title: "Reasonable attorney's fees + costs",
    cite: 'NY Lab Law § 198(1-a) + FLSA § 216(b)',
    headline: 'N/A (pro-se)',
    body: "Both NY and federal law authorize attorney's fees for prevailing plaintiffs in wage cases. Pro-se claims don't generate attorney's fees because there's no attorney to bill.",
    trigger: null,
    readWhy: "Why this is grayed: NY Lab Law § 198(1-a) and FLSA § 216(b) require a prevailing plaintiff with attorney's fees actually incurred. Pro-se litigants by definition have no attorney's fees to recover. If you later retain counsel, this category becomes available.",
  },
];

const V3_DAMAGES_ITEMS_WAGE_NY = (theories) => [
  { label: 'Unpaid regular wages (off-the-clock)', calc: '12 hrs × $20/hr', amount: 240, include: theories.unpaid, theory: 'unpaid' },
  { label: 'Overtime premium owed', calc: '80 hrs × $10/hr (half-time premium)', amount: 800, include: theories.overtime, theory: 'overtime' },
  { label: 'Liquidated damages (NY 100%)', calc: '($240 + $800) × 100% per § 198(1-a)', amount: 1040, include: theories.liquidated, theory: 'liquidated' },
  { label: 'Court filing fee', calc: 'NYC Civil Court · small-claims', amount: 20, include: true },
];

const V3_DAMAGES_INITIAL_WAGE_NY = { unpaid: true, overtime: true, liquidated: true };

/* Filing + service config per matter */
const V3_FILING_MARIA = {
  courtName: 'Salt Lake County Justice Court',
  courtAddress: '450 S State St · Salt Lake City, UT',
  fee: '$60',
  feeNote: 'Fee waiver available. Household income < $35K/yr',
  hearingTimeline: '4–6 weeks',
  hearingNote: 'Justice Court calendar typically',
  serviceIntro: 'Utah requires one of these methods:',
  serviceOptions: [
    { id: 'cert-mail', label: 'Certified mail with return receipt', cost: '$8.45', note: 'You mail it. Cheaper. Works if landlord accepts mail at the office address.', recommend: true },
    { id: 'process-server', label: 'Hire a process server', cost: '$65–$95', note: 'Professional delivers the papers in person. More reliable if you suspect avoidance.' },
    { id: 'sheriff', label: 'County sheriff service', cost: '$30', note: 'Sheriff delivers the papers. Slower (10–20 days) but very official.' },
  ],
};

const V3_FILING_PATEL = {
  /* Court info is shared between Patel's original auto-repair scenario and
     the current Sarah Chen wage-theft repurposing. Fee adjusted to $20 to
     match NYC Civil Court small-claims fee schedule for claims $1K-$5K. */
  courtName: 'NYC Civil Court · Kings County',
  courtAddress: '141 Livingston St · Brooklyn, NY',
  fee: '$20',
  feeNote: 'Poor-person\'s relief available. Household income < $32K/yr (NYC)',
  hearingTimeline: '4–8 weeks',
  hearingNote: 'NYC Civil Court small-claims part',
  serviceIntro: 'NY small-claims under $5K accepts these methods:',
  serviceOptions: [
    { id: 'court-mail-ny', label: 'Court-mailed service (clerk handles it)', cost: '$10', note: 'NYC Civil Court clerk sends certified mail for you. Slowest path but cheapest.', recommend: true },
    { id: 'cert-mail-ny', label: 'Certified mail with return receipt', cost: '$8.45', note: 'You mail it directly. Acceptable for NY small-claims under $5,000.' },
    { id: 'personal-ny', label: 'Personal service (process server)', cost: '$50–$80', note: 'Server hands papers to a person at the business. Most reliable; required if mail bounces.' },
  ],
};

/* Tier 2 — contractor non-payment over the $15K small-claims cap. Routes to
   Utah District Court (general jurisdiction, Provo seat for HOA in Utah County
   per matter data). Lawyer-led from Pleadings on; this dataset still reads in
   V3Filing as the procedural reference. */
const V3_FILING_DANIEL = {
  courtName: 'Utah District Court · 4th District',
  courtAddress: '125 N 100 W · Provo, UT (Utah County seat)',
  fee: '$375',
  feeNote: 'Civil filing fee for claims over $15,000. Fee waivers reviewed case-by-case.',
  hearingTimeline: '6–10 months',
  hearingNote: 'District Court civil docket. First scheduling conference typically 60–90 days post-service',
  serviceIntro: 'Utah Rule of Civil Procedure 4. Service is lawyer-led at this tier:',
  serviceOptions: [
    { id: 'process-server-ut', label: 'Personal service via licensed process server', cost: '$95–$150', note: 'Standard for District Court civil cases. Your lawyer files the proof of service.', recommend: true },
    { id: 'sheriff-ut', label: 'County sheriff service', cost: '$50', note: 'Slower (10–20 days) but accepted for HOA registered agent at the management office.' },
    { id: 'pubservice-ut', label: 'Service by publication', cost: '$200+ in publication costs', note: 'Last resort if registered agent cannot be located after diligent inquiry. Requires court order.' },
  ],
};

/* Audit-trail events per matter. Every legally-significant decision logged */
const V3_AUDIT_EVENTS_MARIA = [
  { ts: 'Apr 18, 11:12 AM', kind: 'atticus', label: 'Opened Atticus on intake summary', sys: 'Atticus offered Plain & Kind orientation', user: 'You read the orientation; no decision logged' },
  { ts: 'Apr 18, 11:42 AM', kind: 'cause-of-action', label: 'Selected wrongful retention of security deposit', sys: 'System presented 2 options · Atticus recommended Card 1', user: 'You chose: wrongful retention (Utah § 57-17-3)' },
  { ts: 'Apr 18, 12:08 PM', kind: 'evidence', label: 'Affirmed: lease agreement supports Elements 1 + 2', sys: 'Atticus suggested elements 1, 2 with explanation', user: 'You affirmed: lease supports tenancy + deposit-paid' },
  { ts: 'Apr 18, 12:14 PM', kind: 'evidence', label: 'Affirmed: deposit receipt supports Element 2', sys: 'Atticus read: receipt shows $1,800 ACH on 4/1/2024', user: 'You affirmed the mapping' },
  { ts: 'Apr 18, 12:21 PM', kind: 'evidence', label: 'Affirmed: walkthrough photos support Element 3', sys: 'Atticus read: 12 photos with 3/15/2026 EXIF', user: 'You affirmed the mapping' },
  { ts: 'Apr 18, 12:31 PM', kind: 'damages', label: 'Affirmed statutory 2× damages theory', sys: 'Atticus suggested under § 57-17-3(c)', user: 'You affirmed: ask for 2× damages ($3,600)' },
  { ts: 'Apr 18, 12:32 PM', kind: 'damages', label: 'Affirmed lost time + court costs', sys: 'Atticus suggested per Utah § 78A-8 + employer records for documented PTO', user: 'You affirmed: include $260 ($200 lost time + $60 costs)' },
  { ts: 'Apr 18, 12:33 PM', kind: 'damages', label: "Declined attorney's fees (statute doesn't authorize)", sys: 'Atticus marked grayed; not eligible', user: 'No affirmation needed. System-locked' },
  { ts: 'Apr 19, 9:04 AM', kind: 'atticus', label: 'Voice mode: Plain & Kind 12th-grade', sys: 'Default at MVP', user: 'No change' },
  { ts: 'Apr 19, 9:08 AM', kind: 'filing', label: 'Selected service-of-process method', sys: 'Atticus recommended certified mail for this case', user: 'You chose: certified mail with return receipt ($15)' },
  { ts: 'Apr 19, 9:12 AM', kind: 'filing', label: 'Affirmed claims for packet generation', sys: 'Affirmation gate shown · 3 claims', user: 'You ticked all 3 affirmations' },
];

const V3_AUDIT_EVENTS_PATEL = [
  { ts: 'Mar 16, 7:44 PM', kind: 'atticus', label: 'Opened Atticus on intake summary', sys: 'Atticus offered Plain & Kind orientation', user: 'You read the orientation; no decision logged' },
  { ts: 'Mar 16, 8:22 PM', kind: 'cause-of-action', label: 'Selected GBL § 349 + § 198-a (auto-repair regulation)', sys: 'System presented 2 NY GBL options · Atticus recommended Card 1 (deceptive practices)', user: 'You chose: deceptive business practices + auto-repair regulation' },
  { ts: 'Mar 16, 8:48 PM', kind: 'evidence', label: 'Affirmed: written estimate supports Element 1', sys: 'Atticus read: $400 estimate dated 2/22/2026 establishes the authorized scope', user: 'You affirmed the mapping' },
  { ts: 'Mar 16, 8:51 PM', kind: 'evidence', label: 'Affirmed: final invoice supports Elements 2 + 4', sys: 'Atticus read: $2,840 invoice − $400 estimate = $2,440 unauthorized', user: 'You affirmed the mapping' },
  { ts: 'Mar 16, 8:55 PM', kind: 'evidence', label: 'Affirmed: text screenshots support Element 3', sys: 'Atticus read: no written authorization request from shop', user: 'You affirmed: texts show no authorization confirmation' },
  { ts: 'Mar 16, 9:02 PM', kind: 'damages', label: 'Affirmed deceptive-practices baseline ($2,490)', sys: 'Atticus suggested: actual ($2,440) + GBL § 349 statutory ($50)', user: 'You affirmed: pursue the starting damages amount' },
  { ts: 'Mar 16, 9:03 PM', kind: 'damages', label: 'Considered treble damages (deferred)', sys: 'Atticus flagged Card 2 as conditional on willfulness finding', user: 'You declined for now. Court will decide if willful' },
  { ts: 'Mar 16, 9:04 PM', kind: 'damages', label: "Attorney's fees: not applicable (pro-se)", sys: 'Atticus marked Card 3 grayed (pro-se = $0 fees)', user: 'No affirmation needed. System-locked' },
  { ts: 'Mar 17, 8:14 AM', kind: 'atticus', label: 'Voice mode: Plain & Kind 12th-grade', sys: 'Default at MVP · NY-reviewed Atticus content', user: 'No change' },
  { ts: 'Mar 17, 8:18 AM', kind: 'filing', label: 'Selected court-mailed service', sys: 'Atticus recommended NYC Civ. Ct. clerk-mailing for under-$5K claims', user: 'You chose: court-mailed certified ($10)' },
  { ts: 'Mar 17, 8:22 AM', kind: 'filing', label: 'Affirmed claims for packet generation', sys: 'Affirmation gate shown · 3 claims (NY-specific)', user: 'You ticked all 3 affirmations' },
];

/* Document + affirmation config per matter */
const V3_DOCS_MARIA = {
  formName: 'Statement of Claim. Form 600S',
  courtHeader: 'Salt Lake County Justice Court',
  plaintiff: 'Maria Vargas, 1834 W. Indiana Ave, Salt Lake City, UT 84104',
  defendant: 'Westhaven Properties LLC, 552 S State St, Salt Lake City, UT 84111',
  amountLine: '$3,860 (deposit + statutory 2× + lost time + costs)',
  statuteLine: 'Utah Code § 57-17-3. Wrongful retention of security deposit',
  bodyText: 'Plaintiff Maria Vargas rented Apt. 4B at 1834 W. Indiana Ave from Defendant from April 1, 2024 through March 15, 2026. Plaintiff paid a $1,800 security deposit at move-in (Exhibit 2). Plaintiff vacated on March 15, 2026, returned the keys (Exhibit 4), and left the unit clean (Exhibit 5). As of April 16, 2026. 32 days after end of tenancy. Defendant has neither returned the deposit nor sent a written itemization, in violation of Utah Code § 57-17-3…',
  affirmations: [
    { k: 'a', label: 'I am claiming wrongful retention of my $1,800 security deposit under Utah § 57-17-3.' },
    { k: 'b', label: 'I am asking for 2× statutory damages ($1,800 additional). Atticus suggested this; I am affirming it as my own claim.' },
    { k: 'c', label: 'The facts in the Statement of Claim are true to the best of my knowledge.' },
  ],
};

const V3_DOCS_PATEL = {
  formName: 'Statement of Claim. NYC Civ. Ct. Form CIV-SC-1A',
  courtHeader: 'NYC Civil Court · Kings County (Small-Claims Part)',
  plaintiff: 'Sarah Patel, 482 8th Ave, Apt 3R, Brooklyn, NY 11215',
  defendant: 'East Side Auto Repair Inc., 2114 Coney Island Ave, Brooklyn, NY 11223',
  amountLine: '$2,490 (unauthorized charges + statutory $50; treble may apply)',
  statuteLine: 'NY GBL § 349 + § 198-a. Deceptive practices / auto-repair regulation',
  bodyText: 'Plaintiff Sarah Patel authorized a $400 brake-pad replacement at Defendant\'s shop on February 22, 2026 (Exhibit 1. Written estimate). On February 24, 2026, Defendant invoiced Plaintiff $2,840, having performed $2,440 of additional repair work without obtaining Plaintiff\'s written authorization as required by NY GBL § 198-a (Exhibit 2. Final invoice; Exhibit 4. Text exchange showing no written authorization request). Plaintiff paid the $2,840 under duress to retrieve her vehicle (Exhibit 5. Bank statement). Defendant has refused to refund the unauthorized $2,440 despite a certified-mail demand on March 15, 2026 (Exhibit 6).',
  affirmations: [
    { k: 'a', label: 'I am claiming deceptive business practices under NY GBL § 349 and an auto-repair regulation violation under NY GBL § 198-a.' },
    { k: 'b', label: 'I did not give written authorization for the work above the $400 estimate; the $2,440 differential is unauthorized.' },
    { k: 'c', label: 'The facts in the Statement of Claim are true to the best of my knowledge.' },
  ],
};

/* Story 0.49 — NY wage/overtime case packet. Statement of Claim for NYC Civil
   Court small-claims part. Damages line + statute cite reflect Atticus-verified
   NY Lab Law § 198 + FLSA § 207. Body narrative pulls from the canned wage
   scenario (8 weeks at 50 hrs/wk, off-the-clock prep, no overtime premium). */
const V3_DOCS_WAGE_NY = {
  formName: 'Statement of Claim. NYC Civ. Ct. Form CIV-SC-1A',
  courtHeader: 'NYC Civil Court · Kings County (Small-Claims Part)',
  plaintiff: 'Sarah Chen, 482 8th Ave, Apt 3R, Brooklyn, NY 11215',
  defendant: 'Brooklyn Bites LLC, 1108 Amsterdam Ave, New York, NY 10025',
  amountLine: '$2,100 (unpaid wages + overtime premium + 100% liquidated damages + filing fee)',
  statuteLine: 'NY Lab Law § 198(1-a) + FLSA § 207. Wage payment / overtime',
  bodyText: 'Plaintiff Sarah Chen worked as a server for Defendant from January 6, 2026 through February 28, 2026 (8 weeks). Her regular rate was $20/hr (Exhibit 1. Offer letter). During each of those 8 weeks Plaintiff worked 50 scheduled hours but was paid straight time only, not the time-and-a-half rate required for hours over 40 by FLSA § 207 and 12 NYCRR § 142-2.2 (Exhibits 2. Schedules; 3. Paystubs). The unpaid overtime premium totals $800 (10 hrs/week × 8 weeks × $10/hr half-time). Plaintiff also performed 12 hours of pre-shift prep work that was required but not recorded on her timecard, totaling $240 in unpaid regular wages (Exhibits 4. Coworker text; 5. Manager text instructing Plaintiff to "clock in after setup"). Under NY Lab Law § 198(1-a), Plaintiff seeks liquidated damages equal to 100% of the unpaid amount ($1,040).',
  affirmations: [
    { k: 'a', label: 'I am claiming unpaid wages and overtime premium under NY Lab Law § 198 and FLSA § 207, plus 100% liquidated damages under NY Lab Law § 198(1-a).' },
    { k: 'b', label: 'My regular rate was $20/hr; I worked 10 hours of overtime each week for 8 weeks; I performed 12 hours of unpaid pre-shift prep work.' },
    { k: 'c', label: 'The facts in the Statement of Claim are true to the best of my knowledge.' },
  ],
};

/* Per-matter dataset router. Pick the right bundle by matter id */
function getMatterDataset(matterId) {
  if (matterId === 'patel') return {
    /* Sarah Chen / Brooklyn Bites / NY wage theft. Wires up the wage-NY data
       authored for the new-case triage flow (V3_CAUSES_WAGE_NY,
       V3_ELEMENTS_WAGE_NY, V3_EVIDENCE_WAGE_NY, V3_DAMAGES_*_WAGE_NY,
       V3_DOCS_WAGE_NY) plus the wage-specific Atticus/Filing constants. */
    causes: V3_CAUSES_WAGE_NY,
    elements: V3_ELEMENTS_WAGE_NY,
    evidence: V3_EVIDENCE_WAGE_NY,
    damages: V3_DAMAGES_PATEL,
    damagesTheoryCards: V3_DAMAGES_THEORY_CARDS_WAGE_NY,
    damagesItems: V3_DAMAGES_ITEMS_WAGE_NY,
    damagesInitial: V3_DAMAGES_INITIAL_WAGE_NY,
    damagesIntroLine: 'Wage cases stack up fast. NY Labor Law § 198 and federal FLSA § 207 give you several layers (unpaid wages, overtime premium, and 100% liquidated damages on top). Pick what to claim. The math runs from your timecard data.',
    evidenceReads: V3_EVIDENCE_READS_WAGE_NY,
    atticus: V3_ATTICUS_WAGE_NY,
    filing: V3_FILING_PATEL,
    docs: V3_DOCS_WAGE_NY,
    auditEvents: V3_AUDIT_EVENTS_WAGE_NY,
    caseNoun: 'wage / overtime',
    initialAffirmed: { 'timecard-w12': 'affirmed', 'timecard-w14': 'affirmed', 'paystubs': 'affirmed', 'schedule-notes': 'affirmed', 'coworker-texts': 'affirmed' },
  };
  if (matterId === 'newcase') {
    /* Story 0.49: brand-new matter from triage. Default flow built around the
       canned MOCK_SCENARIO (NY restaurant wage / overtime case). For the
       walk-through demo we surface real causes + elements + a mock evidence
       set; V3Evidence's empty-state covers the moment before files are
       imported, then the file-grab moment populates with V3_EVIDENCE_WAGE_NY. */
    const m = window.V3_MATTERS && window.V3_MATTERS.newcase;
    const isNY = m && m.jurisdiction === 'ny';
    return {
      causes: isNY ? V3_CAUSES_WAGE_NY : V3_CAUSES_MARIA,
      elements: isNY ? V3_ELEMENTS_WAGE_NY : V3_ELEMENTS_DEPOSIT_UT,
      evidence: (m && m.__evidenceImported) ? V3_EVIDENCE_WAGE_NY : [],
      // Story 0.49 Phase D — wage-NY damages dataset (was Patel auto-repair, wrong domain).
      damages: V3_DAMAGES_PATEL,
      damagesTheoryCards: isNY ? V3_DAMAGES_THEORY_CARDS_WAGE_NY : V3_DAMAGES_THEORY_CARDS_PATEL,
      damagesItems: isNY ? V3_DAMAGES_ITEMS_WAGE_NY : V3_DAMAGES_ITEMS_PATEL,
      damagesInitial: isNY ? V3_DAMAGES_INITIAL_WAGE_NY : V3_DAMAGES_INITIAL_PATEL,
      damagesIntroLine: isNY
        ? "Wage cases stack up fast. NY Labor Law § 198 and federal FLSA § 207 give you several layers: unpaid wages, overtime premium, and 100% liquidated damages on top. Pick what you want to claim. The math runs from your timecard data."
        : "Once you affirm your evidence, your damages roll up automatically here.",
      evidenceReads: {},
      atticus: isNY ? V3_ATTICUS_WAGE_NY : V3_ATTICUS,
      filing: isNY ? V3_FILING_PATEL : V3_FILING_MARIA,
      docs: isNY ? V3_DOCS_WAGE_NY : V3_DOCS_PATEL,
      auditEvents: [],
      caseNoun: 'wage / overtime',
      initialAffirmed: {},
    };
  }
  if (matterId === 'daniel') return {
    /* Tier 2 — V3LawyerPitch is the primary surface; the rest of the dataset
       falls back to Maria's data because Daniel's full Tier 2 dataset ships
       with Story 0.20. Filing is overridden so the audit-flagged "Salt Lake
       County Justice Court" leak from Maria's data is fixed (audit 0.40.3). */
    causes: V3_CAUSES_MARIA,
    elements: V3_ELEMENTS_DEPOSIT_UT,
    evidence: V3_EVIDENCE_MARIA,
    damages: V3_DAMAGES_MARIA,
    damagesTheoryCards: V3_DAMAGES_THEORY_CARDS_MARIA,
    damagesItems: V3_DAMAGES_ITEMS_MARIA,
    damagesInitial: V3_DAMAGES_INITIAL_MARIA,
    damagesIntroLine: 'A lawyer will lead damages strategy at this tier; this is the prep view.',
    evidenceReads: V3_EVIDENCE_READS,
    atticus: V3_ATTICUS,
    filing: V3_FILING_DANIEL,
    docs: V3_DOCS_MARIA,
    auditEvents: V3_AUDIT_EVENTS_MARIA,
    caseNoun: 'contractor non-payment',
    initialAffirmed: {},
  };
  // Maria default
  return {
    causes: V3_CAUSES_MARIA,
    elements: V3_ELEMENTS_DEPOSIT_UT,
    evidence: V3_EVIDENCE_MARIA,
    damages: V3_DAMAGES_MARIA,
    damagesTheoryCards: V3_DAMAGES_THEORY_CARDS_MARIA,
    damagesItems: V3_DAMAGES_ITEMS_MARIA,
    damagesInitial: V3_DAMAGES_INITIAL_MARIA,
    damagesIntroLine: 'The law lets you ask for several different kinds of damages. Some are mechanical (you put in numbers, math happens). Others depend on whether you can prove certain things. We\'ll show you what\'s available. You choose what to pursue.',
    evidenceReads: V3_EVIDENCE_READS,
    atticus: V3_ATTICUS,
    filing: V3_FILING_MARIA,
    docs: V3_DOCS_MARIA,
    auditEvents: V3_AUDIT_EVENTS_MARIA,
    caseNoun: 'security deposit',
    initialAffirmed: { 'lease': 'affirmed', 'deposit-receipt': 'affirmed' },
  };
}

/* Atticus messages catalog. Maria-specific copy from 01-content-pack.md, Plain & Kind 12th-grade */
const V3_ATTICUS = {
  intake: {
    topic: 'Plain & Kind orientation',
    userLabel: 'Reading your case',
    body: "This sounds like a security-deposit case. Utah law gives a landlord 30 days from your forwarding address to either return your deposit or send a written explanation of any deductions. You said they did neither. That's the kind of fact pattern Utah § 57-17-3 was written for.",
  },
  cause: {
    topic: 'Procedural research',
    userLabel: 'Working out the court paperwork',
    body: "Atticus's read: under Utah § 57-17-3, a landlord has 30 days from your forwarding address. By that point they must either return your deposit or send a written explanation of any deductions. You said they did neither. That fact pattern is consistent with what § 57-17-3 calls wrongful retention. The wrongful-retention option is the strongest match against the framework. The choice of which cause to pursue is yours.",
  },
  damages: {
    topic: 'Damages framework',
    userLabel: 'Working out the damages',
    body: "Atticus's read: based on your facts, the wrongful-retention damages line (2× the retained deposit under Utah § 57-17-3) and the court-costs line are both potentially available. Attorney's fees are not authorized in this case type. You affirm what you want to pursue.",
  },
  readiness: {
    topic: 'Case-readiness read',
    body: "Your case sits at 87 of 100. Element completeness is strong. 4 of 4 elements have supporting evidence. Procedural prep is the lightest area; you still need to choose a service-of-process method. That's the last thing standing between you and a fileable packet.",
  },
  evidence: {
    topic: 'Evidence-element mapping',
    userLabel: 'Connecting evidence to the law',
    body: "Each piece of evidence supports one or more 'elements'. The things you need to prove. We'll suggest matches based on what we see, but you confirm each one. You're the one making the legal argument; we're just helping you organize it.",
  },
  filing: {
    topic: 'Where and how to file',
    body: "You'll file at Salt Lake County Justice Court small-claims department, 450 South State Street. Combined court costs come to about $60 (filing fee plus certified-mail service of process), waivable based on income. After filing, certified mail with return receipt is usually the simplest service method. The court will set a hearing 4–8 weeks out.",
  },
  courtroom: {
    topic: 'Courtroom prep',
    body: "Your hearing is in 6 weeks. Salt Lake County Justice Court small-claims hearings run 15–30 minutes. The judge will have read your packet. You'll present your case in your own words, then answer questions. We've prepared a script, anticipated questions, and tips on how to present yourself in court specifically for your case.",
  },
  documents: {
    topic: 'Final document review',
    body: "Read each document carefully. We've drafted everything from your affirmed claims and evidence. But you're the one signing. If any fact reads wrong, edit it before you affirm. Once you affirm, the packet is locked and ready to file.",
  },
};

/* Per-evidence Atticus reads. Used in the affirmation gate */
const V3_EVIDENCE_READS = {
  'lease': {
    elements: ['tenancy', 'deposit-paid'],
    why: "This document supports two elements: the landlord-tenant relationship (Element 1) and that you paid a $1,800 deposit on April 1, 2024 (Element 2). The lease lists you as tenant and shows the deposit clause on page 3.",
  },
  'deposit-receipt': {
    elements: ['deposit-paid'],
    why: "This receipt shows the $1,800 you paid Westhaven on the move-in date. It's the cleanest proof of Element 2. That you actually paid a deposit.",
  },
  'notice': {
    elements: ['tenancy-ended'],
    why: "Your text on February 13, 2026 gave the landlord written 30-day notice you were moving out. The standard trigger that ends a month-to-month tenancy in Utah. Supports Element 3.",
  },
  'keys': {
    elements: ['tenancy-ended'],
    why: "The email confirms you returned the keys on March 15, 2026. Your last day in possession. This is what 'tenancy ended' means procedurally. Supports Element 3.",
  },
  'walkthrough': {
    elements: ['tenancy-ended'],
    why: "The 12 walkthrough photos with March 15, 2026 EXIF timestamps show the unit was clean and undamaged when you left. Supports Element 3 and pre-empts a counterclaim that you owed for damages.",
  },
  'demand': {
    elements: ['no-return'],
    why: "Your certified-mail demand on April 16, 2026. 32 days after the end of tenancy. Documents the moment you formally asked for the deposit back and got nothing. Supports Element 4 (the wrongful-retention element itself).",
  },
  'silence': {
    elements: ['no-return'],
    why: "30+ days have passed with no deposit and no written itemization from Westhaven. This is the heart of the § 57-17-3 claim. Element 4.",
  },
};

/* ================================ HEADER / NAV CHROME ================================ */

const V3_NAV = [
  { id: 'triage', label: 'Start a new case', icon: Icons.briefcase, kind: 'action' },
  { id: 'home', label: 'Home', icon: Icons.dashboard },
  { id: 'matter', label: 'My case', icon: Icons.folder },
  { id: 'theory', label: 'Legal theory', icon: Icons.scale },
  { id: 'evidence', label: 'Evidence', icon: Icons.paperclip },
  { id: 'timeline', label: 'Timeline', icon: Icons.clock },
  { id: 'damages', label: 'Damages', mIcon: 'payments' },
  { id: 'readiness', label: 'Case readiness', icon: Icons.check },
  { id: 'documents', label: 'Documents', icon: Icons.doc },
  { id: 'filing', label: 'Filing & service', icon: Icons.send },
  { id: 'courtroom', label: 'Courtroom prep', icon: Icons.book },
  { id: 'deliverables', label: 'Your packets', icon: Icons.doc },
  { id: 'audit', label: 'Decision log', icon: Icons.shield },
  { id: 'settings', label: 'Settings', icon: Icons.settings, subItems: [
    { id: 'profile', label: 'Profile', mIcon: 'account_circle' },
    { id: 'notifications', label: 'Notifications', mIcon: 'notifications' },
    { id: 'privacy', label: 'Privacy and security', mIcon: 'lock' },
    { id: 'about', label: 'About', mIcon: 'info' },
  ]},
];

const V3_NAV_TIER2 = [
  { id: 'triage', label: 'Start a new case', icon: Icons.briefcase, kind: 'action' },
  { id: 'home', label: 'Home', icon: Icons.dashboard },
  { id: 'matter', label: 'My case', icon: Icons.folder },
  { id: 'evidence', label: 'Evidence', icon: Icons.paperclip },
  { id: 'timeline', label: 'Timeline', icon: Icons.clock },
  { id: 'pitch', label: 'Lawyer-pitch packet', icon: Icons.doc },
  { id: 'audit', label: 'Decision log', icon: Icons.shield },
];

const V3Sidebar = ({ matter, screen, onScreen, onSwitchMatter, onTriggerCrisis, mode, onModeChange, onStartWalkthrough, walkthroughActive, forceCollapsed = false, emptyState = false }) => {
  const m = V3_MATTERS[matter];
  const items = m.tier === 2 ? V3_NAV_TIER2 : V3_NAV;
  // Story 0.64 — empty-state nav. Until the user has started any case
  // (matters.length === 0), every case-scoped nav item is shown in a
  // disabled visual state and the "Start a new case" action is the single
  // highlighted CTA. `emptyState` is the matters.length===0 signal lifted
  // from the parent. Once the first case exists it flips false and nav
  // unlocks normally. Items considered "case-scoped" are the non-action,
  // non-Settings entries (Settings is account-level, not case-bound).
  const isCaseScoped = (item) => item.kind !== 'action' && item.id !== 'settings';
  const [userCollapsed, setUserCollapsed] = React.useState(false);
  // forceCollapsed wins (set by parent when the chat guide panel expands).
  // The user-toggled state is preserved so the sidebar returns to whatever
  // expanded/collapsed mode the user had when the chat closes again.
  const collapsed = forceCollapsed || userCollapsed;
  const setCollapsed = setUserCollapsed;
  const [expandedSubNav, setExpandedSubNav] = React.useState(null);
  // Flyout position for sub-nav. Captured on click via getBoundingClientRect
  // so the flyout sits next to its trigger regardless of scroll position.
  const [subNavFlyout, setSubNavFlyout] = React.useState(null);
  const [hoverItem, setHoverItem] = React.useState(null);
  const [caseMenuOpen, setCaseMenuOpen] = React.useState(false);
  React.useEffect(() => { setCaseMenuOpen(false); }, [matter, collapsed]);
  // Collapse the sub-nav whenever the rail collapses; sub-nav doesn't
  // make sense in the icon-only rail.
  React.useEffect(() => { if (collapsed) { setExpandedSubNav(null); setSubNavFlyout(null); } }, [collapsed]);

  const isDark = THEME.mode === 'dark';
  // Home redesign 2026-06-05: floating glass rail widened to the approved
  // blueprint (280 expanded / 68 collapsed). The chat guide moved to the
  // bottom-right corner (no longer a flex member stealing width), so the rail
  // can breathe again. Styled hover tooltips name each page in the icon rail.
  const W_EXPANDED = 280;
  const W_COLLAPSED = 68;
  // Styled hover-tooltip pill for the collapsed icon rail (names each page).
  // Rendered inside each collapsed item (which is position:relative) when its
  // hoverItem key is active. Replaces the bare native `title` affordance.
  const railTip = (label, show) => show ? (
    <div style={{
      position: 'absolute', left: '100%', top: '50%',
      transform: 'translate(12px, -50%)', whiteSpace: 'nowrap',
      padding: '6px 11px', borderRadius: 8, zIndex: 200,
      background: isDark ? 'rgba(20,26,34,0.97)' : 'rgba(40,46,56,0.97)',
      color: '#F2F4F8', fontSize: 12.5, fontWeight: 600,
      border: `1px solid ${isDark ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.16)'}`,
      boxShadow: '0 8px 24px rgba(0,0,0,0.45)', pointerEvents: 'none',
    }}>{label}</div>
  ) : null;
  // Light-mode sidebar pulled less white on 2026-05-02. Opacity drop lets the
  // warmer (slightly darker) paper bg show through, matching dark-mode rhythm.
  const sidebarBg = isDark ? 'rgba(28,28,32,0.55)' : 'rgba(252,248,238,0.42)';
  const sidebarBorder = isDark ? 'rgba(255,255,255,0.12)' : 'rgba(20,20,30,0.10)';
  const sidebarShadow = `inset 0 1px 0 rgba(255,255,255,${isDark ? 0.08 : 0.6}), 0 16px 40px -16px rgba(0,0,0,${isDark ? 0.55 : 0.18})`;

  const tierTone = (mm) => mm.tier === 1 ? THEME.success : mm.tier === 2 ? THEME.warn : THEME.blue;
  const jxTone = (mm) => mm.jurisdiction === 'utah' ? THEME.success : THEME.blue;

  return (
    <div style={{
      // Outer 12px gutter. Sidebar floats inside the iPad chrome
      paddingTop: 12, paddingLeft: 12, paddingBottom: 12, paddingRight: 0,
      height: '100%', display: 'flex',
      flexShrink: 0,
    }}>
      <div style={{
        width: collapsed ? W_COLLAPSED : W_EXPANDED,
        transition: 'width 0.28s cubic-bezier(0.22, 1, 0.36, 1)',
        height: '100%',
        background: sidebarBg,
        WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
        backdropFilter: 'blur(28px) saturate(1.6)',
        border: `1px solid ${sidebarBorder}`,
        borderRadius: 16,
        boxShadow: sidebarShadow,
        display: 'flex', flexDirection: 'column',
        // overflow:visible so the collapsed-state toggle button can hang
        // half-off the right edge as a railroad-style affordance. Nav scroll
        // is contained by the nav child's own overflowY:auto below.
        overflow: 'visible',
        position: 'relative',
      }}>

      {/* Sub-nav flyout — modern designed panel, periwinkle accent, glass bg
          with subtle inner glow, staggered icon-led item entries, scale +
          slide entry. Portaled to document.body so it escapes the sidebar's
          scroll-container AND any iPad-scene transforms. */}
      {expandedSubNav && subNavFlyout && ReactDOM.createPortal(
        (() => {
          const allNavs = [...V3_NAV, ...V3_NAV_TIER2];
          const navItem = allNavs.find(n => n.id === expandedSubNav);
          if (!navItem || !navItem.subItems) return null;
          return (
            <>
              {/* Click-outside dismiss layer. */}
              <div
                onClick={() => { setExpandedSubNav(null); setSubNavFlyout(null); }}
                style={{ position: 'fixed', inset: 0, zIndex: 9998, background: 'transparent' }}
              />
              <div style={{
                position: 'fixed',
                top: subNavFlyout.top - 4,
                left: subNavFlyout.left,
                minWidth: 220,
                maxWidth: 260,
                padding: 8,
                // Brand-aware glass: dark mode has a periwinkle-tinted base
                // so the panel doesn't read as "system default dropdown."
                // Light mode pulls warm-paper.
                background: isDark
                  ? 'linear-gradient(180deg, rgba(36,46,68,0.96), rgba(26,34,52,0.96))'
                  : 'linear-gradient(180deg, rgba(255,253,247,0.98), rgba(248,243,231,0.98))',
                WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
                backdropFilter: 'blur(28px) saturate(1.6)',
                border: `1px solid ${isDark ? 'rgba(159,179,255,0.28)' : 'rgba(105,134,191,0.32)'}`,
                borderRadius: 12,
                boxShadow: isDark
                  ? '0 18px 48px rgba(0,0,0,0.60), 0 4px 12px rgba(0,0,0,0.35), inset 0 1px 0 rgba(159,179,255,0.10)'
                  : '0 18px 48px rgba(20,30,50,0.18), 0 4px 12px rgba(20,30,50,0.08), inset 0 1px 0 rgba(255,255,255,0.5)',
                zIndex: 9999,
                animation: 'sbFlyoutIn 0.26s cubic-bezier(0.22, 1, 0.36, 1)',
                transformOrigin: 'left center',
              }}>
                <style>{`
                  @keyframes sbFlyoutIn {
                    from { opacity: 0; transform: translateX(-10px) scale(0.96); }
                    to   { opacity: 1; transform: translateX(0) scale(1); }
                  }
                  @keyframes sbItemIn {
                    from { opacity: 0; transform: translateX(-6px); }
                    to   { opacity: 1; transform: translateX(0); }
                  }
                `}</style>
                {/* Eyebrow — small label so the panel reads as a designed
                    surface, not a system menu. */}
                <div style={{
                  fontSize: 9.5, letterSpacing: 1.2, textTransform: 'uppercase',
                  fontWeight: 700, color: isDark ? 'rgba(159,179,255,0.85)' : 'rgba(78,109,168,0.85)',
                  padding: '6px 10px 8px', marginBottom: 4,
                  borderBottom: `1px solid ${isDark ? 'rgba(159,179,255,0.14)' : 'rgba(105,134,191,0.16)'}`,
                }}>
                  {navItem.label}
                </div>
                {navItem.subItems.map((sub, idx) => {
                  const subActive = screen === sub.id;
                  return (
                    <div key={sub.id}
                      onClick={(e) => {
                        e.stopPropagation();
                        onScreen(sub.id);
                        setExpandedSubNav(null);
                        setSubNavFlyout(null);
                      }}
                      onMouseEnter={(e) => {
                        if (subActive) return;
                        e.currentTarget.style.background = isDark ? 'rgba(159,179,255,0.14)' : 'rgba(105,134,191,0.10)';
                        e.currentTarget.style.color = THEME.text;
                        const icon = e.currentTarget.querySelector('.material-symbols-rounded');
                        if (icon) icon.style.color = isDark ? '#9FB3FF' : '#4E6DA8';
                      }}
                      onMouseLeave={(e) => {
                        if (subActive) return;
                        e.currentTarget.style.background = 'transparent';
                        e.currentTarget.style.color = THEME.textDim;
                        const icon = e.currentTarget.querySelector('.material-symbols-rounded');
                        if (icon) icon.style.color = THEME.textDim;
                      }}
                      style={{
                        display: 'flex', alignItems: 'center', gap: 12,
                        padding: '10px 12px', borderRadius: 8, cursor: 'pointer',
                        background: subActive
                          ? (isDark ? 'rgba(159,179,255,0.18)' : 'rgba(105,134,191,0.14)')
                          : 'transparent',
                        color: subActive ? THEME.text : THEME.textDim,
                        fontSize: 12.5, fontWeight: subActive ? 600 : 500,
                        marginBottom: 2,
                        transition: 'background 0.18s ease, color 0.18s ease',
                        animation: `sbItemIn 0.22s cubic-bezier(0.22, 1, 0.36, 1) ${0.08 + idx * 0.045}s both`,
                      }}>
                      <span className="material-symbols-rounded" aria-hidden="true" style={{
                        fontSize: 18, lineHeight: 1, flexShrink: 0,
                        color: subActive ? (isDark ? '#9FB3FF' : '#4E6DA8') : THEME.textDim,
                        transition: 'color 0.18s ease',
                        fontVariationSettings: "'opsz' 24",
                      }}>{sub.mIcon}</span>
                      <span>{sub.label}</span>
                    </div>
                  );
                })}
              </div>
            </>
          );
        })(),
        document.body
      )}

      {/* Old settings overlay code path — kept as `false` guard, will delete on Phase 5 sweep. */}
      {false && (
        <div style={{
          position: 'absolute', inset: 0,
          background: sidebarBg,
          WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
          backdropFilter: 'blur(28px) saturate(1.6)',
          borderRadius: 16,
          display: 'flex', flexDirection: 'column',
          zIndex: 4,
          animation: 'sbSettingsFade 0.28s cubic-bezier(0.22, 1, 0.36, 1)',
        }}>
          <style>{`@keyframes sbSettingsFade { from { opacity: 0; } to { opacity: 1; } }`}</style>
          {/* Settings header — title + close. */}
          <div style={{
            padding: '24px 16px 12px',
            display: 'flex', alignItems: 'center', justifyContent: 'space-between',
            borderBottom: `1px solid ${sidebarBorder}`,
          }}>
            <div style={{ fontFamily: 'Fraunces', fontSize: 20, fontWeight: 500, letterSpacing: -0.3, color: THEME.text }}>
              Settings
            </div>
            <button
              type="button"
              onClick={() => setSettingsOpen(false)}
              aria-label="Close settings"
              style={{
                width: 28, height: 28, borderRadius: 14,
                background: 'transparent', border: `1px solid ${THEME.line}`,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                cursor: 'pointer', color: THEME.textDim,
                transition: 'background 0.15s ease, color 0.15s ease',
              }}
              onMouseEnter={(e) => { e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.04)'; e.currentTarget.style.color = THEME.text; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = THEME.textDim; }}
            >
              <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 16, lineHeight: 1, fontVariationSettings: "'opsz' 24" }}>close</span>
            </button>
          </div>

          {/* Settings body. */}
          <div style={{ padding: '8px 10px', flex: 1, overflowY: 'auto' }}>
            <div style={{ fontSize: 9.5, color: THEME.textMute, letterSpacing: 1.2, textTransform: 'uppercase', fontWeight: 600, marginBottom: 6, paddingLeft: 4, marginTop: 6 }}>Account</div>
            {[
              { icon: 'account_circle', label: 'Profile' },
              { icon: 'notifications', label: 'Notifications' },
              { icon: 'lock', label: 'Privacy and security' },
            ].map(item => (
              <div key={item.label} style={{
                display: 'flex', alignItems: 'center', gap: 9,
                padding: '8px 10px', borderRadius: 7, cursor: 'pointer',
                color: THEME.textDim, fontSize: 12.5, fontWeight: 450,
                marginBottom: 1,
                transition: 'background 0.15s ease, color 0.15s ease',
              }}
              onMouseEnter={(e) => { e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.04)'; e.currentTarget.style.color = THEME.text; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = THEME.textDim; }}
              >
                <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 16, lineHeight: 1, fontVariationSettings: "'opsz' 24" }}>{item.icon}</span>
                <span>{item.label}</span>
              </div>
            ))}

            <div style={{ fontSize: 9.5, color: THEME.textMute, letterSpacing: 1.2, textTransform: 'uppercase', fontWeight: 600, marginBottom: 6, paddingLeft: 4, marginTop: 18 }}>Support</div>
            {[
              { icon: 'help_outline', label: 'Help center' },
              { icon: 'feedback', label: 'Send feedback' },
              { icon: 'info', label: 'About' },
            ].map(item => (
              <div key={item.label} style={{
                display: 'flex', alignItems: 'center', gap: 9,
                padding: '8px 10px', borderRadius: 7, cursor: 'pointer',
                color: THEME.textDim, fontSize: 12.5, fontWeight: 450,
                marginBottom: 1,
                transition: 'background 0.15s ease, color 0.15s ease',
              }}
              onMouseEnter={(e) => { e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.04)'; e.currentTarget.style.color = THEME.text; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = THEME.textDim; }}
              >
                <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 16, lineHeight: 1, fontVariationSettings: "'opsz' 24" }}>{item.icon}</span>
                <span>{item.label}</span>
              </div>
            ))}
          </div>

          {/* Settings footer — sign out. */}
          <div style={{ padding: '10px 14px 14px', borderTop: `1px solid ${sidebarBorder}` }}>
            <button
              type="button"
              style={{
                width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
                padding: '8px 10px', borderRadius: 8,
                background: 'transparent', border: `1px solid ${THEME.line}`,
                color: THEME.textDim, fontSize: 12, fontWeight: 500,
                fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
                cursor: 'pointer',
                transition: 'background 0.15s ease, color 0.15s ease',
              }}
              onMouseEnter={(e) => { e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.04)'; e.currentTarget.style.color = THEME.text; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = THEME.textDim; }}
            >
              <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 14, lineHeight: 1, fontVariationSettings: "'opsz' 24" }}>power_settings_new</span>
              <span>Sign out</span>
            </button>
          </div>
        </div>
      )}
        {/* Header. Logo + collapse toggle. Symmetric vertical padding when
           collapsed so the logo's vertical center matches the header's
           midpoint, letting the absolute-positioned toggle line up cleanly. */}
        <div style={{
          padding: collapsed ? '16px 0 8px' : '24px 16px 12px',
          display: 'flex',
          flexDirection: collapsed ? 'column' : 'row',
          alignItems: 'center',
          justifyContent: collapsed ? 'center' : 'space-between',
          gap: collapsed ? 12 : 8, position: 'relative',
        }}>
          {collapsed ? (
            <AllLawLockupSymbol size={28}/>
          ) : (
            <AllLawLockup scale={0.95}/>
          )}
          <div onClick={() => setCollapsed(c => !c)} style={{
            // Collapsed: the expand toggle sits in normal flow directly below
            // the logo (12px gap via the header column) and 12px above the
            // Start a new case CTA (header padding-bottom 8 + nav padding-top 4).
            position: 'static',
            // WCAG 2.2 SC 2.5.8 — interactive target minimum 24×24. Was 22×22.
            width: 24, height: 24, borderRadius: 12,
            // Dark mode: dark gray-blue solid so the toggle reads as a
            // clearly tappable affordance without competing with the
            // periwinkle accent palette. Light mode stays cream. 100%
            // opacity, soft drop shadow.
            background: isDark ? '#3A4A5F' : '#FCF8EE',
            border: 'none',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            cursor: 'pointer', flexShrink: 0,
            transition: 'background 0.15s ease, box-shadow 0.15s ease',
            zIndex: 5,
            boxShadow: isDark
              ? '0 2px 8px rgba(0,0,0,0.45), 0 1px 2px rgba(0,0,0,0.30)'
              : '0 2px 8px rgba(20,30,50,0.18), 0 1px 2px rgba(20,30,50,0.10)',
          }}
          onMouseEnter={(e) => e.currentTarget.style.background = isDark ? '#48586D' : '#F2EEE0'}
          onMouseLeave={(e) => e.currentTarget.style.background = isDark ? '#3A4A5F' : '#FCF8EE'}
          >
            <div style={{ transform: collapsed ? 'rotate(0deg)' : 'rotate(180deg)', transition: 'transform 0.28s cubic-bezier(0.22, 1, 0.36, 1)', display: 'flex' }}>
              <Icon d={Icons.chevronRight} size={11} stroke={THEME.warn} sw={2.4}/>
            </div>
          </div>
        </div>


        {/* Nav */}
        <div style={{ padding: collapsed ? '4px 8px' : '4px 10px', flex: 1, overflowY: 'auto', minHeight: 0 }}>
          {/* "Demo home" / "Product info page" back link relocated to the
              sidebar FOOTER in the 2026-06-05 home redesign (blueprint puts it
              there, above "Take the walkthrough"). See the footer block below. */}
          {/* Action items (e.g., Start a new case) render BEFORE the per-case
              nav, with breathing room below. Styled to match the existing
              "Take the walkthrough" sidebar button — same subtle periwinkle
              tint, same fontWeight, same icon treatment, on-brand. */}
          {/* Story 0.64 — keyframes for the empty-state CTA highlight: a
              gentle breathing glow that draws the eye to "Start a new case"
              without being garish. Periwinkle-tinted to stay on-brand.
              KINSLEY REVIEW: exact glow/pulse treatment is a design call. */}
          {emptyState && (
            <style>{`
              @keyframes sbCtaPulse {
                0%, 100% { box-shadow: 0 0 0 1px ${isDark ? 'rgba(159,179,255,0.55)' : 'rgba(105,134,191,0.55)'}, 0 0 0 0 ${isDark ? 'rgba(159,179,255,0.0)' : 'rgba(105,134,191,0.0)'}; }
                50%      { box-shadow: 0 0 0 1px ${isDark ? 'rgba(159,179,255,0.85)' : 'rgba(105,134,191,0.85)'}, 0 0 14px 2px ${isDark ? 'rgba(159,179,255,0.35)' : 'rgba(105,134,191,0.30)'}; }
              }
            `}</style>
          )}
          {items.filter(i => i.kind === 'action').map(item => (
            collapsed ? (
              <div key={item.id} onClick={() => onScreen(item.id)}
                data-coach={`sidebar-${item.id}`}
                onMouseEnter={() => setHoverItem('nav:' + item.id)}
                onMouseLeave={() => setHoverItem(null)}
                title={item.label}
                style={{
                  position: 'relative',
                  width: 40, height: 40, margin: '0 auto 18px', borderRadius: 8,
                  cursor: 'pointer',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  background: emptyState
                    ? (isDark ? 'rgba(159,179,255,0.22)' : 'rgba(105,134,191,0.18)')
                    : (isDark ? 'rgba(159,179,255,0.10)' : 'rgba(105,134,191,0.08)'),
                  border: `1px solid ${isDark ? 'rgba(159,179,255,0.28)' : 'rgba(105,134,191,0.30)'}`,
                  transition: 'background 0.15s ease',
                  animation: emptyState ? 'sbCtaPulse 2.4s ease-in-out infinite' : undefined,
                }}>
                <Icon d={item.icon} name={item.mIcon} size={14} stroke={THEME.blue} sw={2}/>
                {railTip(item.label, hoverItem === 'nav:' + item.id)}
              </div>
            ) : (
              <div key={item.id} onClick={() => onScreen(item.id)}
                data-coach={`sidebar-${item.id}`}
                onMouseEnter={(e) => { e.currentTarget.style.background = isDark ? 'rgba(159,179,255,0.18)' : 'rgba(105,134,191,0.14)'; }}
                onMouseLeave={(e) => { e.currentTarget.style.background = emptyState ? (isDark ? 'rgba(159,179,255,0.22)' : 'rgba(105,134,191,0.18)') : (isDark ? 'rgba(159,179,255,0.10)' : 'rgba(105,134,191,0.08)'); }}
                style={{
                  display: 'flex', alignItems: 'center', gap: 8,
                  padding: '8px 10px', borderRadius: 8, cursor: 'pointer',
                  background: emptyState
                    ? (isDark ? 'rgba(159,179,255,0.22)' : 'rgba(105,134,191,0.18)')
                    : (isDark ? 'rgba(159,179,255,0.10)' : 'rgba(105,134,191,0.08)'),
                  border: `1px solid ${isDark ? 'rgba(159,179,255,0.28)' : 'rgba(105,134,191,0.30)'}`,
                  color: THEME.text,
                  fontSize: 11.5, fontWeight: emptyState ? 600 : 500,
                  marginBottom: 18,
                  transition: 'background 0.15s ease, border-color 0.15s ease',
                  animation: emptyState ? 'sbCtaPulse 2.4s ease-in-out infinite' : undefined,
                }}>
                <Icon d={item.icon} name={item.mIcon} size={12} stroke={THEME.blue} sw={2}/>
                <span style={{ flex: 1 }}>{item.label}</span>
              </div>
            )
          ))}
          {!collapsed && (
            <div style={{ fontSize: 9.5, color: THEME.textMute, letterSpacing: 1.2, textTransform: 'uppercase', fontWeight: 600, marginBottom: 6, paddingLeft: 4 }}>{emptyState ? 'Your case' : 'This case'}</div>
          )}
          {items.filter(i => i.kind !== 'action').map(item => {
            const active = screen === item.id;
            const isSubNav = !!item.subItems;
            const isExpanded = expandedSubNav === item.id;
            // Story 0.64 — case-scoped items are locked until the first case
            // exists. Disabled = dimmed (~0.35), not-allowed cursor, no-op click.
            const disabled = emptyState && isCaseScoped(item);

            // ── Sub-nav items get the right-flyout treatment in BOTH
            // collapsed and expanded modes. Checked FIRST so the regular
            // collapsed handler below doesn't swallow these clicks. ──
            if (isSubNav) {
              const handleSubNavClick = (e) => {
                if (isExpanded) {
                  setExpandedSubNav(null);
                  setSubNavFlyout(null);
                } else {
                  const rect = e.currentTarget.getBoundingClientRect();
                  setSubNavFlyout({ id: item.id, top: rect.top, left: rect.right + 8 });
                  setExpandedSubNav(item.id);
                }
              };
              if (collapsed) {
                const isHovered = hoverItem === 'nav:' + item.id;
                const iconStroke = isExpanded
                  ? THEME.blue
                  : (isHovered ? THEME.text : THEME.textDim);
                return (
                  <div key={item.id} onClick={handleSubNavClick}
                    data-coach={`sidebar-${item.id}`}
                    onMouseEnter={() => setHoverItem('nav:' + item.id)}
                    onMouseLeave={() => setHoverItem(null)}
                    title={item.label}
                    aria-label={item.label}
                    role="button"
                    style={{
                      position: 'relative',
                      width: 40, height: 40, margin: '0 auto 4px', borderRadius: 10,
                      cursor: 'pointer',
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      background: isExpanded ? THEME.blueSoft : 'transparent',
                      transition: 'background 0.15s ease',
                    }}>
                    <div style={{
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      transform: isHovered && !isExpanded ? 'scale(1.18)' : 'scale(1)',
                      transition: 'transform 0.18s cubic-bezier(0.22, 1, 0.36, 1)',
                    }}>
                      <Icon d={item.icon} name={item.mIcon} size={16} stroke={iconStroke} sw={2}/>
                    </div>
                    {railTip(item.label, isHovered)}
                  </div>
                );
              }
              // Expanded sub-nav
              return (
                <div key={item.id} onClick={handleSubNavClick}
                  data-coach={`sidebar-${item.id}`}
                  onMouseEnter={(e) => { if (!isExpanded) { e.currentTarget.style.color = THEME.text; e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(20,30,50,0.05)'; } }}
                  onMouseLeave={(e) => { if (!isExpanded) { e.currentTarget.style.color = THEME.textDim; e.currentTarget.style.background = 'transparent'; } }}
                  style={{
                    position: 'relative',
                    display: 'flex', alignItems: 'center', gap: 9,
                    padding: '8px 10px 8px 14px', borderRadius: 7, cursor: 'pointer',
                    background: isExpanded ? THEME.blueSoft : 'transparent',
                    color: isExpanded ? THEME.text : THEME.textDim,
                    fontSize: 12.5, fontWeight: isExpanded ? 600 : 450,
                    marginTop: 4, marginBottom: 4,
                    transition: 'background 0.22s ease, color 0.22s ease',
                  }}>
                  <Icon d={item.icon} name={item.mIcon} size={14} stroke={isExpanded ? THEME.blue : THEME.textDim}/>
                  <span>{item.label}</span>
                </div>
              );
            }

            // ── Regular nav item (no sub-items) ── always case-scoped here.
            if (collapsed) {
              const isHovered = !disabled && hoverItem === 'nav:' + item.id;
              const iconStroke = active
                ? THEME.blue
                : (isHovered ? THEME.text : THEME.textDim);
              return (
                <div key={item.id} onClick={() => { if (disabled) return; onScreen(item.id); }}
                  data-coach={`sidebar-${item.id}`}
                  aria-disabled={disabled || undefined}
                  onMouseEnter={() => { if (!disabled) setHoverItem('nav:' + item.id); }}
                  onMouseLeave={() => setHoverItem(null)}
                  title={item.label}
                  aria-label={item.label}
                  role="button"
                  style={{
                    position: 'relative',
                    width: 40, height: 40, margin: '0 auto 4px', borderRadius: 10,
                    cursor: disabled ? 'not-allowed' : 'pointer',
                    opacity: disabled ? 0.35 : 1,
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    background: active ? THEME.blueSoft : 'transparent',
                    transition: 'background 0.15s ease, opacity 0.15s ease',
                  }}>
                  <div style={{
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    transform: isHovered && !active ? 'scale(1.18)' : 'scale(1)',
                    transition: 'transform 0.18s cubic-bezier(0.22, 1, 0.36, 1)',
                  }}>
                    <Icon d={item.icon} name={item.mIcon} size={16} stroke={iconStroke} sw={2}/>
                  </div>
                  {railTip(item.label, isHovered)}
                </div>
              );
            }
            const handleClick = () => { if (disabled) return; onScreen(item.id); };
            return (
              <div key={item.id} onClick={handleClick}
                data-coach={`sidebar-${item.id}`}
                aria-disabled={disabled || undefined}
                onMouseEnter={(e) => { if (!active && !disabled) { e.currentTarget.style.color = THEME.text; e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(20,30,50,0.05)'; } }}
                onMouseLeave={(e) => { if (!active && !disabled) { e.currentTarget.style.color = THEME.textDim; e.currentTarget.style.background = 'transparent'; } }}
                style={{
                position: 'relative',
                display: 'flex', alignItems: 'center', gap: 9,
                padding: '8px 10px 8px 14px', borderRadius: 7,
                cursor: disabled ? 'not-allowed' : 'pointer',
                opacity: disabled ? 0.35 : 1,
                background: active ? THEME.blueSoft : 'transparent',
                color: active ? THEME.text : THEME.textDim,
                fontSize: 12.5, fontWeight: active ? 600 : 450,
                marginTop: 4, marginBottom: 4,
                transition: 'background 0.22s ease, color 0.22s ease, font-weight 0.22s ease, opacity 0.15s ease',
              }}>
                {/* Active indicator — inset bar with scale-in animation. */}
                <span aria-hidden="true" style={{
                  position: 'absolute',
                  top: 2, bottom: 2, left: 1,
                  width: 4,
                  borderRadius: '6px 0 0 6px',
                  background: THEME.blue,
                  opacity: (active && !disabled) ? 1 : 0,
                  transform: active ? 'scaleY(1)' : 'scaleY(0.4)',
                  transformOrigin: 'center',
                  transition: 'opacity 0.22s ease, transform 0.22s cubic-bezier(0.22, 1, 0.36, 1)',
                }}/>
                <Icon d={item.icon} name={item.mIcon} size={14} stroke={active ? THEME.blue : THEME.textDim}/>
                <span>{item.label}</span>
              </div>
            );
          })}
        </div>

        {/* Product info page: back link to the partner-demo PRODUCT landing
            page (the gate + pitch surface). Relocated to the footer in the
            2026-06-05 home redesign (was "Demo home" at the top of the nav).
            ./index.html resolves to partner-demo/index.html in local + Vercel. */}
        {screen !== 'welcome' && (
          <div style={{ padding: collapsed ? '6px 10px 2px' : '6px 14px 2px' }}>
            <div onClick={() => { window.location.href = 'index.html'; }}
              onMouseEnter={(e) => { setHoverItem('foot:product'); e.currentTarget.style.background = THEME.blueSoft; e.currentTarget.style.color = THEME.blue; }}
              onMouseLeave={(e) => { setHoverItem(null); e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = THEME.textDim; }}
              title="Product info page"
              style={{
                position: 'relative', cursor: 'pointer',
                display: 'flex', alignItems: 'center',
                gap: collapsed ? 0 : 8,
                justifyContent: collapsed ? 'center' : 'flex-start',
                padding: collapsed ? '8px 0' : '8px 10px',
                margin: collapsed ? '0 auto' : 0,
                width: collapsed ? 40 : 'auto', height: collapsed ? 40 : 'auto',
                borderRadius: 8, background: 'transparent',
                border: `1px dashed ${THEME.line}`,
                fontSize: 11.5, color: THEME.textDim, fontWeight: 600, letterSpacing: 0.4,
                transition: 'background 0.15s ease, color 0.15s ease',
              }}>
              <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 16, lineHeight: 1, fontVariationSettings: "'opsz' 24" }}>home</span>
              {!collapsed && <span>Product info page</span>}
              {collapsed && railTip('Product info page', hoverItem === 'foot:product')}
            </div>
          </div>
        )}

        {/* Re-enter walkthrough — visible if a walkthrough exists for this
            matter and a starter has been wired. Lets a partner restart the
            tour after they Skip / Esc / scrim-click out of it. Reads from
            window.V3_WALKTHROUGHS so any matter with a registered walkthrough
            (Story 0.19 Sarah, 0.20 Daniel, future) auto-surfaces the entry
            without code changes here (audit 0.40.5). */}
        {!collapsed && typeof onStartWalkthrough === 'function' && typeof window !== 'undefined' && window.V3_WALKTHROUGHS && window.V3_WALKTHROUGHS[matter] && (
          <div style={{ padding: '4px 14px 6px' }}>
            <button
              type="button"
              onClick={onStartWalkthrough}
              style={{
                width: '100%', display: 'flex', alignItems: 'center', gap: 8,
                padding: '8px 10px', borderRadius: 8,
                background: isDark ? 'rgba(159,179,255,0.10)' : 'rgba(105,134,191,0.08)',
                border: `1px solid ${isDark ? 'rgba(159,179,255,0.28)' : 'rgba(105,134,191,0.30)'}`,
                color: THEME.text, fontSize: 11.5, fontWeight: 500,
                fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
                cursor: 'pointer', textAlign: 'left',
                transition: 'background 0.15s ease, border-color 0.15s ease',
              }}
              onMouseEnter={(e) => {
                e.currentTarget.style.background = isDark ? 'rgba(159,179,255,0.18)' : 'rgba(105,134,191,0.14)';
              }}
              onMouseLeave={(e) => {
                e.currentTarget.style.background = isDark ? 'rgba(159,179,255,0.10)' : 'rgba(105,134,191,0.08)';
              }}
            >
              <Icon d={Icons.dashboard} size={12} stroke={THEME.blue} sw={2}/>
              <span style={{ flex: 1 }}>{walkthroughActive ? 'Restart walkthrough' : 'Take the walkthrough'}</span>
            </button>
          </div>
        )}

        {/* Avatar + name + settings gear. Click anywhere on this row to open
            Settings; the gear is the explicit affordance. Routes via onScreen
            (the old setSettingsOpen handler referenced an undefined setter from
            the dead overlay path and threw on click; fixed 2026-06-05). Hidden
            when collapsed (the icon rail keeps its own structure). */}
        {!collapsed && (
          <div style={{ padding: '4px 14px 8px' }}>
            <div
              onClick={() => onScreen('settings')}
              style={{
                display: 'flex', alignItems: 'center', gap: 10,
                padding: '8px 10px', borderRadius: 9,
                background: 'transparent',
                border: `1px solid ${THEME.line}`,
                cursor: 'pointer',
                transition: 'background 0.15s ease, border-color 0.15s ease',
              }}
              onMouseEnter={(e) => { e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.04)'; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
            >
              <div style={{
                width: 28, height: 28, borderRadius: 14, flexShrink: 0,
                background: `linear-gradient(135deg, ${THEME.blue}, ${THEME.accent})`,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                color: '#D7DEE2', fontWeight: 600, fontSize: 11.5,
              }}>
                {(m.client && m.client.initials) || 'M'}
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 12, color: THEME.text, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  {(m.client && m.client.name) || 'Client'}
                </div>
                <div style={{ fontSize: 10.5, color: THEME.textMute, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  Open settings
                </div>
              </div>
              <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 16, lineHeight: 1, color: THEME.textDim, fontVariationSettings: "'opsz' 24" }}>settings</span>
            </div>
          </div>
        )}

        {/* Theme toggle removed from the sidebar in the 2026-06-05 home
            redesign. Light/dark now lives in the floating glass top-nav
            (single tap-to-flip). The mode / onModeChange props are kept on the
            signature (still passed by App) but are no longer rendered here. */}

        {/* Phase 3 marketplace preview. Hidden when collapsed */}
        {!collapsed && (
          <div style={{ padding: '4px 14px 14px' }}>
            <div style={{
              padding: '8px 10px', borderRadius: 7, background: 'transparent',
              border: `1px dashed ${THEME.line}`, fontSize: 10.5, color: THEME.textMute, lineHeight: 1.4,
            }}>
              <div style={{ fontWeight: 600, color: THEME.textDim, marginBottom: 2, fontSize: 10 }}>Phase 3 · Coming Q2 2027</div>
              Attorney marketplace · Reviewing & operating attorneys · Partner firm portal
            </div>
          </div>
        )}

        {/* Planning artifacts — intentionally non-prominent. Routes to the
            BMad planning index (PRD, epics, journeys, sprint status). Hidden
            when sidebar is collapsed. URL `planning/` is relative to demo.html
            and resolves to partner-demo/planning/ in both dev and prod
            (the export pipeline syncs planning content there for Vercel). */}
        {!collapsed && (
          <div style={{ padding: '0 14px 14px' }}>
            <a
              href="planning/"
              title="Planning artifacts: PRD, epics, journeys, sprint status"
              style={{
                display: 'inline-flex', alignItems: 'center', gap: 5,
                fontSize: 9.5, letterSpacing: 0.6,
                color: THEME.textMute, textDecoration: 'none',
                fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
                opacity: 0.55, transition: 'opacity 0.15s ease',
              }}
              onMouseEnter={(e) => { e.currentTarget.style.opacity = '1'; }}
              onMouseLeave={(e) => { e.currentTarget.style.opacity = '0.55'; }}
            >
              <span>Planning artifacts</span>
              <span style={{ fontSize: 8, lineHeight: 1 }}>↗</span>
            </a>
          </div>
        )}
      </div>
    </div>
  );
};

/* Floating active-case switcher. Sits in the top-right of the iPad inner screen.
   Single pill (active case) opens a dropdown listing all matters. */
const V3MatterSwitcher = ({ matter, onSwitchMatter }) => {
  const [open, setOpen] = React.useState(false);
  React.useEffect(() => { setOpen(false); }, [matter]);
  const m = V3_MATTERS[matter];
  if (!m) return null;
  const isDark = THEME.mode === 'dark';
  const tone = m.tier === 1 ? THEME.success : m.tier === 2 ? THEME.warn : THEME.blue;
  const jTone = m.jurisdiction === 'utah' ? THEME.success : THEME.blue;
  const border = isDark ? 'rgba(255,255,255,0.10)' : 'rgba(20,20,30,0.10)';

  return (
    <div style={{
      /* right: 84 (was 18) leaves room for the chat-guide circle FAB at
         right:18 of the flex container so the pill and FAB sit side-by-side
         at the same vertical line instead of stacking on top of each other. */
      position: 'absolute', top: 36, right: 84, zIndex: 40,
    }}>
      {/* Trigger pill */}
      <div onClick={() => setOpen(o => !o)} style={{
        padding: '8px 12px', borderRadius: 999, cursor: 'pointer',
        background: isDark ? 'rgba(28,28,32,0.55)' : 'rgba(255,255,255,0.55)',
        backdropFilter: 'blur(28px) saturate(1.6)',
        WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
        border: `1px solid ${open ? THEME.blue + '60' : border}`,
        boxShadow: isDark
          ? '0 8px 24px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.08)'
          : '0 8px 24px rgba(20,30,50,0.14), inset 0 1px 0 rgba(255,255,255,0.6)',
        display: 'flex', alignItems: 'center', gap: 8,
        transition: 'border-color 0.15s ease',
      }}>
        <span style={{ fontSize: 9, fontWeight: 700, padding: '2px 6px', borderRadius: 3, background: tone + '22', color: tone, letterSpacing: 0.5 }}>
          TIER {m.tier}
        </span>
        <span style={{ fontSize: 9, fontWeight: 700, padding: '2px 6px', borderRadius: 3, background: m.jurisdiction === 'utah' ? 'rgba(91,167,115,0.18)' : THEME.blueSoft, color: jTone, letterSpacing: 0.4 }}>
          {m.jurisdiction === 'utah' ? 'UT' : 'NY'}
        </span>
        <span style={{ fontSize: 12, fontWeight: 600, color: THEME.text, letterSpacing: -0.1, maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {m.short}
        </span>
        <div style={{ transform: open ? 'rotate(270deg)' : 'rotate(90deg)', transition: 'transform 0.18s cubic-bezier(0.22, 1, 0.36, 1)', display: 'flex' }}>
          <Icon d={Icons.chevronRight} size={11} stroke={THEME.warn} sw={2.4}/>
        </div>
      </div>

      {/* Dropdown menu */}
      {open && (
        <>
          <div onClick={() => setOpen(false)} style={{
            position: 'fixed', inset: 0, zIndex: 39,
          }}/>
          <div style={{
            position: 'absolute', top: 'calc(100% + 6px)', right: 0, zIndex: 41,
            minWidth: 280,
            padding: 6, borderRadius: 12,
            background: isDark ? 'rgba(22,28,36,0.96)' : 'rgba(255,255,255,0.98)',
            backdropFilter: 'blur(28px) saturate(1.6)',
            WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
            border: `1px solid ${border}`,
            boxShadow: isDark
              ? '0 16px 40px rgba(0,0,0,0.55), 0 4px 12px rgba(0,0,0,0.3)'
              : '0 16px 40px rgba(20,30,50,0.18), 0 4px 12px rgba(20,30,50,0.08)',
          }}>
            <div style={{ fontSize: 9.5, color: THEME.textMute, letterSpacing: 1.2, textTransform: 'uppercase', fontWeight: 600, padding: '6px 8px 4px' }}>
              Switch case · {Object.keys(V3_MATTERS).length} matters
            </div>
            {Object.values(V3_MATTERS).map(mm => {
              const a = mm.id === matter;
              const t = mm.tier === 1 ? THEME.success : mm.tier === 2 ? THEME.warn : THEME.blue;
              const jt = mm.jurisdiction === 'utah' ? THEME.success : THEME.blue;
              return (
                <div key={mm.id} onClick={() => { onSwitchMatter(mm.id); setOpen(false); }} style={{
                  padding: '8px 10px', borderRadius: 8, cursor: 'pointer', marginBottom: 1,
                  background: a ? THEME.blueSoft : 'transparent',
                  border: `1px solid ${a ? THEME.blue + '40' : 'transparent'}`,
                  transition: 'background 0.12s ease',
                }}
                  onMouseEnter={(e) => !a && (e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.03)')}
                  onMouseLeave={(e) => !a && (e.currentTarget.style.background = 'transparent')}
                >
                  <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
                    <span style={{ fontSize: 9, fontWeight: 700, padding: '2px 6px', borderRadius: 3, background: t + '22', color: t, letterSpacing: 0.5 }}>
                      TIER {mm.tier}
                    </span>
                    <span style={{ fontSize: 9, fontWeight: 700, padding: '2px 6px', borderRadius: 3, background: mm.jurisdiction === 'utah' ? 'rgba(91,167,115,0.18)' : THEME.blueSoft, color: jt, letterSpacing: 0.4 }}>
                      {mm.jurisdiction === 'utah' ? 'UT' : 'NY'}
                    </span>
                    {a && <span style={{ fontSize: 8.5, fontWeight: 700, color: THEME.blue, letterSpacing: 0.6, marginLeft: 'auto' }}>● ACTIVE</span>}
                  </div>
                  <div style={{ fontSize: 12, color: a ? THEME.text : THEME.textDim, fontWeight: a ? 600 : 500, lineHeight: 1.3, marginBottom: 2 }}>
                    {mm.short}
                  </div>
                  <div style={{ fontSize: 10.5, color: THEME.textMute, lineHeight: 1.35 }}>
                    {mm.type}
                  </div>
                </div>
              );
            })}
          </div>
        </>
      )}
    </div>
  );
};

/* ── Floating liquid-glass top-right nav (home redesign 2026-06-05) ──────────
   Replaces the standalone V3MatterSwitcher pill AND the old in-sidebar theme
   toggle. Houses, left to right: case switcher (dropdown), a divider, updates
   bell, light/dark tap-to-flip, settings gear, a divider, profile (dropdown).
   Mounted by App in the client main column at top:28 right:36.
   Wired actions: case switch, "Start a new case" (triage), theme flip. The
   secondary menu rows (View all matters, profile links, sign out, updates,
   settings) are visual in the demo and just close the menu, matching the
   approved mockup's fidelity and the app's existing settings behavior.
   Per-mode styling is inline (THEME.mode), not a .light CSS class. No arrows
   in buttons; chevrons (rotated) only on the clickable pills. No em dashes. */
const V3GlassNav = ({ matter, onSwitchMatter, mode, onModeChange, emptyState = false, onScreen }) => {
  const [menu, setMenu] = React.useState(null); // null | 'case' | 'profile'
  React.useEffect(() => { setMenu(null); }, [matter]);
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') setMenu(null); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);
  const m = V3_MATTERS[matter];
  if (!m) return null;
  const isDark = THEME.mode === 'dark';

  const tone = (mm) => mm.tier === 1 ? THEME.success : mm.tier === 2 ? THEME.warn : THEME.blue;
  const jTone = (mm) => mm.jurisdiction === 'utah' ? THEME.success : THEME.blue;
  const jLabel = (mm) => mm.jurisdiction === 'utah' ? 'UT' : 'NY';
  const jBg = (mm) => mm.jurisdiction === 'utah' ? 'rgba(91,167,115,0.18)' : THEME.blueSoft;

  const text = THEME.text, dim = THEME.textDim, mute = THEME.textMute;
  const glassBd = isDark ? 'rgba(255,255,255,0.12)' : 'rgba(20,30,50,0.12)';
  const navBg = isDark
    ? 'linear-gradient(155deg, rgba(58,72,86,0.50) 0%, rgba(33,45,57,0.42) 100%)'
    : 'linear-gradient(155deg, rgba(255,255,255,0.62) 0%, rgba(255,255,255,0.50) 100%)';
  const navShadow = isDark
    ? '0 18px 50px rgba(0,0,0,0.42), 0 2px 8px rgba(0,0,0,0.30), inset 0 1px 0 rgba(255,255,255,0.16), inset 0 -1px 0 rgba(0,0,0,0.18)'
    : '0 18px 50px rgba(20,30,50,0.16), inset 0 1px 0 rgba(255,255,255,0.70)';
  const ddBg = isDark
    ? 'linear-gradient(160deg, rgba(48,62,76,0.92) 0%, rgba(30,41,52,0.90) 100%)'
    : 'linear-gradient(160deg, rgba(255,253,247,0.98) 0%, rgba(244,239,227,0.98) 100%)';
  const ddShadow = isDark
    ? '0 26px 64px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.14)'
    : '0 26px 64px rgba(20,30,50,0.22), inset 0 1px 0 rgba(255,255,255,0.70)';
  const hoverBg = isDark ? 'rgba(255,255,255,0.07)' : 'rgba(20,30,50,0.06)';
  const rowHover = isDark ? 'rgba(255,255,255,0.11)' : 'rgba(20,30,50,0.07)';

  const MI = ({ name, size = 18, color }) => (
    <span className="material-symbols-rounded" aria-hidden="true"
      style={{ fontSize: size, lineHeight: 1, color, fontVariationSettings: "'opsz' 24" }}>{name}</span>
  );
  const Badge = ({ children, bg, color }) => (
    <span style={{ fontSize: 9, fontWeight: 700, padding: '2px 6px', borderRadius: 4, background: bg, color, letterSpacing: 0.4 }}>{children}</span>
  );
  const Chevron = ({ open }) => (
    <span style={{ display: 'inline-flex', marginLeft: 2, transform: open ? 'rotate(270deg)' : 'rotate(90deg)', transition: 'transform 0.18s cubic-bezier(0.22,1,0.36,1)' }}>
      <Icon d={Icons.chevronRight} size={10} stroke={THEME.warn} sw={2.4}/>
    </span>
  );

  const iconBtn = (key, name, opts = {}) => (
    <button type="button" title={opts.title} aria-label={opts.title}
      onClick={(e) => { e.stopPropagation(); (opts.onClick || (() => setMenu(null)))(); }}
      onMouseEnter={(e) => { e.currentTarget.style.background = hoverBg; e.currentTarget.style.color = text; }}
      onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = dim; }}
      style={{
        position: 'relative', height: 36, width: 36, border: 0, background: 'transparent',
        color: opts.color || dim, borderRadius: 999, cursor: 'pointer',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        fontFamily: 'inherit', transition: 'background 0.18s ease, color 0.18s ease',
      }}>
      <MI name={name} size={18} color="currentColor"/>
      {opts.dot && (
        <span style={{ position: 'absolute', top: 7, right: 7, width: 7, height: 7, borderRadius: '50%', background: THEME.warn, border: `1.5px solid ${isDark ? '#2a3640' : '#FCF8EE'}` }}/>
      )}
    </button>
  );

  const divider = () => (
    <div style={{ width: 1, height: 22, background: isDark ? 'rgba(255,255,255,0.14)' : 'rgba(20,30,50,0.14)', margin: '0 4px' }}/>
  );

  const ddStyle = (align) => ({
    position: 'absolute', top: 'calc(100% + 14px)', [align]: -4,
    minWidth: 288, padding: 8, borderRadius: 18,
    background: ddBg, border: `1px solid ${glassBd}`,
    backdropFilter: 'blur(30px) saturate(1.7)', WebkitBackdropFilter: 'blur(30px) saturate(1.7)',
    boxShadow: ddShadow, zIndex: 62,
    animation: 'v3gnDdIn 0.22s cubic-bezier(0.16,1,0.3,1)',
    transformOrigin: align === 'left' ? 'top left' : 'top right',
  });
  const ddHead = (label) => (
    <div style={{ fontSize: 10.5, letterSpacing: 1.2, textTransform: 'uppercase', color: mute, fontWeight: 600, margin: '6px 10px 8px' }}>{label}</div>
  );
  const ddDivider = () => <div style={{ height: 1, background: isDark ? 'rgba(255,255,255,0.10)' : 'rgba(20,30,50,0.10)', margin: '8px 6px' }}/>;
  const ddRow = (icon, label, opts = {}) => (
    <button type="button"
      onClick={(e) => { e.stopPropagation(); (opts.onClick || (() => setMenu(null)))(); }}
      onMouseEnter={(e) => { e.currentTarget.style.background = rowHover; }}
      onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
      style={{
        display: 'flex', alignItems: 'center', gap: 11, width: '100%', border: 0, background: 'transparent',
        color: opts.danger ? (isDark ? '#E78A8A' : '#C0444C') : (opts.accent ? THEME.accent : text),
        fontFamily: 'inherit', fontSize: 13.5, padding: '10px 10px', borderRadius: 11, cursor: 'pointer', textAlign: 'left',
        transition: 'background 0.15s ease',
      }}>
      <MI name={icon} size={17} color={opts.danger ? (isDark ? '#E78A8A' : '#C0444C') : (opts.accent ? THEME.accent : dim)}/>
      <span style={{ flex: 1 }}>{label}{opts.sub && <span style={{ color: mute, fontSize: 11.5, fontWeight: 400 }}>{opts.sub}</span>}</span>
    </button>
  );

  // active-case display name + persona for the profile segment
  const person = emptyState
    ? { name: 'Your account', initials: 'You', email: '' }
    : {
        name: (m.client && m.client.name) || 'Client',
        initials: (m.client && m.client.initials) || 'C',
        email: (m.client && m.client.name)
          ? m.client.name.toLowerCase().replace(/[^a-z]+/g, '.').replace(/^\.|\.$/g, '') + '@email.com'
          : '',
      };
  const firstName = person.name.split(' ')[0];
  const avatarGrad = `linear-gradient(150deg, ${THEME.blue}, ${THEME.blueDim})`;

  return (
    <>
      {menu && <div onClick={() => setMenu(null)} style={{ position: 'fixed', inset: 0, zIndex: 49 }}/>}
      <nav className="v3-glass-nav" style={{
        position: 'absolute', top: 28, right: 36, zIndex: 50,
        display: 'inline-flex', alignItems: 'center', gap: 6,
        padding: '8px 12px', borderRadius: 999,
        background: navBg, border: `1px solid ${glassBd}`,
        backdropFilter: 'blur(26px) saturate(1.7)', WebkitBackdropFilter: 'blur(26px) saturate(1.7)',
        boxShadow: navShadow,
      }}>
        <style>{`@keyframes v3gnDdIn { from { opacity: 0; transform: translateY(-8px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); } }`}</style>

        {/* ── Case switcher (hidden in the empty/first-run state) ── */}
        {!emptyState && (
          <div style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}>
            <button type="button" title="Switch case"
              onClick={(e) => { e.stopPropagation(); setMenu(menu === 'case' ? null : 'case'); }}
              onMouseEnter={(e) => { if (menu !== 'case') e.currentTarget.style.background = isDark ? 'rgba(0,0,0,0.28)' : 'rgba(20,30,50,0.11)'; }}
              onMouseLeave={(e) => { if (menu !== 'case') e.currentTarget.style.background = isDark ? 'rgba(0,0,0,0.18)' : 'rgba(20,30,50,0.06)'; }}
              style={{
                height: 36, display: 'inline-flex', alignItems: 'center', gap: 6,
                border: 0, borderRadius: 999, cursor: 'pointer', padding: '0 12px 0 9px',
                background: menu === 'case' ? (isDark ? 'rgba(0,0,0,0.28)' : 'rgba(20,30,50,0.11)') : (isDark ? 'rgba(0,0,0,0.18)' : 'rgba(20,30,50,0.06)'),
                color: text, fontWeight: 600, fontFamily: 'inherit', fontSize: 13,
                transition: 'background 0.18s ease',
              }}>
              <Badge bg={tone(m) + '33'} color={tone(m)}>TIER {m.tier}</Badge>
              <Badge bg={jBg(m)} color={jTone(m)}>{jLabel(m)}</Badge>
              <span style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.short}</span>
              <Chevron open={menu === 'case'}/>
            </button>
            {menu === 'case' && (
              <div style={ddStyle('left')}>
                {ddHead('Your cases')}
                {Object.values(V3_MATTERS).filter(mm => mm.id !== 'newcase').map(mm => {
                  const active = mm.id === matter;
                  return (
                    <button type="button" key={mm.id}
                      onClick={(e) => { e.stopPropagation(); onSwitchMatter(mm.id); setMenu(null); }}
                      onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = rowHover; }}
                      onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}
                      style={{
                        display: 'flex', alignItems: 'center', gap: 11, width: '100%', border: active ? `1px solid ${isDark ? 'rgba(143,164,208,0.30)' : 'rgba(78,109,168,0.30)'}` : '1px solid transparent',
                        background: active ? THEME.blueSoft : 'transparent', borderRadius: 12, padding: '9px 10px', cursor: 'pointer',
                        textAlign: 'left', marginBottom: 6, transition: 'background 0.15s ease',
                      }}>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13.5, fontWeight: 600, color: text }}>
                          <Badge bg={tone(mm) + '33'} color={tone(mm)}>T{mm.tier}</Badge>
                          <Badge bg={jBg(mm)} color={jTone(mm)}>{jLabel(mm)}</Badge>
                          <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{mm.short}</span>
                        </div>
                        <div style={{ color: dim, fontSize: 11.5, marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                          {(mm.client && mm.client.name) || ''} · {mm.type}{mm.amountLabel ? ' · ' + mm.amountLabel : ''}
                        </div>
                        <div style={{ height: 5, borderRadius: 99, background: isDark ? 'rgba(255,255,255,0.10)' : 'rgba(20,30,50,0.10)', overflow: 'hidden', marginTop: 6 }}>
                          <div style={{ height: '100%', width: `${mm.readiness || 0}%`, background: `linear-gradient(90deg, ${THEME.blueDim}, ${THEME.palette.periwinkleLight})` }}/>
                        </div>
                      </div>
                      {active && <MI name="check" size={18} color={THEME.success}/>}
                    </button>
                  );
                })}
                {ddDivider()}
                {ddRow('grid_view', 'View all matters')}
                {ddRow('add', 'Start a new case', { accent: true, onClick: () => { setMenu(null); onScreen && onScreen('triage'); } })}
              </div>
            )}
          </div>
        )}

        {!emptyState && divider()}

        {iconBtn('updates', 'notifications', { title: 'Updates', dot: true })}
        {iconBtn('theme', isDark ? 'light_mode' : 'dark_mode', {
          title: 'Light / dark', color: THEME.warn,
          onClick: () => onModeChange && onModeChange(isDark ? 'light' : 'dark'),
        })}
        {iconBtn('settings', 'settings', { title: 'Settings', onClick: () => { setMenu(null); onScreen && onScreen('settings'); } })}

        {divider()}

        {/* ── Profile ── */}
        <div style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}>
          <button type="button" title="Your profile"
            onClick={(e) => { e.stopPropagation(); setMenu(menu === 'profile' ? null : 'profile'); }}
            onMouseEnter={(e) => { if (menu !== 'profile') e.currentTarget.style.background = hoverBg; }}
            onMouseLeave={(e) => { if (menu !== 'profile') e.currentTarget.style.background = 'transparent'; }}
            style={{
              height: 36, display: 'inline-flex', alignItems: 'center', gap: 7,
              border: 0, borderRadius: 999, cursor: 'pointer', padding: emptyState ? '0 4px' : '0 8px 0 4px',
              background: menu === 'profile' ? hoverBg : 'transparent', color: text, fontFamily: 'inherit',
              transition: 'background 0.18s ease',
            }}>
            <span style={{ width: 28, height: 28, borderRadius: '50%', background: avatarGrad, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: '#fff' }}>
              {/* Person avatar = icon glyph (2026-07-02, matches the locked mobile Account-avatar decision; case switchers keep initials). */}
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="8" r="3.6"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/></svg>
            </span>
            {/* first-run has no account name yet, so the "Your" label + chevron read as placeholder noise; show just the avatar */}
            {!emptyState && <span style={{ fontSize: 13, fontWeight: 600 }}>{firstName}</span>}
            {!emptyState && <Chevron open={menu === 'profile'}/>}
          </button>
          {menu === 'profile' && (
            <div style={ddStyle('right')}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 10px 12px' }}>
                <span style={{ width: 42, height: 42, borderRadius: '50%', background: avatarGrad, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: '#fff', flexShrink: 0 }}>
                  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="8" r="3.6"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/></svg>
                </span>
                <div style={{ minWidth: 0 }}>
                  <div style={{ fontSize: 15, fontWeight: 600, color: text, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{person.name}</div>
                  {person.email && <div style={{ fontSize: 12, color: mute, marginTop: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{person.email}</div>}
                </div>
              </div>
              {ddDivider()}
              {ddRow('person', 'Your profile', { onClick: () => { setMenu(null); onScreen && onScreen('profile'); } })}
              {ddRow('settings', 'Settings', { onClick: () => { setMenu(null); onScreen && onScreen('settings'); } })}
              {/* Language: English is active; Español is NOT available yet, so it
                  renders as a DISABLED coming-soon affordance (muted, not
                  clickable, "Soon" tag) and routes nowhere. Not a ddRow button —
                  it must not close the menu or navigate.
                  TODO(i18n): when Spanish ships, replace the static EN value +
                  dead-ES pill with a real EN/ES toggle driving the app `lang`
                  state (the V3LanguageRow segmented control already models it). */}
              <div style={{ display: 'flex', alignItems: 'center', gap: 11, width: '100%', padding: '10px 10px', borderRadius: 11 }}>
                <MI name="translate" size={17} color={dim}/>
                <span style={{ flex: 1, fontSize: 13.5, color: text }}>Language: English</span>
                <span aria-label="Español, coming soon" title="Español is coming soon"
                  style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12.5, color: mute }}>
                  Español
                  <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: 0.4, textTransform: 'uppercase', padding: '2px 6px', borderRadius: 5, background: THEME.warnSoft, color: THEME.warnText }}>Soon</span>
                </span>
              </div>
              {ddRow('help', 'Help and guides', { onClick: () => { setMenu(null); onScreen && onScreen('help'); } })}
              {ddDivider()}
              {ddRow('logout', 'Sign out', { danger: true, onClick: () => { setMenu(null); onScreen && onScreen('welcome'); } })}
            </div>
          )}
        </div>
      </nav>
    </>
  );
};

/* Disclosure footer. Non-attorney disclosure surface, persistent */
const V3Disclosure = ({ jx, compact, named }) => (
  <div style={{
    padding: compact ? '8px 12px' : '12px 14px', borderRadius: 10,
    background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
    border: `1px dashed ${THEME.line}`,
    fontSize: compact ? 10.5 : 11.5, color: THEME.textDim, lineHeight: 1.5,
    display: 'flex', gap: 10, alignItems: 'flex-start',
  }}>
    <Icon d={Icons.shield} size={compact ? 12 : 14} stroke={THEME.textMute}/>
    <div style={{ flex: 1 }}>
      <span style={{ color: THEME.text, fontWeight: 600 }}>Hello Court is not a law firm.</span>{' '}
      We help you organize your case and generate documents you can file yourself. The Guide is a legal information tool, not a licensed attorney.
    </div>
  </div>
);

/* Auto-wrap known jargon terms inside an Atticus body string with <Term>
   so partners see click-to-define affordances on jurisdiction, statute,
   service of process, etc. across V3Damages, V3Filing, V3Documents and
   anywhere else V3Atticus renders. Story 0.27 — sweep the body surfaces
   for jargon without having to manually JSX-wrap each occurrence. Pattern
   matching uses word boundaries so we don't match inside larger words.
   Order matters — multi-word terms first so they take precedence over
   single-word terms that would otherwise win the regex race. */
const ATTICUS_TERM_PATTERNS = [
  { re: /\bstatement of claim\b/gi, k: 'statementOfClaim' },
  { re: /\bcause of action\b/gi, k: 'causeOfAction' },
  { re: /\bdamages worksheet\b/gi, k: 'damagesWorksheet' },
  { re: /\bexhibit index\b/gi, k: 'exhibitIndex' },
  { re: /\btreble damages\b/gi, k: 'trebleDamages' },
  { re: /\bstatutory damages\b/gi, k: 'statutoryDamages' },
  { re: /\blegal theory\b/gi, k: 'legalTheory' },
  { re: /\bservice of process\b/gi, k: 'service' },
  { re: /\bsubstantial performance\b/gi, k: 'substantialPerformance' },
  { re: /\bconsumer-oriented\b/gi, k: 'consumerOriented' },
  { re: /\bjurisdiction(s)?\b/gi, k: 'jurisdiction' },
  { re: /\bstatute(s)?\b/gi, k: 'statute' },
  { re: /\bpro se\b/gi, k: 'proSe' },
];
function renderWithTerms(text) {
  if (typeof text !== 'string') return text;
  // Find all matches across all patterns, dedupe overlaps (longest wins).
  const matches = [];
  ATTICUS_TERM_PATTERNS.forEach(({ re, k }) => {
    let m;
    re.lastIndex = 0;
    while ((m = re.exec(text)) !== null) {
      matches.push({ start: m.index, end: m.index + m[0].length, word: m[0], k });
    }
  });
  matches.sort((a, b) => a.start - b.start || (b.end - b.start) - (a.end - a.start));
  const kept = [];
  let cursor = 0;
  for (const mat of matches) {
    if (mat.start < cursor) continue; // overlaps an already-kept match
    kept.push(mat);
    cursor = mat.end;
  }
  if (kept.length === 0) return text;
  const out = [];
  let pos = 0;
  kept.forEach((mat, i) => {
    if (mat.start > pos) out.push(text.slice(pos, mat.start));
    out.push(<Term key={`t${i}`} k={mat.k}>{mat.word}</Term>);
    pos = mat.end;
  });
  if (pos < text.length) out.push(text.slice(pos));
  return out;
}

/* Atticus card. Every Atticus output uses this */
/* V3Atticus. Clean message card. Liquid-glass surface, left accent rail,
   compact header with sparkle + identity + jurisdiction pill, body, disclosure footer. */
const V3Atticus = ({ topic, body, jx, dense }) => {
  const isDark = THEME.mode === 'dark';
  return (
    <div style={{
      position: 'relative',
      padding: dense ? '12px 16px 12px 18px' : '14px 18px 12px 20px',
      borderRadius: 12,
      background: isDark ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)',
      backdropFilter: 'blur(28px) saturate(1.6)',
      WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
      border: `1px solid ${THEME.line}`,
      overflow: 'hidden',
    }}>
      {/* Left accent rail. Periwinkle gradient, marks the Guide message */}
      <div style={{
        position: 'absolute', left: 0, top: 0, bottom: 0, width: 3,
        background: `linear-gradient(180deg, ${THEME.blue} 0%, ${THEME.accent} 100%)`,
      }}/>
      {/* Header. min-width:0 on the topic span lets it shrink + ellipsis
          when the row gets tight; "Your Guide" + the dot + the sandbox pill
          all hold their full size. Sparkle icon also flex-shrink:0. */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, minWidth: 0 }}>
        <span style={{ flexShrink: 0, display: 'inline-flex' }}>
          <Icon d={Icons.sparkle} size={13} stroke={THEME.blue} fill={THEME.blue}/>
        </span>
        <span style={{ fontSize: 11.5, color: THEME.text, fontWeight: 600, letterSpacing: -0.1, whiteSpace: 'nowrap', flexShrink: 0 }}>Your Guide</span>
        <span style={{ width: 3, height: 3, borderRadius: 2, background: THEME.textMute, opacity: 0.5, flexShrink: 0 }}/>
        <span style={{ fontSize: 11, color: THEME.textDim, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', minWidth: 0, flex: 1 }}>{topic}</span>
        <div style={{
          fontSize: 11.5, lineHeight: 1.4, fontWeight: 400,
          color: THEME.textDim,
          flexShrink: 0, whiteSpace: 'nowrap',
        }}>
          {jx === 'utah' ? 'sandbox-authorized (UT)' : 'educational · NY'}
        </div>
      </div>
      {/* Body. renderWithTerms auto-wraps known jargon (statute, jurisdiction,
          legal theory, etc.) so partners get click-to-define glossary entries
          in Atticus output across every screen the card renders on. */}
      <div style={{ fontSize: 12.75, color: THEME.text, lineHeight: 1.55, marginBottom: 8, fontWeight: 400 }}>
        {renderWithTerms(body)}
      </div>
      {/* Footer disclosure */}
      <div style={{
        fontSize: 9.5, color: THEME.textMute, fontStyle: 'italic',
        paddingTop: 7, borderTop: `1px solid ${THEME.lineSoft}`,
        letterSpacing: 0.1,
      }}>
        Guide note · not legal advice.
      </div>
    </div>
  );
};

/* Tier badge */
/* Tier badge with click-to-expand explainer popover (Story 0.18). The
   tier model is load-bearing for partners but isn't self-explanatory
   from the label alone — clicking the badge reveals what each tier
   covers so they don't have to ask. Esc / click-outside dismiss. */
const V3TierBadge = ({ tier }) => {
  const map = {
    1: { label: 'Tier 1 · Full pro-se support', tone: THEME.success },
    2: { label: 'Tier 2 · Lawyer handoff', tone: THEME.warn },
    3: { label: 'Tier 3 · Orientation + lawyer match', tone: THEME.blue },
  };
  const { label, tone } = map[tier];
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onClick);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onClick);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);
  const tiers = [
    { n: 1, label: 'Full pro-se support', tone: THEME.success,
      body: 'You have a clear claim, the dollars fall under the small-claims cap, and the path is documented. Hello Court walks you all the way to a file-ready packet you take to court yourself.' },
    { n: 2, label: 'Lawyer handoff', tone: THEME.warn,
      body: 'Your case is too complex or too high-value to handle pro se. Hello Court organizes everything you have into a structured packet a lawyer can read in 10 minutes, then matches you with verified attorneys for the case-type and jurisdiction.' },
    { n: 3, label: 'Orientation + lawyer match', tone: THEME.blue,
      body: 'You\'re not sure what kind of case (or whether) you have yet. Hello Court walks you through what the law treats as actionable, what evidence you would need, and connects you with attorneys who offer free first-look consults in your area.' },
  ];
  return (
    <span ref={ref} style={{ position: 'relative', display: 'inline-flex' }}>
      <button
        type="button"
        onClick={() => setOpen(o => !o)}
        title="Click to see what each tier covers"
        style={{
          fontSize: 10.5, fontWeight: 700, padding: '3px 9px', borderRadius: 5, letterSpacing: 0.4,
          background: tone + '22', color: tone, border: `1px solid ${tone}40`,
          display: 'inline-flex', alignItems: 'center', gap: 5, cursor: 'pointer',
          fontFamily: 'inherit',
          whiteSpace: 'nowrap', flexShrink: 0,
        }}>
        <span style={{ width: 5, height: 5, borderRadius: 3, background: tone }}/>
        {label}
        <span style={{ fontSize: 9.5, opacity: 0.7, marginLeft: 2 }}>?</span>
      </button>
      {open && (
        <div style={{
          position: 'absolute', top: 'calc(100% + 8px)', left: 0,
          width: 320, zIndex: 200,
          padding: '14px 16px',
          background: THEME.mode === 'dark' ? 'rgba(35,48,61,0.97)' : 'rgba(255,255,255,0.98)',
          border: `1px solid ${THEME.line}`,
          borderRadius: 12,
          boxShadow: '0 18px 40px -12px rgba(0,0,0,0.45)',
          backdropFilter: 'blur(20px) saturate(1.4)',
          WebkitBackdropFilter: 'blur(20px) saturate(1.4)',
          fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
        }}>
          <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1, fontWeight: 700, textTransform: 'uppercase', marginBottom: 8 }}>How tiers work</div>
          {tiers.map(t => {
            const isCurrent = t.n === tier;
            return (
              <div key={t.n} style={{
                marginBottom: 10, padding: '8px 10px', borderRadius: 8,
                background: isCurrent ? (THEME.mode === 'dark' ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.03)') : 'transparent',
                border: isCurrent ? `1px solid ${t.tone}40` : '1px solid transparent',
              }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 }}>
                  <span style={{ width: 6, height: 6, borderRadius: 3, background: t.tone }}/>
                  <span style={{ fontSize: 11.5, fontWeight: 700, color: t.tone, letterSpacing: 0.3 }}>Tier {t.n}</span>
                  <span style={{ fontSize: 11.5, color: THEME.text, fontWeight: 600 }}>{t.label}</span>
                  {isCurrent && <span style={{ marginLeft: 'auto', fontSize: 9.5, color: t.tone, fontWeight: 700, letterSpacing: 0.5, textTransform: 'uppercase' }}>This case</span>}
                </div>
                <div style={{ fontSize: 11.5, color: THEME.textDim, lineHeight: 1.5, paddingLeft: 12 }}>{t.body}</div>
              </div>
            );
          })}
          <div style={{ fontSize: 10, color: THEME.textMute, fontStyle: 'italic', marginTop: 6, lineHeight: 1.45 }}>
            Click the badge again or press Esc to close.
          </div>
        </div>
      )}
    </span>
  );
};

/* Stage rail */
const V3StageRail = ({ stageIdx }) => {
  const stages = ['Intake', 'Preparation', 'Filing', 'Hearing', 'Resolution'];
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 0 }}>
      {stages.map((s, i) => {
        const done = i < stageIdx, active = i === stageIdx;
        const tone = done || active ? THEME.blue : THEME.lineStrong;
        return (
          <React.Fragment key={s}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
              <div style={{
                width: 18, height: 18, borderRadius: 9,
                background: done ? THEME.blue : active ? THEME.blue + '30' : 'transparent',
                border: `1.5px solid ${tone}`,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}>
                {done && <Icon d={Icons.check} size={10} stroke="#D7DEE2" sw={3}/>}
                {active && <div style={{ width: 6, height: 6, borderRadius: 3, background: THEME.blue }}/>}
              </div>
              <span style={{ fontSize: 11, color: active ? THEME.text : done ? THEME.textDim : THEME.textMute, fontWeight: active ? 600 : 500 }}>{s}</span>
            </div>
            {i < stages.length - 1 && <div style={{ width: 24, height: 1, background: i < stageIdx ? THEME.blue : THEME.line, margin: '0 8px' }}/>}
          </React.Fragment>
        );
      })}
    </div>
  );
};

/* ================================ SCREENS ================================ */

/* Free-guide content for the home "Free guides" chips + preview modal
   (home redesign 2026-06-05). Plain-language, UPL-safe, no em dashes. The
   modal's Download PDF will point at the standalone Survival Guide PDF once
   that ships (Workstream B); for now Download / Share are visual. */
const V3_HOME_GUIDES = {
  'Hearing-day prep checklist': {
    intro: 'Walk into your hearing calm and ready. Here is everything to do on the day, in order.',
    bullets: [
      'Arrive at least 30 minutes early and find your courtroom.',
      'Bring three copies of every document.',
      'Have your one-sentence ask ready: what you want and why.',
      'Silence your phone before you walk in.',
      'Be respectful, speak slowly, and stick to the facts.',
    ],
  },
  "Can't afford the fee? Fee waiver": {
    intro: 'If the filing fee is a hardship, you can ask the court to waive it. Here is how, step by step.',
    bullets: [
      'Who qualifies, based on your income or benefits.',
      'The fee waiver form you need for your court.',
      'What proof to attach, like pay stubs or a benefits letter.',
      'Where and how to file it.',
      'What happens after you submit it.',
    ],
  },
  'What to bring and wear': {
    intro: 'Show up looking prepared and feeling confident. Here is exactly what to bring and how to dress.',
    bullets: [
      'Photo ID and your case or docket number.',
      'Three copies of all your evidence.',
      'Your filing packet and any receipts.',
      'Neat, plain clothing with no logos or hats.',
      'A pen and notepad to take notes.',
    ],
  },
  'After you win: collecting': {
    intro: 'Winning is step one. Here is how to actually collect the money the court awarded you.',
    bullets: [
      'Ask for payment in writing first.',
      'Request the official judgment papers from the court.',
      'Your options if they still do not pay.',
      'The basics of wage garnishment and bank levies.',
      'Keep a record of every payment and contact.',
    ],
  },
  'Your complete guide library': {
    intro: 'This is just a taste. Our full library has free, plain-language guides for every step of a small claims case, from deciding whether it is right for you, to filing, serving, your hearing, and collecting after you win.',
    bullets: [
      'Before you file: is small claims right for you',
      'Filing your claim and serving the other side',
      'Building and organizing your evidence',
      'Your hearing, from start to finish',
      'After the decision: collecting what you are owed',
    ],
    note: 'Plus the complete Self-Represented Survival Guide, all in one place.',
  },
};

/* Free-guide preview modal. Portaled to body so it escapes tile transforms /
   overflow. Per-mode inline styling (no .light class). */
const V3GuidePreviewModal = ({ name, onClose }) => {
  if (!name) return null;
  const isDark = THEME.mode === 'dark';
  const g = V3_HOME_GUIDES[name] || { intro: 'A plain-language guide on this topic.', bullets: [] };
  const text = THEME.text, dim = THEME.textDim, mute = THEME.textMute, accent = THEME.accent;
  const noteBg = isDark ? 'rgba(105,134,191,0.10)' : 'rgba(105,134,191,0.12)';
  const noteBd = isDark ? 'rgba(143,164,208,0.25)' : 'rgba(78,109,168,0.30)';
  return ReactDOM.createPortal(
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 300, display: 'flex', alignItems: 'center', justifyContent: 'center',
      background: 'rgba(10,14,20,0.62)', backdropFilter: 'blur(5px)', WebkitBackdropFilter: 'blur(5px)', padding: 20,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        width: 'min(560px, 92vw)', maxHeight: '82vh', overflow: 'auto', position: 'relative',
        background: isDark ? 'linear-gradient(165deg, rgba(42,54,68,0.97), rgba(26,34,46,0.97))' : 'linear-gradient(165deg, rgba(255,253,247,0.98), rgba(244,239,227,0.98))',
        border: `1px solid ${isDark ? 'rgba(255,255,255,0.12)' : 'rgba(20,30,50,0.12)'}`,
        borderRadius: 18, padding: '28px 30px', boxShadow: '0 30px 80px rgba(0,0,0,0.6)',
      }}>
        <button type="button" onClick={onClose} aria-label="Close" style={{ position: 'absolute', top: 14, right: 16, background: 'transparent', border: 0, color: mute, fontSize: 24, lineHeight: 1, cursor: 'pointer' }}>×</button>
        <div style={{ fontSize: 11, letterSpacing: 1.4, textTransform: 'uppercase', color: accent, fontWeight: 600 }}>Free guide · no sign-up</div>
        <h2 style={{ fontFamily: 'Fraunces', fontWeight: 600, fontSize: 24, margin: '6px 0 14px', color: text }}>{name}</h2>
        <p style={{ color: dim, fontSize: 14, lineHeight: 1.7, margin: '0 0 12px' }}>{g.intro}</p>
        <ul style={{ margin: '6px 0 0', padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column', gap: 9 }}>
          {g.bullets.map((b, i) => (
            <li key={i} style={{ display: 'flex', gap: 10, color: text, fontSize: 13.5, lineHeight: 1.5 }}>
              <span style={{ flexShrink: 0, width: 7, height: 7, borderRadius: '50%', background: accent, marginTop: 6 }}/>
              <span>{b}</span>
            </li>
          ))}
        </ul>
        {g.note && <p style={{ color: dim, fontSize: 14, lineHeight: 1.7, margin: '14px 0 0' }}>{g.note}</p>}
        <div style={{ marginTop: 16, padding: '12px 14px', borderRadius: 10, background: noteBg, border: `1px solid ${noteBd}`, display: 'flex', alignItems: 'flex-start', gap: 8, fontSize: 12.5, fontStyle: 'italic', color: mute, lineHeight: 1.5 }}>
          <Icon d={Icons.shield} size={15} stroke={mute}/>
          <span>This is a short preview. The full guide walks through each step in detail, with examples and what to expect.</span>
        </div>
        <div style={{ display: 'flex', gap: 10, marginTop: 20 }}>
          <Btn kind="primary" onClick={onClose}>Download PDF</Btn>
          <Btn kind="secondary" onClick={onClose}>Share</Btn>
        </div>
        <div style={{ marginTop: 16, padding: '12px 14px', borderRadius: 10, background: noteBg, border: `1px solid ${noteBd}`, color: accent, fontSize: 13, lineHeight: 1.5 }}>
          Free to read, save, print, or share anytime. This is general information, not legal advice.
        </div>
      </div>
    </div>,
    document.body
  );
};

/* Slot-machine deadline counter. Each digit reel spins through 0-9 twice then
   lands on its final digit; later digits land slightly after earlier ones. */
const V3Odometer = ({ value, unit = 'days' }) => {
  const H = 38;
  const digits = String(value == null ? 0 : value).split('');
  return (
    <span style={{ display: 'inline-flex', alignItems: 'flex-end', fontFamily: 'Fraunces', fontSize: 34, fontWeight: 600, color: THEME.warnText, lineHeight: 1 }}>
      <style>{`@keyframes v3reelSpin { from { transform: translateY(0); } to { transform: translateY(-${20 * H}px); } }`}</style>
      {digits.map((d, i) => (
        <span key={i} style={{ display: 'inline-block', height: H, overflow: 'hidden' }}>
          <span style={{ display: 'flex', flexDirection: 'column', animation: `v3reelSpin ${1.3 + i * 0.5}s cubic-bezier(0.2,0.85,0.12,1) 0.3s both` }}>
            {Array.from({ length: 20 }).map((_, n) => (
              <span key={n} style={{ height: H, lineHeight: `${H}px`, display: 'block', textAlign: 'center' }}>{n % 10}</span>
            ))}
            <span style={{ height: H, lineHeight: `${H}px`, display: 'block', textAlign: 'center' }}>{d}</span>
          </span>
        </span>
      ))}
      <span style={{ height: H, lineHeight: `${H}px`, fontSize: 15, fontWeight: 500, color: THEME.textDim, paddingBottom: 4 }}>&nbsp;{unit}</span>
    </span>
  );
};

/* Compact readiness donut for the home hero (animates in on load). The full
   factor-breakdown ring (V3ReadinessRing) still powers the Case-readiness
   screen. */
const V3HomeDonut = ({ score = 0 }) => {
  const isDark = THEME.mode === 'dark';
  const r = 42;
  const C = 2 * Math.PI * r;
  const s = Math.max(0, Math.min(100, score));
  const off = C * (1 - s / 100);
  const ring = THEME.accent;
  const track = isDark ? 'rgba(255,255,255,0.09)' : 'rgba(20,30,50,0.12)';
  const anim = `v3donut_${Math.round(s)}`;
  return (
    <div style={{ position: 'relative', width: 96, height: 96, display: 'flex', alignItems: 'center', justifyContent: 'center', filter: 'drop-shadow(0 1px 4px rgba(0,0,0,0.30))' }}>
      <style>{`@keyframes ${anim} { to { stroke-dashoffset: ${off.toFixed(1)}; } } @keyframes v3donutNum { to { opacity: 1; } }`}</style>
      <svg width="96" height="96" viewBox="0 0 100 100" style={{ transform: 'rotate(-90deg)' }}>
        <circle cx="50" cy="50" r={r} fill="none" stroke={track} strokeWidth="7"/>
        <circle cx="50" cy="50" r={r} fill="none" stroke={ring} strokeWidth="7" strokeLinecap="round"
          strokeDasharray={C.toFixed(1)} strokeDashoffset={C.toFixed(1)}
          style={{ animation: `${anim} 1.15s cubic-bezier(0.22,1,0.36,1) 0.25s forwards` }}/>
      </svg>
      <span style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'Fraunces', fontWeight: 600, fontSize: 23, letterSpacing: -0.5, color: THEME.text, opacity: 0, animation: 'v3donutNum 0.5s ease 0.75s forwards' }}>
        {Math.round(s)}%
      </span>
    </div>
  );
};

const V3Home = ({ matter, screen, onScreen }) => {
  const ds = getMatterDataset(matter);
  const m = V3_MATTERS[matter];
  // Newcase tile selection — none picked by default. Click selects the tile
  // (no auto-nav). Continue at bottom is disabled until a tile is selected.
  const [selectedStep, setSelectedStep] = React.useState(null);
  const [openGuide, setOpenGuide] = React.useState(null);
  const [showSteps, setShowSteps] = React.useState(false); // "What's left" overflow modal
  React.useEffect(() => { setSelectedStep(null); }, [matter]);

  /* New-case focused home — shown for matter==='newcase' when __tourHome is
     false (i.e., after TriageIntake creates a fresh blank case). When
     __tourHome is true (the pre-seeded tour example), skip this block so the
     standard V3Home below renders with the readiness ring and data-coach
     targets that the Story 0.17 guided-tour steps require.
     Tiles select (do not auto-nav). Back + Continue drive forward movement.
     Story 0.49 Phase A. */
  if (matter === 'newcase' && !m.__tourHome) {
    const next = m.whatsNext || [];
    return (
      <div style={{ padding: '32px 36px', overflow: 'auto', height: '100%' }}>
        <div style={{ maxWidth: 720 }}>
          <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase', marginBottom: 6 }}>{m.stage || 'Just started'}</div>
          <div style={{ fontFamily: 'Fraunces', fontSize: 30, color: THEME.text, fontWeight: 500, letterSpacing: -0.5, lineHeight: 1.15, marginBottom: 10 }}>
            {m.title}
          </div>
          <div style={{ fontSize: 13.5, color: THEME.textDim, lineHeight: 1.6, marginBottom: 26 }}>
            {m.summary}
          </div>
          <div style={{ fontSize: 11.5, color: THEME.textHeader, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase', marginBottom: 14 }}>
            Pick where to start
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {next.map((item, i) => {
              const isSelected = selectedStep === item.screen;
              const recommended = i === 0;
              const baseBg = isSelected ? THEME.blueSoft : (THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)');
              const hoverBg = isSelected ? 'rgba(105,134,191,0.22)' : (THEME.mode === 'dark' ? 'rgba(36,42,52,0.85)' : 'rgba(255,255,255,0.88)');
              return (
                <button
                  key={i}
                  type="button"
                  onClick={() => setSelectedStep(item.screen)}
                  onMouseEnter={(e) => { e.currentTarget.style.background = hoverBg; }}
                  onMouseLeave={(e) => { e.currentTarget.style.background = baseBg; }}
                  style={{
                    width: '100%', textAlign: 'left',
                    padding: '16px 18px', borderRadius: 14, cursor: 'pointer',
                    background: baseBg,
                    border: `${isSelected ? 2 : 1}px solid ${isSelected ? THEME.blue : THEME.line}`,
                    fontFamily: 'inherit', color: 'inherit',
                    display: 'flex', alignItems: 'center', gap: 14,
                    transition: 'background 0.18s ease, border-color 0.18s ease',
                  }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3, flexWrap: 'wrap' }}>
                      <div style={{ fontSize: 14, color: THEME.text, fontWeight: 600 }}>{item.label}</div>
                      {recommended && <span style={{ fontSize: 11.5, lineHeight: 1.4, fontWeight: 400, color: THEME.textDim }}>recommended first</span>}
                    </div>
                    <div style={{ fontSize: 12, color: THEME.textDim, lineHeight: 1.5 }}>{item.sub}</div>
                  </div>
                </button>
              );
            })}
          </div>

          {/* Back + Continue at the bottom. Continue is disabled until a tile
              is selected. Click Continue to actually navigate. */}
          <div style={{ marginTop: 22, display: 'flex', alignItems: 'center', gap: 12 }}>
            <div style={{ flex: 1 }}/>
            <Btn kind="secondary" onClick={() => onScreen('welcome')}>Back</Btn>
            <Btn kind="primary" disabled={!selectedStep} onClick={() => onScreen(selectedStep)}>
              Continue
            </Btn>
          </div>

          <div style={{ marginTop: 22, padding: '12px 14px', borderRadius: 9, background: THEME.mode === 'dark' ? 'rgba(255,255,255,0.025)' : 'rgba(0,0,0,0.025)', border: `1px dashed ${THEME.line}`, fontSize: 11.5, color: THEME.textDim, lineHeight: 1.55 }}>
            <span style={{ color: THEME.text, fontWeight: 600 }}>You picked the {m.chosenOutcome || 'self-service'} path.</span>{m.recommendedOutcome && m.chosenOutcome && m.chosenOutcome !== m.recommendedOutcome ? ` We had recommended ${m.recommendedOutcome}.` : ''} You can switch any time, or talk to your Guide if you'd like a second pair of eyes.
          </div>
        </div>
      </div>
    );
  }

  // ── Home redesign content (2026-06-05) ──
  const isDark = THEME.mode === 'dark';
  const jx = m.jurisdiction;
  const isUT = jx === 'utah';
  const peopleWord = isUT ? 'Utahns' : 'New Yorkers';
  const rLabel = (m.readiness >= 75) ? 'On track' : (m.readiness >= 50) ? 'Getting there' : 'Just getting started';
  const nextStep = (m.whatsNext && m.whatsNext[0]) || { label: 'Continue your case', sub: 'Pick up where you left off.', screen: 'matter' };
  // "What's left" steps. List up to MAX_STEPS inline; overflow opens a modal.
  const stepsLeft = m.whatsNext || [];
  const MAX_STEPS = 2;
  const stepsInline = stepsLeft.slice(0, MAX_STEPS);
  const stepsOverflow = stepsLeft.length > MAX_STEPS;

  // Demo target dates (not hard legal deadlines; labeled "Target date" to stay UPL-safe).
  const deadlineByMatter = {
    maria: { days: 12, label: 'to finish your filing packet', when: 'Target date: June 17, 2026' },
    daniel: { days: 21, label: 'to your scheduling conference', when: 'Target date: June 26, 2026' },
    patel: { days: 9, label: 'to file your claim', when: 'Target date: June 14, 2026' },
  };
  const deadline = deadlineByMatter[matter] || { days: 14, label: 'for your next step', when: '' };

  const goodToKnowByMatter = {
    maria: { text: 'In Utah, a landlord generally has 30 days after you move out to return your deposit or send an itemized list of any deductions.', cite: 'General information from Utah Code § 57-17-3. This is not legal advice.' },
    daniel: { text: 'In Utah, a written agreement is generally enforceable, and you can usually ask the court for what you are owed when the other side does not pay.', cite: 'General information about Utah contract law. This is not legal advice.' },
    patel: { text: 'In New York, employers generally must pay 1.5 times your regular rate for hours over 40 in a week, and unpaid wages can carry an extra 100 percent in liquidated damages.', cite: 'General information from NY Labor Law § 198 and FLSA § 207. This is not legal advice.' },
  };
  const goodToKnow = goodToKnowByMatter[matter] || { text: m.statute ? `Your case is based on ${m.statute}.` : 'We will surface the key rule for your case here as it firms up.', cite: 'General information, not legal advice.' };

  const notAlone = `Thousands of ${peopleWord} bring cases like this themselves each year, without a lawyer. You can too, and we will be with you the whole way.`;

  const learnRows = [
    { title: 'How to serve the other side', sub: '5 min read, plain language', screen: 'filing', icon: 'send' },
    { title: 'Gather your evidence', sub: `What helps prove a ${ds.caseNoun} case`, screen: 'evidence', icon: 'folder_open' },
  ];
  const guideNames = ['Hearing-day prep checklist', "Can't afford the fee? Fee waiver", 'What to bring and wear', 'After you win: collecting'];

  // True liquid-glass tile (both modes inline; not a .light class).
  const tile = (extra) => ({
    background: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(255,255,255,0.42)',
    border: `1px solid ${isDark ? 'rgba(255,255,255,0.075)' : 'rgba(20,30,50,0.10)'}`,
    borderRadius: 16, padding: '18px 18px',
    backdropFilter: 'blur(22px) saturate(1.5)', WebkitBackdropFilter: 'blur(22px) saturate(1.5)',
    boxShadow: isDark ? 'inset 0 1px 0 rgba(255,255,255,0.06), 0 8px 28px -12px rgba(0,0,0,0.4)' : 'inset 0 1px 0 rgba(255,255,255,0.6), 0 8px 24px -14px rgba(20,30,50,0.20)',
    ...(extra || {}),
  });
  const kLabel = { fontSize: 10.5, letterSpacing: 1.1, textTransform: 'uppercase', color: THEME.textMute, fontWeight: 600, margin: '0 0 9px', display: 'flex', alignItems: 'center', gap: 7 };
  const link = { color: THEME.accent, fontSize: 12.5, cursor: 'pointer' };
  const mi = (name, size = 14, color = THEME.textMute) => (
    <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: size, lineHeight: 1, color, fontVariationSettings: "'opsz' 24" }}>{name}</span>
  );

  const banner = (
    <div data-coach="home-hero" className="v3-matter-banner" style={{ marginBottom: 18 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8, flexWrap: 'wrap' }}>
        <V3TierBadge tier={m.tier}/>
        <span style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase', whiteSpace: 'nowrap' }}>{m.statute}</span>
      </div>
      <div style={{ fontFamily: 'Fraunces', fontSize: 30, color: THEME.text, fontWeight: 500, letterSpacing: -0.5, lineHeight: 1.1, marginBottom: 6 }}>
        {m.title}
      </div>
      <div style={{ fontSize: 12.5, color: THEME.textDim }}>
        {m.client.name} · {m.client.city} · {m.type}
      </div>
    </div>
  );

  // Detailed readiness view (the "Case readiness" nav). Keeps the full factor
  // breakdown + popover; the home-readiness walkthrough anchor lives inside
  // V3ReadinessRing AND on the compact home donut below, so both screens work.
  if (screen === 'readiness') {
    return (
      <div className="v3-case-home-root" style={{ padding: '28px 36px 80px', overflow: 'hidden auto', height: '100%' }}>
        <style>{`
          /* Mirror the Home hero: never let one donut hog a full row. Three tiles
             across on wide; drop to two when three will not fit, with the next
             deadline carried below; single column when very narrow. Evaluated
             against .v3-case-home-root (container-type: inline-size in demo.html). */
          .v3-readiness-hero { display: grid; grid-template-columns: 200px 1fr 1fr; gap: 14px; margin-bottom: 16px; }
          @container (max-width: 900px) {
            .v3-readiness-hero { grid-template-columns: 200px 1fr; }
            .v3-readiness-hero .v3-rh-deadline { grid-column: 1 / -1; }
          }
          @container (max-width: 560px) {
            .v3-readiness-hero { grid-template-columns: 1fr; }
            .v3-readiness-hero .v3-rh-deadline { grid-column: auto; }
            /* Single column: the donut content is tiny, so don't let its tile
               stretch full-width. Cap it and center it. (James QA 2026-06-08.) */
            .v3-readiness-hero .v3-rh-donut { width: 220px; max-width: 100%; justify-self: center; }
          }
        `}</style>
        {banner}

        {/* Hero summary row (mirrors Home): compact readiness donut + next step +
            next deadline. Reuses V3HomeDonut, the nextStep object, the deadline
            tile markup, and the tile()/kLabel/link/mi helpers from this scope. */}
        <div className="v3-readiness-hero">
          <div className="v3-rh-donut" style={tile({ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 9 })}>
            <V3HomeDonut score={m.readiness || 0}/>
            <div style={{ fontSize: 12, color: THEME.textDim, textAlign: 'center', lineHeight: 1.4 }}>Case readiness<br/>{rLabel}</div>
          </div>

          <div style={tile()}>
            <p style={kLabel}>{mi('bolt')} Your next step</p>
            <div style={{ fontFamily: 'Fraunces', fontSize: 18, fontWeight: 600, color: THEME.text, letterSpacing: -0.2, lineHeight: 1.25, margin: '2px 0 6px' }}>{nextStep.label}</div>
            <div style={{ color: THEME.textDim, fontSize: 13, lineHeight: 1.5 }}>{nextStep.sub}</div>
            <div style={{ marginTop: 13 }}><Btn kind="primary" onClick={() => onScreen(nextStep.screen)}>Continue</Btn></div>
          </div>

          <div className="v3-rh-deadline" style={tile({ display: 'flex', flexDirection: 'column' })}>
            <p style={kLabel}>{mi('event')} Next deadline</p>
            <V3Odometer value={deadline.days} unit="days"/>
            <div style={{ color: THEME.textDim, fontSize: 13, marginTop: 6 }}>{deadline.label}</div>
            {deadline.when && <div style={{ color: THEME.textMute, fontSize: 12, marginTop: 3 }}>{deadline.when}</div>}
            <span onClick={() => onScreen(nextStep.screen)} style={{ ...link, marginTop: 'auto', alignSelf: 'flex-end', paddingTop: 12 }}>Continue your case ›</span>
          </div>
        </div>

        {/* The real purpose of this screen: the detailed factor breakdown ring. */}
        <V3ReadinessRing matter={matter} onScreen={onScreen}/>
        <div style={{ marginTop: 18 }}>
          <V3Disclosure jx={m.jurisdiction}/>
        </div>
      </div>
    );
  }

  const lrowBase = {
    display: 'flex', alignItems: 'center', gap: 11, padding: '11px 12px', borderRadius: 11,
    background: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(20,30,50,0.04)',
    border: `1px solid ${isDark ? 'rgba(255,255,255,0.075)' : 'rgba(20,30,50,0.08)'}`,
    cursor: 'pointer',
  };
  const courtRow = { display: 'flex', gap: 9, fontSize: 13, color: THEME.textDim, marginBottom: 7, alignItems: 'flex-start' };
  const gchip = {
    display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 12.5, color: THEME.text,
    background: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(20,30,50,0.05)',
    border: `1px solid ${isDark ? 'rgba(255,255,255,0.075)' : 'rgba(20,30,50,0.10)'}`,
    borderRadius: 999, padding: '8px 13px', cursor: 'pointer', fontFamily: 'inherit',
  };
  const gchipMore = {
    ...gchip,
    background: 'rgba(105,134,191,0.22)',
    border: `1px solid ${isDark ? 'rgba(143,164,208,0.52)' : 'rgba(78,109,168,0.45)'}`,
    color: isDark ? '#EAEFF9' : '#26324B', fontWeight: 600,
  };

  return (
    <div className="v3-case-home-root" style={{ padding: '28px 36px 110px', overflow: 'hidden auto', height: '100%' }}>
      <style>{`
        /* Hero: compact 200px tiles (donut, deadline) alternate with wide 1fr
           tiles (next step, what's left). 4-across on wide, 2x2 on medium, stack
           on mobile. The donut never spans a full-width band. */
        .v3-home-hero { display: grid; grid-template-columns: 200px 1fr 200px 1fr; gap: 14px; margin-bottom: 14px; }
        .v3-home-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
        .v3-home-grid .v3-span2 { grid-column: 1 / -1; }
        @container (max-width: 1100px) { .v3-home-hero { grid-template-columns: 200px 1fr; } } /* 2x2: donut+step / deadline+whats-left */
        @container (max-width: 560px) {
          .v3-home-hero { grid-template-columns: 1fr; }                                          /* mobile: stack */
          .v3-home-hero .v3-rh-donut { width: 220px; max-width: 100%; justify-self: center; }    /* don't stretch the tiny donut full-width (James QA 2026-06-08) */
        }
        @container (max-width: 720px) { .v3-home-grid { grid-template-columns: 1fr; } }
      `}</style>

      {banner}

      {/* Hero: readiness donut + your next step + next deadline */}
      <div className="v3-home-hero">
        <div data-coach="home-readiness" className="v3-rh-donut" onClick={() => onScreen('readiness')}
          style={tile({ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 9, cursor: 'pointer' })}>
          <V3HomeDonut score={m.readiness || 0}/>
          <div style={{ fontSize: 12, color: THEME.textDim, textAlign: 'center', lineHeight: 1.4 }}>Case readiness<br/>{rLabel}</div>
        </div>

        <div style={tile()}>
          <p style={kLabel}>{mi('bolt')} Your next step</p>
          <div style={{ fontFamily: 'Fraunces', fontSize: 18, fontWeight: 600, color: THEME.text, letterSpacing: -0.2, lineHeight: 1.25, margin: '2px 0 6px' }}>{nextStep.label}</div>
          <div style={{ color: THEME.textDim, fontSize: 13, lineHeight: 1.5 }}>{nextStep.sub}</div>
          <div style={{ marginTop: 13 }}><Btn kind="primary" onClick={() => onScreen(nextStep.screen)}>Continue</Btn></div>
        </div>

        <div style={tile({ display: 'flex', flexDirection: 'column' })}>
          <p style={kLabel}>{mi('event')} Next deadline</p>
          <V3Odometer value={deadline.days} unit="days"/>
          <div style={{ color: THEME.textDim, fontSize: 13, marginTop: 6 }}>{deadline.label}</div>
          {deadline.when && <div style={{ color: THEME.textMute, fontSize: 12, marginTop: 3 }}>{deadline.when}</div>}
          <span onClick={() => onScreen(nextStep.screen)} style={{ ...link, marginTop: 'auto', alignSelf: 'flex-end', paddingTop: 12 }}>Continue your case ›</span>
        </div>

        {/* What's left: the actual remaining steps, listed inline. Overflow past
            MAX_STEPS opens a modal with the full list. */}
        <div style={tile({ display: 'flex', flexDirection: 'column' })}>
          <p style={kLabel}>{mi('checklist')} What’s left</p>
          <div style={{ color: THEME.textDim, fontSize: 12.5, margin: '-2px 0 10px' }}>
            {stepsLeft.length} {stepsLeft.length === 1 ? 'step' : 'steps'} to be file-ready
          </div>
          {stepsInline.map((st, i) => (
            <div key={i} onClick={() => onScreen(st.screen)}
              onMouseEnter={(e) => { e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.07)' : 'rgba(20,30,50,0.07)'; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(20,30,50,0.04)'; }}
              style={{ ...lrowBase, alignItems: 'flex-start', marginBottom: i === stepsInline.length - 1 ? 0 : 9 }}>
              {/* To-do glyph = numbered honey ring (2026-07-02, match mobile). warnText
                  keeps the honey number/ring AA-legible in both light and dark. */}
              <span style={{ width: 30, height: 30, borderRadius: '50%', border: `1.5px solid ${THEME.warnText}`, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, marginTop: 1, fontSize: 12.5, fontWeight: 700, color: THEME.warnText }}>{i + 1}</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13, fontWeight: 600, color: THEME.text, lineHeight: 1.3 }}>{st.label}</div>
                {st.sub && <div style={{ fontSize: 11.5, color: THEME.textMute, marginTop: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{st.sub}</div>}
              </div>
              <span style={{ color: THEME.warn, marginTop: 1 }}>›</span>
            </div>
          ))}
          {stepsOverflow && (
            <span onClick={() => setShowSteps(true)} style={{ fontSize: 11.5, fontWeight: 700, padding: '4px 11px', borderRadius: 999, background: THEME.blueSoft, color: THEME.blueOnSoft, border: `1px solid ${THEME.blue}33`, cursor: 'pointer', marginTop: 12, alignSelf: 'flex-end' }}>See all {stepsLeft.length} steps</span>
          )}
        </div>
      </div>

      {/* What's-left overflow modal: full step list when too many to fit the tile. */}
      {showSteps && (
        <div onClick={() => setShowSteps(false)} style={{ position: 'fixed', inset: 0, zIndex: 300, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(3px)', WebkitBackdropFilter: 'blur(3px)', padding: 20 }}>
          <div onClick={(e) => e.stopPropagation()} role="dialog" aria-label="What's left to be file-ready" style={{
            width: 'min(520px, 100%)', maxHeight: '80vh', overflowY: 'auto', borderRadius: 16, padding: 22,
            background: isDark ? 'rgba(31,42,54,0.98)' : '#FCF8EE',
            border: `1px solid ${isDark ? 'rgba(159,179,255,0.18)' : 'rgba(26,35,44,0.10)'}`,
            boxShadow: '0 24px 60px rgba(0,0,0,0.45)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
          }}>
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
              <p style={kLabel}>{mi('checklist')} What’s left</p>
              <span onClick={() => setShowSteps(false)} style={{ cursor: 'pointer', color: THEME.textMute, display: 'inline-flex' }}>{mi('close', 18)}</span>
            </div>
            <div style={{ fontFamily: 'Fraunces', fontSize: 20, fontWeight: 600, color: THEME.text, letterSpacing: -0.3, marginBottom: 14 }}>
              {stepsLeft.length} {stepsLeft.length === 1 ? 'step' : 'steps'} to be file-ready
            </div>
            {stepsLeft.map((st, i) => (
              <div key={i} onClick={() => { setShowSteps(false); onScreen(st.screen); }}
                onMouseEnter={(e) => { e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.07)' : 'rgba(20,30,50,0.07)'; }}
                onMouseLeave={(e) => { e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(20,30,50,0.04)'; }}
                style={{ ...lrowBase, alignItems: 'flex-start', marginBottom: i === stepsLeft.length - 1 ? 0 : 9 }}>
                <span style={{ width: 30, height: 30, borderRadius: 8, background: 'rgba(105,134,191,0.16)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, marginTop: 1, fontSize: 12, fontWeight: 700, color: THEME.accent }}>{i + 1}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13.5, fontWeight: 600, color: THEME.text, lineHeight: 1.3 }}>{st.label}</div>
                  {st.sub && <div style={{ fontSize: 12, color: THEME.textMute, marginTop: 2 }}>{st.sub}</div>}
                </div>
                <span style={{ color: THEME.textMute, marginTop: 1 }}>›</span>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Grid */}
      <div className="v3-home-grid">
        {/* Learn for where you are */}
        <div style={tile()}>
          <p style={kLabel}>{mi('menu_book')} Learn for where you are</p>
          {learnRows.map((row, i) => (
            <div key={i} onClick={() => onScreen(row.screen)}
              onMouseEnter={(e) => { e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.07)' : 'rgba(20,30,50,0.07)'; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(20,30,50,0.04)'; }}
              style={{ ...lrowBase, marginBottom: i === learnRows.length - 1 ? 0 : 9 }}>
              <span style={{ width: 34, height: 34, borderRadius: 9, background: 'rgba(105,134,191,0.16)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>{mi(row.icon, 17, THEME.accent)}</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13.5, fontWeight: 600, color: THEME.text }}>{row.title}</div>
                <div style={{ fontSize: 11.5, color: THEME.textMute, marginTop: 1 }}>{row.sub}</div>
              </div>
              <span style={{ color: THEME.textMute }}>›</span>
            </div>
          ))}
        </div>

        {/* Your court at a glance */}
        <div style={tile({ display: 'flex', flexDirection: 'column' })}>
          <p style={kLabel}>{mi('account_balance')} Your court at a glance</p>
          <div style={courtRow}>{mi('location_on', 15)}<div><b style={{ color: THEME.text, fontWeight: 600 }}>{ds.filing.courtName}</b><br/>{ds.filing.courtAddress}</div></div>
          <div style={courtRow}>{mi('schedule', 15)}<div>Open Monday to Friday, 8:00am to 5:00pm</div></div>
          <span onClick={() => onScreen('courtroom')} style={{ ...link, marginTop: 'auto', alignSelf: 'flex-end', paddingTop: 12 }}>What to bring and wear ›</span>
        </div>

        {/* What happens next (stage rail) */}
        <div className="v3-span2" data-coach="home-stage" style={tile()}>
          <p style={kLabel}>{mi('timeline')} What happens next</p>
          <div style={{ paddingTop: 6, overflowX: 'auto' }}><V3StageRail stageIdx={m.stageIdx}/></div>
        </div>

        {/* Good to know */}
        <div style={tile()}>
          <p style={kLabel}>{mi('info')} Good to know</p>
          <p style={{ fontSize: 13.5, lineHeight: 1.6, color: THEME.text, margin: 0 }}>
            {goodToKnow.text}
            <span style={{ display: 'block', color: THEME.textMute, fontSize: 11.5, marginTop: 8 }}>{goodToKnow.cite}</span>
          </p>
        </div>

        {/* You're not alone */}
        <div style={tile({
          background: isDark ? 'linear-gradient(150deg, rgba(105,134,191,0.16), rgba(78,109,168,0.08))' : 'linear-gradient(150deg, rgba(105,134,191,0.12), rgba(78,109,168,0.06))',
          border: `1px solid ${isDark ? 'rgba(143,164,208,0.25)' : 'rgba(78,109,168,0.22)'}`,
        })}>
          <p style={{ ...kLabel, color: THEME.accent }}>{mi('group', 14, THEME.accent)} You're not alone</p>
          <div style={{ fontFamily: 'Fraunces', fontSize: 17, fontWeight: 600, color: THEME.text, margin: '0 0 5px' }}>People handle these every day.</div>
          <div style={{ color: THEME.accent, fontSize: 13, lineHeight: 1.5 }}>{notAlone}</div>
        </div>

        {/* Free guides */}
        <div className="v3-span2" style={tile()}>
          <p style={kLabel}>{mi('description')} Free guides you can use anytime</p>
          <div style={{ display: 'flex', flexWrap: 'wrap', columnGap: 9, rowGap: 12, marginTop: 4 }}>
            {guideNames.map(g => (
              <button key={g} type="button" onClick={() => setOpenGuide(g)} style={gchip}>{mi('check_circle', 14, THEME.accent)}<span>{g}</span></button>
            ))}
            <button type="button" onClick={() => setOpenGuide('Your complete guide library')} style={gchipMore}>{mi('library_books', 14, isDark ? '#C9D6F0' : '#26324B')}<span>Your complete guide library</span></button>
          </div>
        </div>
      </div>{/* /v3-home-grid */}

      <div style={{ marginTop: 18 }}>
        <V3Disclosure jx={m.jurisdiction}/>
      </div>

      <V3GuidePreviewModal name={openGuide} onClose={() => setOpenGuide(null)}/>
    </div>
  );
};

/* Universal metadata per readiness factor. Same across all matters */
const READINESS_FACTOR_META = {
  elements: {
    description: 'Each part of the law your case has to prove. And whether you have evidence backing each one up.',
    howCalculated: 'For every part of the law (we call these "elements"), we check: do you have a piece of evidence for it? Have you confirmed that piece of evidence is real? If yes. That part counts. If you uploaded something but haven\'t confirmed it yet, you get partial credit. If there\'s nothing for it, that part doesn\'t count.',
    weight: '40%',
    cta: { label: 'Add or confirm evidence', screen: 'evidence' },
  },
  mapping: {
    description: 'How well each piece of evidence you have lines up with the parts of the law your case needs to prove.',
    howCalculated: "We suggest which piece of evidence supports which part of the law. You confirm or change those suggestions. Only confirmed connections count.",
    weight: '30%',
    cta: { label: 'Review evidence connections', screen: 'evidence' },
  },
  statutory: {
    description: "Whether the law we're using actually matches your state and case type. And whether the version of that law is current.",
    howCalculated: 'Atticus (our legal AI) checks the current law and its last update date against the official state court and state bar feeds. Outside lawyers re-check this twice a year.',
    weight: '15%',
    cta: { label: 'Open the law check', screen: 'theory' },
  },
  procedural: {
    description: "The court-paperwork steps for your case: how to serve papers on the other side, whether you qualify for a fee waiver, hearing prep, etc.",
    howCalculated: "We have a checklist for your specific court and case type. Each step you've finished counts. Each missing step lowers the score.",
    weight: '15%',
    cta: { label: 'Open filing checklist', screen: 'filing' },
  },
};

/* Jargon glossary. Single source of truth for "click to define" terms.
   Atticus + Quentin will expand this as we audit more screens. Keep entries short
   and plain (avoid using legal terms inside legal definitions). */
const JARGON = {
  element: {
    title: 'Element',
    body: 'A specific thing the law says you have to prove for your case to win. Different cases have different elements. For a security-deposit case, one element is "the landlord kept your deposit." For a small-claims fraud case, one element is "the other party knew the statement was false." We list yours out, one by one, so you know exactly what to prove.',
  },
  statute: {
    title: 'Statute',
    body: 'The actual written law passed by the state legislature. The rule book your case is decided against. Statutes have numbers (like "Utah § 57-17-3") so courts can point to the exact section.',
  },
  jurisdiction: {
    title: 'Jurisdiction',
    body: 'The state (and sometimes the specific court) whose rules apply to your case. Your jurisdiction depends on where the events happened, where you live, and what the case is about.',
  },
  proSe: {
    title: 'Pro se',
    body: 'A Latin phrase meaning "representing yourself." When you file a case without hiring a lawyer, you\'re pro se. Courts allow it, and we\'re built to help you do it well.',
  },
  service: {
    title: 'Service of process',
    body: "Officially notifying the other side that you've filed a case against them. The court won't move forward until they're properly served. There are specific rules about how it has to be done.",
  },
  affirmation: {
    title: 'Affirm',
    body: 'When you click "Affirm" on a piece of evidence, you\'re telling us, and creating a record, that the fact is true and that you stand behind it. You\'re the one signing off, not the AI.',
  },
  legalTheory: {
    title: 'Legal theory',
    body: 'The specific legal argument you\'re making. It names the law you say was broken and the harm that resulted. Your case sits on top of one or more legal theories.',
  },
  causeOfAction: {
    title: 'Cause of action',
    body: 'A formal name for a legal claim. Each cause of action has elements you have to prove. "Breach of contract" and "wrongful retention of security deposit" are examples.',
  },
  trebleDamages: {
    title: 'Treble damages',
    body: 'Three times the actual damages. Some statutes allow the court to triple what you\'re owed when the other side\'s conduct was willful or particularly bad. The court chooses whether to award it.',
  },
  statutoryDamages: {
    title: 'Statutory damages',
    body: 'A fixed amount the law itself sets, separate from your actual losses. Often a small dollar figure that\'s automatic if you prove the violation. NY GBL § 349 has a $50 statutory damages line.',
  },
  consumerOriented: {
    title: 'Consumer-oriented',
    body: 'Conduct aimed at the general public, not a private dispute between two specific parties. NY GBL § 349 requires the conduct to be consumer-oriented before it counts as a deceptive practice.',
  },
  substantialPerformance: {
    title: 'Substantial performance',
    body: 'When you\'ve done most of what a contract required, even if not perfectly. Courts often credit substantial performance toward what you\'re owed under the contract.',
  },
  statementOfClaim: {
    title: 'Statement of claim',
    body: 'The court form that opens a small-claims case. It says who you\'re suing, what you want, and why you\'re entitled to it. The court uses it to schedule the hearing.',
  },
  exhibitIndex: {
    title: 'Exhibit index',
    body: 'A list of every piece of evidence you\'re bringing to court, numbered and described. Helps the judge follow your case and helps you stay organized at the hearing.',
  },
  damagesWorksheet: {
    title: 'Damages worksheet',
    body: 'A document that shows the math behind what you\'re asking for. Each dollar amount traces back to a fact you affirmed. Courts find it persuasive when damages are easy to verify.',
  },
};

/* Term. Wraps a jargon word with a click-to-define popover. Use anywhere in
   user-facing copy. <Term k="element">elements</Term> renders the word with a
   subtle dotted underline; clicking opens a small definition card. */
const Term = ({ k, children }) => {
  const [open, setOpen] = React.useState(false);
  const [hover, setHover] = React.useState(false);
  const def = JARGON[k];
  if (!def) return <>{children}</>;
  return (
    <span style={{ position: 'relative', display: 'inline' }}>
      <span
        onClick={(e) => { e.stopPropagation(); setOpen(o => !o); }}
        onMouseEnter={() => setHover(true)}
        onMouseLeave={() => setHover(false)}
        style={{
          cursor: 'help',
          // Subtle treatment: no persistent underline. The dotted underline
          // appears on hover so the term reads as plain copy at rest but
          // signals interactivity when explored.
          borderBottom: (hover || open) ? `1px dotted ${THEME.blue}` : '1px solid transparent',
          color: open ? THEME.blue : 'inherit',
          transition: 'border-color 0.15s ease, color 0.15s ease',
        }}
      >{children}</span>
      {open && (
        <>
          <div onClick={() => setOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 79 }}/>
          <div style={{
            position: 'absolute', top: '1.4em', left: 0, zIndex: 80,
            width: 280, padding: '12px 14px', borderRadius: 10,
            background: THEME.mode === 'dark' ? 'rgba(22,28,36,0.98)' : 'rgba(255,255,255,0.99)',
            border: `1px solid ${THEME.blue}40`,
            boxShadow: THEME.mode === 'dark' ? '0 12px 32px rgba(0,0,0,0.55)' : '0 12px 32px rgba(20,30,50,0.18)',
            fontSize: 12, lineHeight: 1.5, color: THEME.text, fontWeight: 400,
            textTransform: 'none', letterSpacing: 0,
          }}>
            <div style={{ fontFamily: 'Fraunces', fontSize: 14, fontWeight: 500, color: THEME.blue, marginBottom: 4 }}>{def.title}</div>
            <div style={{ color: THEME.textDim }}>{def.body}</div>
          </div>
        </>
      )}
    </span>
  );
};

const V3ReadinessRing = ({ matter, onScreen }) => {
  const m = V3_MATTERS[matter];
  const score = m.readiness;
  const pct = score / 100;
  const r = 56, c = 2 * Math.PI * r;
  const tone = score >= 75 ? THEME.success : score >= 50 ? THEME.warn : THEME.danger;
  const [popover, setPopover] = React.useState(null); // factor key or null

  return (
    <>
    <style>{`
      /* Donut and factor breakdown side by side so the donut does not hog a
         full-width band with dead space. Stacks on narrow. Evaluated against
         .v3-case-home-root (container-type: inline-size in demo.html). */
      .v3-readiness-body { display: grid; grid-template-columns: 232px 1fr; gap: 14px 28px; align-items: center; }
      .v3-readiness-ring-wrap { align-self: center; }
      @container (max-width: 620px) { .v3-readiness-body { grid-template-columns: 1fr; } }
    `}</style>
    {/* Spotlight scrim. Dims everything when a factor popover is open. The card
        below has higher z-index, so it stays bright above the scrim. */}
    {popover && (
      <div onClick={() => setPopover(null)} style={{
        position: 'fixed', inset: 0, zIndex: 49,
        background: 'rgba(0,0,0,0.55)',
        backdropFilter: 'blur(2px)',
        WebkitBackdropFilter: 'blur(2px)',
        animation: 'none',
        cursor: 'pointer',
      }}/>
    )}
    <div data-coach="home-readiness" className="v3-readiness-card" style={{
      padding: 20, borderRadius: 14, position: 'relative',
      background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
      border: `1px solid ${popover ? THEME.blue + '60' : THEME.line}`,
      /* When popover is open: lift the whole card above the scrim with a glow */
      ...(popover ? {
        zIndex: 50,
        boxShadow: THEME.mode === 'dark'
          ? `0 0 0 1px ${THEME.blue}40, 0 24px 60px rgba(0,0,0,0.6), 0 0 80px ${THEME.blue}20`
          : `0 0 0 1px ${THEME.blue}40, 0 24px 60px rgba(20,30,50,0.25), 0 0 80px ${THEME.blue}20`,
        transition: 'box-shadow 0.22s ease, border-color 0.22s ease',
      } : {
        transition: 'box-shadow 0.22s ease, border-color 0.22s ease',
      }),
    }}>
      <div style={{ fontSize: 10.5, color: THEME.textMute, letterSpacing: 1.1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 8 }}>Case readiness</div>
      <div className="v3-readiness-body">
      <div className="v3-readiness-ring-wrap">
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative', height: 140 }}>
        <svg width={140} height={140} style={{ transform: 'rotate(-90deg)' }}>
          <circle cx={70} cy={70} r={r} fill="none" stroke={THEME.line} strokeWidth={9}/>
          <circle cx={70} cy={70} r={r} fill="none" stroke={tone} strokeWidth={9}
            strokeDasharray={c} strokeDashoffset={c * (1 - pct)} strokeLinecap="round"
            style={{ transition: 'stroke-dashoffset 0.6s ease' }}/>
        </svg>
        <div style={{ position: 'absolute', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
          <div style={{ fontFamily: 'Fraunces', fontSize: 38, color: THEME.text, fontWeight: 500, letterSpacing: -1, lineHeight: 1 }}>{score}</div>
          <div style={{ fontSize: 9.5, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginTop: 2 }}>of 100</div>
        </div>
      </div>
      <div style={{ textAlign: 'center', fontSize: 12, color: tone, fontWeight: 600, marginTop: 4, marginBottom: 12 }}>
        {score >= 75 ? '✓ Above file-ready threshold' : 'Below 75 file-ready threshold'}
      </div>
      </div>{/* /v3-readiness-ring-wrap */}

      {/* Factors. Always visible. Click any row to open popover with details. */}
      {m.readinessBreakdown && (
        <div className="v3-readiness-factors-wrap" style={{ minWidth: 0 }}>
          {Object.entries(m.readinessBreakdown).map(([key, b]) => {
            const pctFactor = (b.score / b.max) * 100;
            const isOpen = popover === key;
            return (
              <div key={key}
                className="v3-readiness-factor-row"
                onClick={(e) => { e.stopPropagation(); setPopover(isOpen ? null : key); }}
                style={{
                  marginBottom: 8, padding: '6px 8px', borderRadius: 7, cursor: 'pointer',
                  // Selected row LIGHTENS to match the popover info box, so the
                  // visual connection between the row and its popover is obvious.
                  background: isOpen
                    ? (THEME.mode === 'dark' ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.95)')
                    : 'transparent',
                  border: `1px solid ${isOpen ? THEME.blue + '70' : 'transparent'}`,
                  boxShadow: isOpen
                    ? (THEME.mode === 'dark'
                        ? `0 0 0 3px ${THEME.blue}18, 0 4px 14px rgba(0,0,0,0.35)`
                        : `0 0 0 3px ${THEME.blue}22, 0 4px 14px rgba(20,30,50,0.10)`)
                    : 'none',
                  transition: 'background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease',
                }}
                onMouseEnter={(e) => !isOpen && (e.currentTarget.style.background = THEME.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(255,255,255,0.55)')}
                onMouseLeave={(e) => !isOpen && (e.currentTarget.style.background = 'transparent')}
              >
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 11, marginBottom: 4, gap: 8 }}>
                  <span className="v3-readiness-factor-label" style={{ color: THEME.textDim, display: 'inline-flex', alignItems: 'center', gap: 4 }}>
                    {b.label}
                    <span style={{ fontSize: 9, color: THEME.textMute, fontWeight: 600, flexShrink: 0 }}>· {READINESS_FACTOR_META[key]?.weight}</span>
                  </span>
                  <span style={{ color: THEME.text, fontWeight: 600, fontVariantNumeric: 'tabular-nums', flexShrink: 0 }}>
                    {b.score}<span style={{ color: THEME.textMute, fontWeight: 500 }}>/{b.max}</span>
                  </span>
                </div>
                <div style={{ height: 4, background: THEME.line, borderRadius: 2, overflow: 'hidden' }}>
                  <div style={{
                    width: `${pctFactor}%`, height: '100%',
                    background: pctFactor >= 80 ? THEME.success : pctFactor >= 60 ? THEME.blue : THEME.warn,
                    transition: 'width 0.6s ease',
                  }}/>
                </div>
              </div>
            );
          })}
          <div style={{ fontSize: 10, color: THEME.textMute, textAlign: 'center', marginTop: 4 }}>
            Tap any factor for details
          </div>
        </div>
      )}
      </div>{/* /v3-readiness-body */}

      {/* Popover for the active factor */}
      {popover && m.readinessBreakdown && m.readinessBreakdown[popover] && (
        <>
          <div onClick={() => setPopover(null)} style={{
            position: 'fixed', inset: 0, zIndex: 39,
          }}/>
          <div style={{
            position: 'absolute', top: 8, left: 'calc(100% + 12px)', zIndex: 40,
            width: 360, padding: 18, borderRadius: 14,
            background: THEME.mode === 'dark' ? 'rgba(22,28,36,0.96)' : 'rgba(255,255,255,0.98)',
            backdropFilter: 'blur(28px) saturate(1.6)',
            WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
            border: `1px solid ${THEME.line}`,
            boxShadow: THEME.mode === 'dark'
              ? '0 16px 40px rgba(0,0,0,0.55), 0 4px 12px rgba(0,0,0,0.3)'
              : '0 16px 40px rgba(20,30,50,0.18), 0 4px 12px rgba(20,30,50,0.08)',
          }}>
            {(() => {
              const b = m.readinessBreakdown[popover];
              const meta = READINESS_FACTOR_META[popover] || {};
              const pctFactor = (b.score / b.max) * 100;
              return (
                <>
                  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
                    <span style={{ fontSize: 10, color: THEME.textMute, letterSpacing: 1.2, textTransform: 'uppercase', fontWeight: 600 }}>Readiness factor · weight {meta.weight}</span>
                    <span style={{ fontFamily: 'Fraunces', fontSize: 18, color: THEME.text, fontWeight: 500, fontVariantNumeric: 'tabular-nums' }}>
                      {b.score}<span style={{ color: THEME.textMute, fontWeight: 500 }}>/{b.max}</span>
                    </span>
                  </div>
                  <div style={{ fontFamily: 'Fraunces', fontSize: 18, color: THEME.text, fontWeight: 500, letterSpacing: -0.2, marginBottom: 10 }}>{b.label}</div>
                  <div style={{ height: 5, background: THEME.line, borderRadius: 3, overflow: 'hidden', marginBottom: 14 }}>
                    <div style={{
                      width: `${pctFactor}%`, height: '100%',
                      background: pctFactor >= 80 ? THEME.success : pctFactor >= 60 ? THEME.blue : THEME.warn,
                    }}/>
                  </div>
                  <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 4 }}>What it is</div>
                  <div style={{ fontSize: 12.5, color: THEME.text, lineHeight: 1.55, marginBottom: 12 }}>{meta.description}</div>
                  <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 4 }}>How it's calculated</div>
                  <div style={{ fontSize: 12, color: THEME.textDim, lineHeight: 1.55, marginBottom: 12 }}>{meta.howCalculated}</div>
                  {b.missing && (
                    <>
                      <div style={{ fontSize: 11, color: THEME.warnText, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 4 }}>Why this score</div>
                      <div style={{ fontSize: 12.5, color: THEME.text, lineHeight: 1.55, padding: '10px 12px', borderRadius: 8, background: THEME.warnSoft, border: `1px solid ${THEME.warn}30` }}>{b.missing}</div>
                    </>
                  )}
                  {/* Action row: primary CTA jumps to the relevant screen so the user can fix it. */}
                  <div style={{ marginTop: 16, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
                    <Btn kind="secondary" onClick={() => setPopover(null)} style={{ padding: '8px 14px', fontSize: 12 }}>Close</Btn>
                    {meta.cta && onScreen && (
                      <Btn kind="primary" onClick={() => { setPopover(null); onScreen(meta.cta.screen); }} style={{ padding: '9px 14px', fontSize: 12 }}>
                        {meta.cta.label}
                      </Btn>
                    )}
                  </div>
                </>
              );
            })()}
          </div>
        </>
      )}
    </div>
    </>
  );
};

/* Theory: cause-of-action selection. UPL B.2 pattern */
const V3Theory = ({ matter, onScreen, selectedCause, onCauseSelect }) => {
  const m = V3_MATTERS[matter];
  const ds = getMatterDataset(matter);
  const causes = ds.causes;
  // Controlled when parent passes selectedCause + onCauseSelect; local fallback
  // for matters that don't lift this state (Maria autoplay, etc.).
  const [localSelected, setLocalSelected] = React.useState(null);
  const selected = selectedCause !== undefined ? selectedCause : localSelected;
  const setSelected = (id) => {
    if (typeof onCauseSelect === 'function') onCauseSelect(id);
    else setLocalSelected(id);
  };
  const [affirmed, setAffirmed] = React.useState(false);
  React.useEffect(() => {
    if (selectedCause === undefined) setLocalSelected(null);
    setAffirmed(false);
  }, [matter, selectedCause]);
  return (
    <div className="v3-screen-root" style={{ padding: '32px 40px', overflow: 'auto', height: '100%', maxWidth: 1000 }}>
      <div className="v3-screen-title">
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
          <V3TierBadge tier={m.tier}/>
          <Pill soft tone={THEME.blue}>Step 1 of 3</Pill>
          <SectionLabel style={{ marginBottom: 0 }}>Pick the legal theory</SectionLabel>
        </div>
        <div style={{ fontFamily: 'Fraunces', fontSize: 27, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, marginBottom: 10, lineHeight: 1.2 }}>
          Which <Term k="legalTheory">legal theory</Term> describes what happened?
        </div>
        <div style={{ fontSize: 13.5, color: THEME.textDim, lineHeight: 1.65, marginBottom: 22, maxWidth: 720 }}>
          Pick the option that fits your situation best. We'll show you what each one lets you ask for and what you'd need to prove. You can change your mind any time.
        </div>
      </div>

      {/* Atticus only renders for matters with case-specific cause content.
          Newcase uses generic V3_ATTICUS (Maria's deposit content) so it
          would show wrong copy — skip it there until per-category Atticus
          ships. */}
      {matter !== 'newcase' && ds.atticus && ds.atticus.cause && (
        <V3Atticus topic={ds.atticus.cause.topic} body={ds.atticus.cause.body} jx={m.jurisdiction}/>
      )}

      <div data-coach="theory-causes" style={{ display: 'flex', flexDirection: 'column', gap: 14, marginTop: matter !== 'newcase' ? 22 : 0 }}>
        {causes.map(c => {
          const sel = selected === c.id;
          return (
            <div key={c.id} data-cause-id={c.id} onClick={() => { setSelected(c.id); setAffirmed(false); }} style={{
              padding: '20px 22px', borderRadius: 13, cursor: 'pointer',
              background: sel ? (THEME.mode === 'dark' ? 'rgba(105,134,191,0.10)' : 'rgba(105,134,191,0.08)') : THEME.mode === 'dark' ? 'rgba(255,255,255,0.025)' : 'rgba(255,255,255,0.55)',
              border: `1.5px solid ${sel ? THEME.blue : THEME.line}`,
            }}>
              <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
                {/* Canonical checkbox — matches Btn primary aesthetic.
                   Checked: dark navy bg (#1A2230 dark / #2A3640 light) + white check.
                   Unchecked: transparent + lineStrong border. */}
                <div style={{
                  width: 22, height: 22, borderRadius: 6, marginTop: 2, flexShrink: 0,
                  border: `1.5px solid ${sel ? 'rgba(242,244,248,0.18)' : THEME.lineStrong}`,
                  background: sel ? (THEME.mode === 'dark' ? '#1A2230' : '#2A3640') : 'transparent',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  transition: 'background 0.18s ease, border-color 0.18s ease',
                }}>{sel && <Icon d={Icons.check} size={13} stroke="#F2F4F8" sw={3}/>}</div>
                <div style={{ flex: 1 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8, flexWrap: 'wrap' }}>
                    <div style={{ fontFamily: 'Fraunces', fontSize: 18, color: THEME.text, fontWeight: 500 }}>{c.label}</div>
                    <Term k="statute"><Pill soft tone={THEME.textDim} style={{ cursor: 'help' }}>{c.cite}</Pill></Term>
                    {c.remedy && <Pill soft tone={THEME.success}>{c.remedy}</Pill>}
                  </div>
                  <div style={{ fontSize: 13, color: THEME.textDim, lineHeight: 1.6, marginBottom: 12 }}>
                    <span style={{ color: THEME.text, fontWeight: 600 }}>This applies if:</span> {c.applies}
                  </div>
                  {sel && (
                    <div style={{ paddingTop: 14, borderTop: `1px solid ${THEME.lineSoft}` }}>
                      <div style={{ fontSize: 10.5, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 10 }}>You'd need to prove:</div>
                      {c.prove.map((p, i) => (
                        <div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 10, fontSize: 13, color: THEME.text, padding: '5px 0', lineHeight: 1.55 }}>
                          <div style={{ width: 16, height: 16, borderRadius: 8, marginTop: 1, background: THEME.success + '22', border: `1px solid ${THEME.success}40`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                            <Icon d={Icons.check} size={9} stroke={THEME.success} sw={3}/>
                          </div>
                          <div>{p}</div>
                        </div>
                      ))}
                    </div>
                  )}
                </div>
              </div>
            </div>
          );
        })}
      </div>

      {selected && (
        <div style={{ marginTop: 22, padding: '16px 18px', borderRadius: 12, background: affirmed ? THEME.successSoft : THEME.warnSoft, border: `1px solid ${affirmed ? THEME.success + '40' : THEME.warn + '40'}` }}>
          <div onClick={() => setAffirmed(a => !a)} style={{ display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer' }}>
            <div style={{
              width: 20, height: 20, borderRadius: 5, flexShrink: 0,
              border: `1.5px solid ${affirmed ? THEME.success : THEME.warn}`,
              background: affirmed ? THEME.success : 'transparent',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}>{affirmed && <Icon d={Icons.check} size={12} stroke="#D7DEE2" sw={3}/>}</div>
            <div style={{ flex: 1, fontSize: 13, color: THEME.text, fontWeight: 500, lineHeight: 1.55 }}>
              I affirm: <span style={{ fontWeight: 600 }}>this is the <Term k="legalTheory">legal theory</Term> I want to bring.</span> The system presented options; I made the choice.
            </div>
          </div>
        </div>
      )}

      <div style={{ marginTop: 18 }}><V3Disclosure jx={m.jurisdiction} compact/></div>

      {/* Continue CTA — sticky at the bottom; always rendered so the user
          knows it's there. The button is enabled (active) once a theory is
          selected. Clicking it advances to Evidence. The affirmation
          checkbox above is now optional consent UI; the Continue click
          itself records the choice. No auto-advance — user clicks Continue
          when they're ready. */}
      {onScreen && (() => {
        const ready = selected && affirmed;
        const statusTitle = !selected
          ? 'Pick a theory above to continue.'
          : !affirmed
            ? "Check the 'I affirm' box above to continue."
            : 'Theory picked. Next: connect your evidence.';
        const statusSub = !selected
          ? "Each option lays out the law and what you'd need to prove."
          : !affirmed
            ? "Affirming records that you made the choice; the system presented options."
            : "Each piece of evidence supports one or more elements. We'll suggest matches; you confirm each one.";
        return (
          <div style={{ marginTop: 22, padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', border: `1px solid ${THEME.line}`, display: 'flex', alignItems: 'center', gap: 12 }}>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 13, color: THEME.text, fontWeight: 600, marginBottom: 2 }}>{statusTitle}</div>
              <div style={{ fontSize: 12, color: THEME.textDim, lineHeight: 1.5 }}>{statusSub}</div>
            </div>
            <Btn kind="secondary" onClick={() => onScreen('home')}>Back</Btn>
            <Btn kind="primary" disabled={!ready} onClick={() => onScreen('evidence')}>
              Continue with this theory
            </Btn>
          </div>
        );
      })()}
    </div>
  );
};

/* Per-evidence Atticus affirmation modal */
const V3EvidenceAffirmModal = ({ ev, jx, evidenceReads, elements, onAffirm, onChooseDifferently, onMarkUnrelated, onClose }) => {
  if (!ev) return null;
  const reads = evidenceReads || V3_EVIDENCE_READS;
  const els = elements || V3_ELEMENTS_DEPOSIT_UT;
  const read = reads[ev.id] || { elements: ev.maps, why: "Atticus suggests this piece supports the elements shown below. Confirm the connection or change it." };
  const elNames = read.elements.map(eId => {
    const el = els.find(x => x.id === eId);
    const idx = els.findIndex(x => x.id === eId) + 1;
    return `Element ${idx} ("${el ? el.label : eId}")`;
  }).join(' and ');
  return (
    <div onClick={onClose} style={{
      position: 'absolute', inset: 0, zIndex: 30,
      background: THEME.mode === 'dark' ? 'rgba(18,25,33,0.78)' : 'rgba(40,40,40,0.42)',
      backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        maxWidth: 560, width: '100%', padding: '24px 26px', borderRadius: 16,
        background: THEME.mode === 'dark' ? '#23303D' : '#D7DEE2',
        border: `1px solid ${THEME.line}`, boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
          <div style={{
            width: 26, height: 26, borderRadius: 13,
            background: `linear-gradient(135deg, ${THEME.blue}, ${THEME.accent})`,
            display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
          }}>
            <Icon d={Icons.sparkle} size={13} stroke="#D7DEE2" fill="#D7DEE2"/>
          </div>
          <div style={{ fontSize: 12, color: THEME.text, fontWeight: 600 }}>
            Atticus · <span style={{ color: THEME.textDim, fontWeight: 500 }}>Evidence-element mapping</span>
          </div>
          <div style={{ flex: 1 }}/>
          <div style={{
            fontSize: 11.5, lineHeight: 1.4, fontWeight: 400,
            color: THEME.textDim,
          }}>{jx === 'utah' ? 'sandbox-authorized' : 'educational · NY'}</div>
        </div>
        <div style={{ fontSize: 10.5, color: THEME.textMute, letterSpacing: 1.1, textTransform: 'uppercase', fontWeight: 600, marginTop: 12, marginBottom: 4 }}>Atticus's read on this:</div>
        <div style={{ fontFamily: 'Fraunces', fontSize: 16, color: THEME.text, fontWeight: 500, lineHeight: 1.4, marginBottom: 8, letterSpacing: -0.2 }}>
          "{ev.name}"
        </div>
        <div style={{ fontSize: 13, color: THEME.text, lineHeight: 1.6, marginBottom: 14 }}>
          This document supports {elNames}. Here's why: {read.why}
        </div>
        <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.55, marginBottom: 18, fontStyle: 'italic' }}>
          Is that the right connection?
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          <Btn kind="primary" onClick={() => onAffirm(ev.id)} style={{ width: '100%' }}>
            Affirm this mapping
          </Btn>
          <div style={{ display: 'flex', gap: 8 }}>
            <Btn kind="secondary" onClick={() => onChooseDifferently(ev.id)} style={{ flex: 1 }}>
              Choose differently
            </Btn>
            <Btn kind="secondary" onClick={() => onMarkUnrelated(ev.id)} style={{ flex: 1 }}>
              Mark as unrelated
            </Btn>
          </div>
        </div>

        <div style={{ marginTop: 14, paddingTop: 12, borderTop: `1px solid ${THEME.lineSoft}`, fontSize: 10.5, color: THEME.textMute, fontStyle: 'italic' }}>
          This is Atticus output, not legal advice.
        </div>
      </div>
    </div>
  );
};

/* Evidence + element mapping with per-element affirmation gate (Fix 1) */
const V3Evidence = ({ matter, onScreen }) => {
  const m = V3_MATTERS[matter];
  const ds = getMatterDataset(matter);
  const evidence = ds.evidence;
  const elements = ds.elements;
  const evidenceReads = ds.evidenceReads;
  // affirmStatus: { [evId]: 'affirmed' | 'changed' | 'unrelated' | undefined }
  const [affirmStatus, setAffirmStatus] = React.useState(ds.initialAffirmed);
  const [modalEv, setModalEv] = React.useState(null);
  React.useEffect(() => { setAffirmStatus(ds.initialAffirmed); setModalEv(null); }, [matter]);

  const handleAffirm = (id) => { setAffirmStatus(s => ({ ...s, [id]: 'affirmed' })); setModalEv(null); };
  const handleChange = (id) => { setAffirmStatus(s => ({ ...s, [id]: 'changed' })); setModalEv(null); };
  const handleUnrelated = (id) => { setAffirmStatus(s => ({ ...s, [id]: 'unrelated' })); setModalEv(null); };

  // Element status derived from affirmations
  const elementStatus = (eId) => {
    const supportingEv = evidence.filter(e => e.maps.includes(eId) && affirmStatus[e.id] === 'affirmed');
    return supportingEv.length > 0 ? 'supported' : 'missing';
  };

  const totalAffirmed = Object.values(affirmStatus).filter(v => v === 'affirmed').length;
  const elementsCovered = elements.filter(el => elementStatus(el.id) === 'supported').length;
  const fileReady = elementsCovered === elements.length;

  // Empty state — when a matter has no evidence yet. For newcase, this is
  // also the moment for the mock file-grab demo (Story 0.49 Phase B): show
  // Drive / iCloud connectors that "find" relevant files based on the
  // intake description, then populate the evidence list once a connector
  // is clicked.
  if (!evidence || evidence.length === 0) {
    const isNewcase = matter === 'newcase';
    return (
      <div style={{ padding: '32px 36px', overflow: 'auto', height: '100%' }}>
        <div style={{ marginBottom: 4, fontSize: 11, color: THEME.textMute, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase' }}>Evidence + element mapping</div>
        <div style={{ fontFamily: 'Fraunces', fontSize: 24, color: THEME.text, fontWeight: 500, letterSpacing: -0.3, marginBottom: 6 }}>
          {isNewcase ? "Let's pull in your records." : "No evidence on this case yet."}
        </div>
        <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.55, marginBottom: 22, maxWidth: 720 }}>
          {isNewcase
            ? "Based on what you described, we know what to look for. Connect a service, or upload from your device. We'll map each item to the elements your case needs to prove."
            : "When you add documents, photos, or correspondence to this case, they'll show up here. We'll suggest which element of your legal argument each one supports, and you'll confirm each match before it counts toward filing readiness."}
        </div>
        {isNewcase ? (
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 12, maxWidth: 720, marginBottom: 18 }}>
            {[
              { id: 'gdrive', label: 'Connect Google Drive', sub: 'Looks for paystubs, schedules, contracts', icon: Icons.drive, tone: THEME.success },
              { id: 'icloud', label: 'Connect iCloud', sub: 'Notes, texts, photos with EXIF dates', icon: Icons.cloud, tone: THEME.blue },
              { id: 'upload', label: 'Upload from device', sub: 'Files you already have on this computer', icon: Icons.upload, tone: THEME.warn },
            ].map(opt => (
              <button
                key={opt.id}
                type="button"
                onClick={() => {
                  const m = window.V3_MATTERS && window.V3_MATTERS.newcase;
                  if (m) m.__evidenceImported = true;
                  setAffirmStatus({});
                }}
                onMouseEnter={(e) => { e.currentTarget.style.background = `linear-gradient(135deg, ${opt.tone}22, transparent)`; }}
                onMouseLeave={(e) => { e.currentTarget.style.background = `linear-gradient(135deg, ${opt.tone}10, transparent)`; }}
                style={{
                  textAlign: 'left',
                  padding: '14px 16px', borderRadius: 12, cursor: 'pointer',
                  background: `linear-gradient(135deg, ${opt.tone}10, transparent)`,
                  border: `1px solid ${opt.tone}38`,
                  fontFamily: 'inherit', color: 'inherit',
                  display: 'flex', alignItems: 'flex-start', gap: 12,
                  transition: 'background 0.18s ease, border-color 0.18s ease',
                }}>
                <div style={{
                  width: 36, height: 36, borderRadius: 9, flexShrink: 0,
                  background: opt.tone + '20',
                  border: `1px solid ${opt.tone}40`,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  color: opt.tone,
                }}>
                  <Icon d={opt.icon} size={18} stroke="currentColor" sw={1.6}/>
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13, color: THEME.text, fontWeight: 600, marginBottom: 2 }}>{opt.label}</div>
                  <div style={{ fontSize: 11.5, color: THEME.textDim, lineHeight: 1.4 }}>{opt.sub}</div>
                </div>
              </button>
            ))}
          </div>
        ) : (
          <div style={{ padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}`, maxWidth: 480 }}>
            <div style={{ fontSize: 10.5, color: THEME.textMute, letterSpacing: 1.1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 6 }}>What you can add</div>
            <ul style={{ fontSize: 12.5, color: THEME.text, lineHeight: 1.55, paddingLeft: 18, margin: 0 }}>
              <li>Contracts, leases, written agreements</li>
              <li>Receipts, invoices, payment confirmations</li>
              <li>Photos with their original capture dates</li>
              <li>Emails, texts, or letters with the other party</li>
              <li>Anything else that shows what happened or when</li>
            </ul>
          </div>
        )}
        {isNewcase && (
          <div style={{ padding: '11px 13px', borderRadius: 9, background: THEME.mode === 'dark' ? 'rgba(255,255,255,0.025)' : 'rgba(0,0,0,0.025)', border: `1px dashed ${THEME.line}`, fontSize: 11.5, color: THEME.textDim, lineHeight: 1.5, maxWidth: 720 }}>
            <span style={{ color: THEME.text, fontWeight: 600 }}>Connections are read-only.</span> We index the relevant files. Originals stay in your account. You can disconnect any time.
          </div>
        )}
      </div>
    );
  }

  return (
    <div style={{ padding: '24px 36px', overflow: 'auto', height: '100%', position: 'relative' }}>
      <div style={{ marginBottom: 4, fontSize: 11, color: THEME.textMute, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase' }}>Evidence + element mapping</div>
      <div style={{ fontFamily: 'Fraunces', fontSize: 24, color: THEME.text, fontWeight: 500, letterSpacing: -0.3, marginBottom: 6 }}>
        Now let's connect your evidence to what you need to prove.
      </div>
      <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.55, marginBottom: 14, maxWidth: 760 }}>
        Each piece of evidence supports one or more "elements". The things you need to prove. We'll suggest matches based on what we see, but <span style={{ color: THEME.text, fontWeight: 600 }}>you confirm each one</span>. You're the one making the legal argument; we're just helping you organize it.
      </div>

      {/* Progress strip */}
      <div data-coach="evidence-progress" style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '10px 14px', borderRadius: 10, background: fileReady ? THEME.successSoft : THEME.warnSoft, border: `1px solid ${fileReady ? THEME.success + '40' : THEME.warn + '40'}`, marginBottom: 14 }}>
        <Icon d={fileReady ? Icons.check : Icons.alert} size={14} stroke={fileReady ? THEME.success : THEME.warn}/>
        <div style={{ flex: 1, fontSize: 12.5, color: THEME.text }}>
          <span style={{ fontWeight: 600 }}>{totalAffirmed} of {evidence.length}</span> evidence items affirmed · <span style={{ fontWeight: 600 }}>{elementsCovered} of {elements.length}</span> elements supported
        </div>
        <div style={{ fontSize: 11.5, color: fileReady ? THEME.success : THEME.warn, fontWeight: 600 }}>
          {fileReady ? '✓ File-ready threshold met' : 'Each evidence item must support an element before file-ready'}
        </div>
      </div>

      <div style={{ padding: '10px 12px', borderRadius: 9, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}`, fontSize: 11.5, color: THEME.textDim, marginBottom: 14, display: 'flex', gap: 8, alignItems: 'flex-start' }}>
        <Icon d={Icons.alert} size={13} stroke={THEME.textMute}/>
        <div>
          <span style={{ fontWeight: 600, color: THEME.text }}>EXIF / OCR note:</span> Photos retain their original capture dates and GPS coordinates so you can show the court when and where each was taken. You can review or remove this data before filing.
        </div>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 320px', gap: 16 }}>
        {/* Evidence list */}
        <div data-coach="evidence-list">
          <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 8 }}>Your evidence · {evidence.length} items</div>
          {evidence.map(ev => {
            const status = affirmStatus[ev.id];
            const isAffirmed = status === 'affirmed';
            const isUnrelated = status === 'unrelated';
            const isChanged = status === 'changed';
            const tone = isAffirmed ? THEME.success : isUnrelated ? THEME.textMute : isChanged ? THEME.warn : THEME.warn;
            const statusLabel = isAffirmed ? 'Affirmed' : isUnrelated ? 'Marked unrelated' : isChanged ? 'Re-mapping needed' : 'Awaiting your affirmation';
            return (
              <div key={ev.id} onClick={() => setModalEv(ev)} style={{
                position: 'relative',
                padding: '14px 16px 14px 22px', borderRadius: 12, cursor: 'pointer', marginBottom: 10,
                background: `linear-gradient(135deg, ${tone}14, transparent)`,
                border: `1px solid ${tone}30`,
                opacity: isUnrelated ? 0.6 : 1,
              }}>
                <span aria-hidden="true" style={{
                  position: 'absolute', top: 8, bottom: 8, left: 4,
                  width: 4, borderRadius: '6px 0 0 6px',
                  background: tone,
                }}/>
                <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                  <Icon d={Icons.paperclip} size={14} stroke={THEME.textDim}/>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 12.5, color: THEME.text, fontWeight: 500 }}>{ev.name}</div>
                    <div style={{ fontSize: 10.5, color: THEME.textMute, marginTop: 2 }}>
                      {ev.type}{ev.exif && ` · ${ev.exif}`}
                    </div>
                  </div>
                  <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
                    {!isUnrelated && ev.maps.map(eId => (
                      <span key={eId} style={{ fontSize: 9, fontWeight: 700, padding: '2px 5px', borderRadius: 4, background: isAffirmed ? THEME.success + '22' : THEME.line, color: isAffirmed ? THEME.success : THEME.textDim, letterSpacing: 0.4 }}>
                        E{elements.findIndex(x => x.id === eId) + 1}
                      </span>
                    ))}
                    <span style={{ fontSize: 9.5, fontWeight: 700, padding: '3px 7px', borderRadius: 4, background: tone + '22', color: tone, letterSpacing: 0.4, textTransform: 'uppercase' }}>
                      {isAffirmed ? '✓ AFFIRMED' : isUnrelated ? 'UNRELATED' : isChanged ? 'CHANGED' : 'GATE'}
                    </span>
                  </div>
                </div>
                <div style={{ marginTop: 6, fontSize: 11, color: tone, fontWeight: 500 }}>
                  {isAffirmed ? '✓ ' : ''}{statusLabel}{!status && '. Tap to review Atticus\'s read'}
                </div>
              </div>
            );
          })}
        </div>

        {/* Elements panel */}
        <div data-coach="evidence-elements">
          <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 8 }}>Elements you must prove</div>
          {elements.map((el, i) => {
            const isSupported = elementStatus(el.id) === 'supported';
            const evCount = evidence.filter(e => e.maps.includes(el.id) && affirmStatus[e.id] === 'affirmed').length;
            return (
              <div key={el.id} style={{
                position: 'relative',
                padding: '14px 16px 14px 22px', borderRadius: 12, marginBottom: 10,
                background: `linear-gradient(135deg, ${(isSupported ? THEME.success : THEME.warn)}14, transparent)`,
                border: `1px solid ${(isSupported ? THEME.success : THEME.warn)}30`,
              }}>
                <span aria-hidden="true" style={{
                  position: 'absolute', top: 8, bottom: 8, left: 4,
                  width: 4, borderRadius: '6px 0 0 6px',
                  background: isSupported ? THEME.success : THEME.warn,
                }}/>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
                  <span style={{ fontSize: 9.5, fontWeight: 700, padding: '2px 5px', borderRadius: 3, background: isSupported ? THEME.success : THEME.warn, color: '#D7DEE2' }}>{i + 1}</span>
                  <span style={{ fontSize: 11, color: isSupported ? THEME.success : THEME.warn, fontWeight: 700, letterSpacing: 0.4, textTransform: 'uppercase', display: 'inline-flex', alignItems: 'center', gap: 4 }}>
                    <Icon d={isSupported ? Icons.checkCircle : Icons.warnCircle} size={13} stroke={isSupported ? THEME.success : THEME.warn} sw={1.8}/>
                    {isSupported ? `${evCount} affirmed` : 'missing'}
                  </span>
                </div>
                <div style={{ fontSize: 12, color: THEME.text, fontWeight: 500, lineHeight: 1.4, marginBottom: 4 }}>{el.label}</div>
                <div style={{ fontSize: 11, color: THEME.textDim, lineHeight: 1.45 }}>{el.explain}</div>
              </div>
            );
          })}
        </div>
      </div>

      <div style={{ marginTop: 14 }}><V3Disclosure jx={m.jurisdiction} compact/></div>

      {/* Back + Continue — same pattern as V3Theory. Continue is enabled
          once at least one evidence item has been affirmed (matches the
          demo's pre-imported state). Back returns to Theory. */}
      {onScreen && (
        <div style={{ marginTop: 22, padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', border: `1px solid ${THEME.line}`, display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 13, color: THEME.text, fontWeight: 600, marginBottom: 2 }}>
              {fileReady ? 'Evidence connected. Next: damages.' : (totalAffirmed > 0 ? 'Some evidence affirmed. You can continue or affirm more.' : 'Affirm your evidence connections to continue.')}
            </div>
            <div style={{ fontSize: 12, color: THEME.textDim, lineHeight: 1.5 }}>
              Damages is where the math kicks in. Statutory penalties, actual loss, and what the court can award.
            </div>
          </div>
          <Btn kind="secondary" onClick={() => onScreen('theory')}>Back</Btn>
          <Btn kind="primary" disabled={totalAffirmed === 0} onClick={() => onScreen('damages')}>
            Continue to damages
          </Btn>
        </div>
      )}

      <V3EvidenceAffirmModal
        ev={modalEv}
        jx={m.jurisdiction}
        evidenceReads={evidenceReads}
        elements={elements}
        onAffirm={handleAffirm}
        onChooseDifferently={handleChange}
        onMarkUnrelated={handleUnrelated}
        onClose={() => setModalEv(null)}
      />
    </div>
  );
};

/* Damages. Step 1: theory cards (recommended/costs/grayed) + Step 2: arithmetic table (Fix 5) */
const V3Damages = ({ matter, onScreen, selectedCause }) => {
  const m = V3_MATTERS[matter];
  const ds = getMatterDataset(matter);
  const theoryCards = ds.damagesTheoryCards;
  const [theories, setTheories] = React.useState(ds.damagesInitial);
  React.useEffect(() => { setTheories(ds.damagesInitial); }, [matter]);

  // byCause override: when a primary cause is selected AND the matter has a
  // byCause variant for it, the arithmetic table uses the variant's line
  // items + total instead of the theories-driven calc. Theory cards above
  // remain a multi-select for the user's reference; the final math reflects
  // the primary cause's framing. Falls back to the theories-driven calc
  // when no variant is set.
  const variant = (selectedCause && m.byCause && m.byCause[selectedCause]) || null;
  const items = variant && Array.isArray(variant.damagesLineItems)
    ? variant.damagesLineItems.map(li => ({ ...li, include: !li.deferred && !li.conditional }))
    : ds.damagesItems(theories);
  const total = variant && typeof variant.damagesTotal === 'number'
    ? variant.damagesTotal
    : items.filter(i => i.include).reduce((s, i) => s + i.amount, 0);
  const totalLabel = variant && variant.damagesTotalLabel
    ? variant.damagesTotalLabel
    : `$${total.toLocaleString()}`;
  const introLine = variant && variant.damagesIntroLine
    ? variant.damagesIntroLine
    : ds.damagesIntroLine;

  const [showWhy, setShowWhy] = React.useState(false);

  return (
    <div style={{ padding: '28px 36px', overflow: 'auto', height: '100%', maxWidth: 920 }}>
      <SectionLabel style={{ marginBottom: 4 }}>Damages · Step 1 of 2</SectionLabel>
      <div style={{ fontFamily: 'Fraunces', fontSize: 26, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, marginBottom: 6 }}>
        What damages do you want to seek?
      </div>
      <div style={{ fontSize: 13, color: THEME.textDim, lineHeight: 1.55, marginBottom: 14, maxWidth: 720 }}>
        {introLine}
      </div>

      <V3Atticus topic={ds.atticus.damages.topic} body={ds.atticus.damages.body} jx={m.jurisdiction}/>

      {/* Theory cards */}
      <div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 12 }}>
        {theoryCards.map(card => {
          const checked = card.eligible && theories[card.id];
          const disabled = !card.eligible;
          const borderColor = disabled ? THEME.line : checked ? THEME.blue : THEME.line;
          return (
            <div key={card.id} onClick={() => !disabled && setTheories(t => ({ ...t, [card.id]: !t[card.id] }))} style={{
              padding: '16px 18px', borderRadius: 13, cursor: disabled ? 'default' : 'pointer',
              background: disabled
                ? (THEME.mode === 'dark' ? 'rgba(255,255,255,0.015)' : 'rgba(0,0,0,0.025)')
                : checked
                  ? (THEME.mode === 'dark' ? 'rgba(105,134,191,0.10)' : 'rgba(105,134,191,0.08)')
                  : THEME.mode === 'dark' ? 'rgba(255,255,255,0.025)' : 'rgba(255,255,255,0.55)',
              border: `1.5px solid ${borderColor}`,
              opacity: disabled ? 0.55 : 1,
              position: 'relative',
            }}>
              {card.recommended && (
                // Form-helper-style hint (matches .ds-fld__helper in form-fields.jsx):
                // 11.5px, line-height 1.4, textDim color, regular weight, no italic,
                // no letter-spacing. Replaces the previous chunky floating Pill so
                // the "recommended" signal lives in the same visual language as
                // form active-state helper text.
                <div style={{
                  fontSize: 11.5, lineHeight: 1.4, fontWeight: 400,
                  color: THEME.textDim, marginBottom: 8,
                }}>recommended for your case</div>
              )}
              <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
                {/* Canonical checkbox — matches Btn primary aesthetic.
                   Checked: dark navy bg + white check. Unchecked: transparent + lineStrong. */}
                <div style={{
                  width: 22, height: 22, borderRadius: 6, marginTop: 2, flexShrink: 0,
                  border: `1.5px solid ${checked && !disabled ? 'rgba(242,244,248,0.18)' : THEME.lineStrong}`,
                  background: checked && !disabled ? (THEME.mode === 'dark' ? '#1A2230' : '#2A3640') : 'transparent',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  transition: 'background 0.18s ease, border-color 0.18s ease',
                }}>
                  {checked && !disabled && <Icon d={Icons.check} size={13} stroke="#F2F4F8" sw={3}/>}
                  {disabled && <span style={{ fontSize: 11, color: THEME.textMute }}>-</span>}
                </div>
                <div style={{ flex: 1 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4, flexWrap: 'wrap' }}>
                    {(() => {
                      const tone = card.icon === 'check' ? THEME.success : card.icon === 'warn' ? THEME.warn : THEME.blue;
                      const glyph = card.icon === 'check' ? Icons.checkCircle : card.icon === 'warn' ? Icons.warnCircle : Icons.infoCircle;
                      return <Icon d={glyph} size={18} stroke={tone} sw={1.8}/>;
                    })()}
                    <div style={{ fontFamily: 'Fraunces', fontSize: 17, color: THEME.text, fontWeight: 500, letterSpacing: -0.2 }}>{card.title}</div>
                    <Pill soft tone={THEME.textDim}>{card.cite}</Pill>
                  </div>
                  <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.55, marginBottom: 6 }}>{card.body}</div>
                  {card.trigger && (
                    <div style={{ fontSize: 11.5, color: THEME.textMute, fontStyle: 'italic', lineHeight: 1.5 }}>{card.trigger}</div>
                  )}
                  {disabled && card.readWhy && (
                    <>
                      <div onClick={(e) => { e.stopPropagation(); setShowWhy(w => !w); }} style={{ fontSize: 11.5, color: THEME.blue, fontWeight: 600, marginTop: 6, cursor: 'pointer' }}>
                        {showWhy ? 'Hide why →' : 'Read why →'}
                      </div>
                      {showWhy && (
                        <div style={{ marginTop: 8, padding: '10px 12px', borderRadius: 8, background: THEME.mode === 'dark' ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.04)', border: `1px solid ${THEME.lineSoft}`, fontSize: 11.5, color: THEME.textDim, lineHeight: 1.55 }}>
                          {card.readWhy}
                        </div>
                      )}
                    </>
                  )}
                </div>
                <div style={{ fontFamily: 'Fraunces', fontSize: 22, color: disabled ? THEME.textMute : THEME.text, fontWeight: 500, letterSpacing: -0.5, fontVariantNumeric: 'tabular-nums' }}>
                  {card.headline}
                </div>
              </div>
            </div>
          );
        })}
      </div>

      {/* Step 2. Arithmetic table */}
      <div data-coach="damages-math" style={{ marginTop: 24, paddingTop: 20, borderTop: `1px solid ${THEME.line}` }}>
        <SectionLabel style={{ marginBottom: 4 }}>Damages · Step 2 of 2</SectionLabel>
        <div style={{ fontFamily: 'Fraunces', fontSize: 22, color: THEME.text, fontWeight: 500, letterSpacing: -0.3, marginBottom: 8 }}>
          Here's the math.
        </div>
        <div style={{ fontSize: 12.5, color: THEME.textDim, marginBottom: 12, lineHeight: 1.55 }}>
          This calculation is mechanical. We add and multiply numbers you've already affirmed. No legal judgment is made here. The damage theories you affirmed in the previous step are what make these line items eligible.
        </div>

        <div style={{ borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}`, overflow: 'hidden' }}>
          <div style={{ display: 'grid', gridTemplateColumns: '2fr 2fr 1fr', padding: '10px 16px', fontSize: 10.5, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, background: THEME.mode === 'dark' ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)', borderBottom: `1px solid ${THEME.lineSoft}` }}>
            <span>Item</span><span>Calculation</span><span style={{ textAlign: 'right' }}>Amount</span>
          </div>
          {items.map((it, i) => (
            <div key={i} style={{
              display: 'grid', gridTemplateColumns: '2fr 2fr 1fr',
              padding: '12px 16px', borderBottom: i < items.length - 1 ? `1px solid ${THEME.lineSoft}` : 'none',
              opacity: it.include ? 1 : 0.4,
              fontSize: 12.5, color: THEME.text, alignItems: 'center',
            }}>
              <span style={{ fontWeight: 500 }}>{it.label}</span>
              <span style={{ color: THEME.textDim, fontSize: 11.5 }}>{it.calc}</span>
              <span style={{ textAlign: 'right', fontFamily: 'Fraunces', fontWeight: 500, fontVariantNumeric: 'tabular-nums', fontSize: 14 }}>
                {it.include ? `$${it.amount.toLocaleString()}` : '-'}
              </span>
            </div>
          ))}
          <div style={{ display: 'grid', gridTemplateColumns: '2fr 2fr 1fr', padding: '14px 16px', alignItems: 'center', background: THEME.mode === 'dark' ? 'rgba(105,134,191,0.06)' : 'rgba(105,134,191,0.05)', borderTop: `1px solid ${THEME.line}` }}>
            <span style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 700 }}>Total recovery sought</span>
            <span/>
            <span style={{ textAlign: 'right', fontFamily: 'Fraunces', fontSize: 24, color: THEME.text, fontWeight: 500, fontVariantNumeric: 'tabular-nums', letterSpacing: -0.5 }}>
              {totalLabel}
            </span>
          </div>
        </div>
      </div>

      <div style={{ marginTop: 14 }}><V3Disclosure jx={m.jurisdiction} compact/></div>

      {/* Back + Continue — Story 0.49 Phase C tile-select pattern propagation.
          Same strip as V3Theory + V3Evidence. Continue is enabled once at
          least one damage theory is affirmed (otherwise no claim to assert). */}
      {onScreen && (
        <div style={{ marginTop: 22, padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', border: `1px solid ${THEME.line}`, display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 13, color: THEME.text, fontWeight: 600, marginBottom: 2 }}>
              {total > 0 ? `Total recovery sought: $${total.toLocaleString()}. Next: documents.` : 'Affirm at least one damage theory to continue.'}
            </div>
            <div style={{ fontSize: 12, color: THEME.textDim, lineHeight: 1.5 }}>
              Documents is where your packet comes together: Statement of Claim, exhibits, all the supporting paperwork.
            </div>
          </div>
          <Btn kind="secondary" onClick={() => onScreen('evidence')}>Back</Btn>
          <Btn kind="primary" disabled={total === 0} onClick={() => onScreen('documents')}>
            Continue to documents
          </Btn>
        </div>
      )}
    </div>
  );
};

/* Documents + final affirmation gate */
const V3Documents = ({ matter, onScreen, selectedCause }) => {
  const m = V3_MATTERS[matter];
  const ds = getMatterDataset(matter);
  // byCause override: if a primary cause is selected and the matter has a
  // variant with a statementBody, use the variant's bodyText. Falls back to
  // ds.docs (default content) otherwise.
  const variant = (selectedCause && m.byCause && m.byCause[selectedCause]) || null;
  const d = variant && variant.statementBody
    ? { ...ds.docs, bodyText: variant.statementBody }
    : ds.docs;
  const [opened, setOpened] = React.useState(0);
  // Story 0.49 Phase D — gate "generate packet" on the user actually clicking
  // each doc to read it. `readSet` tracks doc indices the user has clicked.
  // First doc is auto-marked read because it's already showing on mount.
  const [readSet, setReadSet] = React.useState(() => new Set([0]));
  const [affirms, setAffirms] = React.useState(() =>
    Object.fromEntries(d.affirmations.map(a => [a.k, a.k !== d.affirmations[d.affirmations.length - 1].k]))
  );
  React.useEffect(() => {
    setOpened(0);
    setReadSet(new Set([0]));
    setAffirms(Object.fromEntries(d.affirmations.map(a => [a.k, a.k !== d.affirmations[d.affirmations.length - 1].k])));
  }, [matter]);
  const allAffirmed = Object.values(affirms).every(Boolean);
  // Full canonical packet structure — same ordering as the downloadable
  // Filing Packet PDF and the V3Deliverables card. Mirrors the small-claims
  // master-packet template (Ashe Van Lines reference) so every surface tells
  // the same story.
  const docs = [
    { name: 'Cover Page', pages: 1, status: 'Drafted' },
    { name: 'Table of Contents', pages: 1, status: 'Drafted' },
    { name: 'Exhibit Index (Reference Copy)', pages: 1, status: 'Drafted' },
    { name: 'Case Snapshot', pages: 1, status: 'Drafted' },
    { name: 'Case Summary', pages: 2, status: 'Drafted' },
    { name: 'Chronological Timeline of Events', pages: 2, status: 'Drafted' },
    { name: d.formName, pages: 5, status: 'Drafted' },
    { name: 'Additional Documentation', pages: 3, status: 'Drafted' },
    { name: 'Key Admissions and Undisputed Facts', pages: 1, status: 'Drafted' },
    { name: `Exhibit Index and Descriptions (${ds.evidence.length} exhibits)`, pages: 2, status: 'Drafted' },
  ];
  const allRead = readSet.size === docs.length;
  // Per-doc mock preview content — drives the right-hand pane based on
  // `opened` index. Real implementation would render the actual document.
  const docPreview = (idx) => {
    const exhibits = (ds.evidence || []).map((e, i) => `Exhibit ${i + 1}. ${e.label || e.kind || 'Document'}`);
    if (idx === 0) return { kind: 'cover', title: 'Filing Packet · Cover',
      lines: [d.plaintiff, 'v.', d.defendant, '', d.courtHeader, '', d.formName, d.amountLine] };
    if (idx === 1) return { kind: 'toc', title: 'Table of Contents',
      lines: docs.map((dx, i) => `${i + 1}.  ${dx.name}  ·  ${dx.pages} pg`) };
    if (idx === 2) return { kind: 'exhibit-ref', title: 'Exhibit Index (Reference Copy)',
      lines: exhibits.length ? exhibits : ['No exhibits attached.'] };
    if (idx === 3) return { kind: 'snapshot', title: 'Case Snapshot',
      lines: [`Plaintiff: ${d.plaintiff}`, `Defendant: ${d.defendant}`, `Amount sought: ${d.amountLine}`, `Statute: ${d.statuteLine}`] };
    if (idx === 4) return { kind: 'summary', title: 'Case Summary', body: d.bodyText };
    if (idx === 5) return { kind: 'timeline', title: 'Chronological Timeline of Events',
      lines: ['Day 0  ·  Employment began', 'Week 1-8  ·  Worked 50 hrs/wk · paid straight time only',
              'Week 4  ·  Manager texted "clock in after setup" (Exhibit 5)',
              'Week 8  ·  Employment ended', 'Day +14  ·  Demand letter sent (no response)',
              'Day +30  ·  Filed Statement of Claim'] };
    if (idx === 6) return { kind: 'soc', title: d.formName, body: d.bodyText };
    if (idx === 7) return { kind: 'addl', title: 'Additional Documentation',
      lines: ['Demand letter sent to employer (no response)',
              'Notice of Filing (served separately on the day of filing)',
              'Wage statement reconstruction (calculations from timecards + paystubs)'] };
    if (idx === 8) return { kind: 'admissions', title: 'Key Admissions and Undisputed Facts',
      lines: ['Plaintiff was employed by Defendant from Jan 6–Feb 28, 2026.',
              'Plaintiff\'s regular hourly rate was $20/hr.',
              'Plaintiff\'s schedules show 50-hour weeks.',
              'Plaintiff was paid straight-time wages only (no overtime premium reflected on paystubs).'] };
    if (idx === 9) return { kind: 'exhibit-full', title: `Exhibit Index (${ds.evidence.length} exhibits)`,
      lines: exhibits.length ? exhibits.map((e, i) => `${e}  ·  ${(ds.evidence[i] && ds.evidence[i].kind) || ''}`) : ['No exhibits attached.'] };
    return { kind: 'soc', title: d.formName, body: d.bodyText };
  };
  return (
    <div style={{ padding: '28px 36px', overflow: 'auto', height: '100%' }}>
      <SectionLabel style={{ marginBottom: 4 }}>Documents · Final review</SectionLabel>
      <div style={{ fontFamily: 'Fraunces', fontSize: 26, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, marginBottom: 12 }}>
        Read each document. Affirm what it says.
      </div>

      <div data-coach="documents-list" style={{ display: 'grid', gridTemplateColumns: '1fr 1.4fr', gap: 16 }}>
        <div>
          {docs.map((dx, i) => {
            const isRead = readSet.has(i);
            return (
              <div key={i} onClick={() => { setOpened(i); setReadSet(s => new Set(s).add(i)); }} style={{
                padding: '12px 14px', borderRadius: 10, marginBottom: 8, cursor: 'pointer',
                background: opened === i ? (THEME.mode === 'dark' ? 'rgba(105,134,191,0.10)' : 'rgba(105,134,191,0.08)') : THEME.mode === 'dark' ? 'rgba(255,255,255,0.025)' : 'rgba(255,255,255,0.55)',
                border: `1px solid ${opened === i ? THEME.blue : THEME.line}`,
              }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                  <Icon d={Icons.doc} size={14} stroke={THEME.blue}/>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontSize: 12.5, color: THEME.text, fontWeight: 600 }}>{dx.name}</div>
                    <div style={{ fontSize: 10.5, color: THEME.textMute, marginTop: 2 }}>{dx.pages} pages · {dx.status}</div>
                  </div>
                  {isRead ? (
                    <span className="material-symbols-rounded" style={{ fontSize: 16, color: THEME.success, fontVariationSettings: "'FILL' 1" }}>check_circle</span>
                  ) : (
                    <Icon d={Icons.eye} size={13} stroke={THEME.textDim}/>
                  )}
                </div>
              </div>
            );
          })}
        </div>

        {/* Mock document preview — content varies by `opened` index. */}
        {(() => {
          const p = docPreview(opened);
          return (
            <div style={{ padding: 18, borderRadius: 12, background: THEME.mode === 'dark' ? '#23303D' : '#FBFAF5', border: `1px solid ${THEME.line}`, fontFamily: 'Fraunces', minHeight: 320 }}>
              <div style={{ textAlign: 'center', fontSize: 11, color: THEME.textMute, marginBottom: 8, letterSpacing: 1, textTransform: 'uppercase' }}>{d.courtHeader}</div>
              <div style={{ textAlign: 'center', fontSize: 16, color: THEME.text, fontWeight: 600, marginBottom: 16 }}>{p.title}</div>
              {p.kind === 'snapshot' || p.kind === 'cover' ? (
                <div style={{ fontSize: 12, color: THEME.text, lineHeight: 1.85, fontFamily: 'Fraunces' }}>
                  {p.lines.map((ln, i) => <div key={i} style={{ textAlign: p.kind === 'cover' ? 'center' : 'left' }}>{ln}</div>)}
                </div>
              ) : (p.kind === 'soc' || p.kind === 'summary') ? (
                <>
                  <div style={{ display: 'grid', gridTemplateColumns: '90px 1fr', gap: 6, fontSize: 12, color: THEME.text, lineHeight: 1.7 }}>
                    <span style={{ color: THEME.textMute }}>Plaintiff:</span><span>{d.plaintiff}</span>
                    <span style={{ color: THEME.textMute }}>Defendant:</span><span>{d.defendant}</span>
                    <span style={{ color: THEME.textMute }}>Amount:</span><span>{d.amountLine}</span>
                    <span style={{ color: THEME.textMute }}>Statute:</span><span>{d.statuteLine}</span>
                  </div>
                  <div style={{ marginTop: 14, paddingTop: 10, fontSize: 11.5, color: THEME.textDim, lineHeight: 1.6 }}>
                    {p.body}
                  </div>
                </>
              ) : (
                <ol style={{ margin: 0, paddingLeft: p.kind === 'toc' ? 0 : 18, listStyle: p.kind === 'toc' ? 'none' : 'decimal', fontSize: 12, color: THEME.text, lineHeight: 1.85 }}>
                  {p.lines.map((ln, i) => <li key={i} style={{ marginBottom: 4 }}>{ln}</li>)}
                </ol>
              )}
              <div style={{ marginTop: 16, fontSize: 9.5, color: THEME.textMute, textAlign: 'center', paddingTop: 8 }}>
                Hello Court is not a law firm. This document was prepared by the user with platform tools.
              </div>
            </div>
          );
        })()}
      </div>

      {/* Read-progress + affirmation gate. Affirmation list is disabled
          (greyed) until every doc has been clicked at least once. */}
      <div style={{ marginTop: 18, padding: '16px 18px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1.5px solid ${THEME.warn + '40'}` }}>
        <div style={{ fontSize: 11, color: THEME.warnText, letterSpacing: 1.2, fontWeight: 700, textTransform: 'uppercase', marginBottom: 4, display: 'flex', alignItems: 'center', gap: 8 }}>
          <span>Affirmation gate</span>
          <Pill soft tone={allRead ? THEME.success : THEME.warn}>{readSet.size} of {docs.length} read</Pill>
        </div>
        <div style={{ fontFamily: 'Fraunces', fontSize: 17, color: THEME.text, fontWeight: 500, marginBottom: 10 }}>
          {allRead ? 'Affirm each claim before we generate the packet:' : 'Open each document on the left first. Then affirm.'}
        </div>
        <div style={{ opacity: allRead ? 1 : 0.45, pointerEvents: allRead ? 'auto' : 'none' }}>
        {d.affirmations.map(x => (
          <div key={x.k} onClick={() => setAffirms(a => ({ ...a, [x.k]: !a[x.k] }))} style={{
            display: 'flex', alignItems: 'flex-start', gap: 10, padding: '8px 0', cursor: 'pointer',
          }}>
            <div style={{
              width: 20, height: 20, borderRadius: 5, marginTop: 1, flexShrink: 0,
              border: `1.5px solid ${affirms[x.k] ? THEME.blue : THEME.lineStrong}`,
              background: affirms[x.k] ? THEME.blue : 'transparent',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}>{affirms[x.k] && <Icon d={Icons.check} size={12} stroke="#D7DEE2" sw={3}/>}</div>
            <div style={{ flex: 1, fontSize: 12.5, color: THEME.text }}>{x.label}</div>
          </div>
        ))}
        </div>

        <div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 12 }}>
          {!allRead && <span style={{ fontSize: 11, color: THEME.warnText }}>Open the {docs.length - readSet.size} remaining {docs.length - readSet.size === 1 ? 'document' : 'documents'} to unlock affirmations.</span>}
          {allRead && !allAffirmed && <span style={{ fontSize: 11, color: THEME.textDim }}>Tick all three to enable.</span>}
        </div>
      </div>

      <div style={{ marginTop: 12 }}><V3Disclosure jx={m.jurisdiction} compact/></div>

      {/* Back + Continue — Story 0.49 Phase C. Continue is gated on all
          affirmations being checked. Replaces the inline "Generate packet"
          div with a real Btn that advances to filing instructions. */}
      {onScreen && (
        <div style={{ marginTop: 22, padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', border: `1px solid ${THEME.line}`, display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 13, color: THEME.text, fontWeight: 600, marginBottom: 2 }}>
              {!allRead
                ? `Open ${docs.length - readSet.size} more ${docs.length - readSet.size === 1 ? 'document' : 'documents'} to continue.`
                : (allAffirmed
                    ? 'All read and affirmed. Generate packet and continue to filing.'
                    : 'Tick all affirmations to generate the packet.')}
            </div>
            <div style={{ fontSize: 12, color: THEME.textDim, lineHeight: 1.5 }}>
              Filing is where you learn how to deliver the packet to the court and serve the other side.
            </div>
          </div>
          <Btn kind="secondary" onClick={() => onScreen('damages')}>Back</Btn>
          <Btn kind="primary" disabled={!allRead || !allAffirmed} onClick={() => onScreen('filing')}>
            Generate &amp; continue
          </Btn>
        </div>
      )}
    </div>
  );
};

/* Filing instructions */
const V3Filing = ({ matter, onScreen }) => {
  const m = V3_MATTERS[matter];
  const ds = getMatterDataset(matter);
  const f = ds.filing;
  const [service, setService] = React.useState('');
  React.useEffect(() => { setService(''); }, [matter]);
  return (
    <div style={{ padding: '28px 36px', overflow: 'auto', height: '100%', maxWidth: 880 }}>
      <SectionLabel style={{ marginBottom: 4 }}>Filing &amp; service</SectionLabel>
      <div style={{ fontFamily: 'Fraunces', fontSize: 26, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, marginBottom: 12 }}>
        Where to file, what it costs, how to serve.
      </div>

      <div data-coach="filing-summary" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12, marginBottom: 18 }}>
        {[
          { l: 'Where to file', v: f.courtName, sub: f.courtAddress },
          { l: 'Filing fee', v: f.fee, sub: f.feeNote },
          { l: 'Estimated hearing', v: f.hearingTimeline, sub: f.hearingNote },
        ].map((s, i) => (
          <div key={i} style={{ padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}` }}>
            <div style={{ fontSize: 10.5, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600 }}>{s.l}</div>
            <div style={{ fontFamily: 'Fraunces', fontSize: 18, color: THEME.text, fontWeight: 500, marginTop: 4 }}>{s.v}</div>
            <div style={{ fontSize: 11, color: THEME.textDim, marginTop: 4, lineHeight: 1.45 }}>{s.sub}</div>
          </div>
        ))}
      </div>

      <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 8 }}>How will you serve the defendant?</div>
      <div style={{ fontSize: 12.5, color: THEME.textDim, marginBottom: 10, lineHeight: 1.55 }}>
        "Serve" means delivering a copy of your Statement of Claim to the other party so they know about the case. {f.serviceIntro}
      </div>
      {f.serviceOptions.map(opt => (
        <div key={opt.id} onClick={() => setService(opt.id)} style={{
          padding: '12px 14px', borderRadius: 10, marginBottom: 6, cursor: 'pointer',
          background: service === opt.id ? (THEME.mode === 'dark' ? 'rgba(105,134,191,0.10)' : 'rgba(105,134,191,0.08)') : THEME.mode === 'dark' ? 'rgba(255,255,255,0.025)' : 'rgba(255,255,255,0.55)',
          border: `1px solid ${service === opt.id ? THEME.blue : THEME.line}`,
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <div style={{
              /* Canonical radio — matches Btn primary aesthetic.
                 Selected: dark navy fill + small white dot. Unselected: transparent. */
              width: 18, height: 18, borderRadius: 9, flexShrink: 0,
              border: `1.5px solid ${service === opt.id ? 'rgba(242,244,248,0.18)' : THEME.lineStrong}`,
              background: service === opt.id ? (THEME.mode === 'dark' ? '#1A2230' : '#2A3640') : 'transparent',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              transition: 'background 0.18s ease, border-color 0.18s ease',
            }}>{service === opt.id && <div style={{ width: 6, height: 6, borderRadius: 3, background: '#F2F4F8' }}/>}</div>
            <div style={{ flex: 1 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <span style={{ fontSize: 13, color: THEME.text, fontWeight: 600 }}>{opt.label}</span>
                {opt.recommend && (
                  <span style={{
                    fontSize: 11.5, lineHeight: 1.4, fontWeight: 400,
                    color: THEME.textDim,
                  }}>recommended for your case</span>
                )}
              </div>
              <div style={{ fontSize: 11.5, color: THEME.textDim, marginTop: 3, lineHeight: 1.4 }}>{opt.note}</div>
            </div>
            <div style={{ fontFamily: 'Fraunces', fontSize: 15, color: THEME.text, fontWeight: 500 }}>{opt.cost}</div>
          </div>
        </div>
      ))}

      {/* Back + Continue — Story 0.49 Phase C. Continue is gated on the user
          having picked a service method (the load-bearing decision on this
          screen). */}
      {onScreen && (
        <div style={{ marginTop: 22, padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', border: `1px solid ${THEME.line}`, display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 13, color: THEME.text, fontWeight: 600, marginBottom: 2 }}>
              {service ? 'Service method picked. Next: courtroom prep.' : 'Pick a service method to continue.'}
            </div>
            <div style={{ fontSize: 12, color: THEME.textDim, lineHeight: 1.5 }}>
              Courtroom prep is what to expect at the hearing: what the judge does, when you speak, what to bring.
            </div>
          </div>
          <Btn kind="secondary" onClick={() => onScreen('documents')}>Back</Btn>
          <Btn kind="primary" disabled={!service} onClick={() => onScreen('courtroom')}>
            Continue to courtroom
          </Btn>
        </div>
      )}

      <div style={{ marginTop: 14 }}><V3Disclosure jx={m.jurisdiction} compact/></div>
    </div>
  );
};

/* Courtroom prep. Utah track (sandbox-authorized scripts) vs NY track (procedural framing only per UPL discipline) */
const V3Courtroom = ({ matter, onScreen, selectedCause }) => {
  const m = V3_MATTERS[matter];
  const isUtah = m.jurisdiction === 'utah';
  // byCause override: when a primary cause is selected, the opening tab
  // renders the variant's openingLeadIn instead of Maria's hardcoded text.
  // Also surfaces the opening tab for NY (with an educational disclaimer)
  // so Sarah's walkthrough can land step 18 on the opening anchor.
  const variant = (selectedCause && m.byCause && m.byCause[selectedCause]) || null;
  const [tab, setTab] = React.useState(isUtah || variant ? 'opening' : 'expect');
  React.useEffect(() => { setTab(isUtah || variant ? 'opening' : 'expect'); }, [matter, selectedCause]);
  return (
    <div style={{ padding: '24px 36px', overflow: 'auto', height: '100%' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6, flexWrap: 'wrap' }}>
        <span style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase' }}>Courtroom prep</span>
        <span style={{ fontSize: 10, fontWeight: 700, padding: '2px 7px', borderRadius: 4, background: isUtah ? 'rgba(91,167,115,0.18)' : THEME.blueSoft, color: isUtah ? THEME.success : THEME.blue, letterSpacing: 0.4 }}>
          {isUtah ? 'SANDBOX-AUTHORIZED · Utah Office of Legal Services Innovation' : 'EDUCATIONAL · NY'}
        </span>
      </div>
      <div style={{ fontFamily: 'Fraunces', fontSize: 26, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, marginBottom: 6 }}>
        {isUtah ? 'Walk into court ready.' : 'What to expect in court.'}
      </div>
      <div style={{ fontSize: 12.5, color: THEME.textDim, marginBottom: 14, maxWidth: 700, lineHeight: 1.55 }}>
        {isUtah
          ? 'Loose script for your hearing. The judge will run the show. You don\'t recite this verbatim. Use it as a backbone for what to cover.'
          : 'No script. NY UPL discipline doesn\'t allow case-specific advocacy coaching from us. What follows is courtroom etiquette and what judges typically ask in auto-repair / consumer-protection cases of this kind, generic-to-case-type.'}
      </div>

      <div style={{ display: 'flex', gap: 4, borderBottom: `1px solid ${THEME.line}`, marginBottom: 14 }}>
        {(isUtah ? [
          { v: 'opening', l: 'Opening' },
          { v: 'evidence', l: 'Evidence walk' },
          { v: 'closing', l: 'Closing' },
          { v: 'qa', l: 'Q&A prep · 12 questions' },
          { v: 'behavior', l: 'Courtroom etiquette' },
        ] : variant ? [
          { v: 'opening', l: 'Opening (starting point)' },
          { v: 'expect', l: 'What to expect' },
          { v: 'qa-generic', l: 'Typical judge questions · 8' },
          { v: 'behavior', l: 'Courtroom etiquette' },
        ] : [
          { v: 'expect', l: 'What to expect' },
          { v: 'qa-generic', l: 'Typical judge questions · 8' },
          { v: 'behavior', l: 'Courtroom etiquette' },
        ]).map(t => {
          const a = tab === t.v;
          return (
            <div key={t.v} onClick={() => setTab(t.v)} style={{
              padding: '10px 14px', cursor: 'pointer', fontSize: 12.5, fontWeight: 600,
              color: a ? THEME.text : THEME.textDim,
              borderBottom: `2px solid ${a ? THEME.blue : 'transparent'}`, marginBottom: -1,
            }}>{t.l}</div>
          );
        })}
      </div>

      {tab === 'opening' && (
        <div data-coach="courtroom-opening">
          <V3Atticus
            topic={variant ? `Opening: ${variant.label || 'starting point'}` : "Opening statement script"}
            jx={m.jurisdiction}
            body={variant
              ? `Keep it under 90 seconds. This is a starting point reflecting your primary claim. NY UPL discipline means we can't tune this further for you; treat it as scaffolding to rehearse and adjust in your own words.`
              : "Keep it under 60 seconds. Judge wants to know: who you are, what happened, what you want. Specific dates and the statute carry weight; outrage and adjectives don't."}/>
          <div style={{ marginTop: 12, padding: 18, borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}`, fontFamily: 'Fraunces', fontSize: 14.5, color: THEME.text, lineHeight: 1.7 }}>
            {variant
              ? variant.openingLeadIn
              : '"Your Honor, my name is Maria Vargas. I rented an apartment from Westhaven Properties for almost two years. When I moved out on March 15th, I gave proper notice, returned the keys, and left the unit clean. Utah law gives a landlord 30 days to either return my security deposit or send me a written explanation of why they\'re keeping any of it. It\'s now been more than 30 days. They\'ve sent neither. I\'m here under Utah Code 57-17-3, asking the court for the return of my $1,800 deposit, the statutory 2× damages the law allows, and the $60 filing fee. Thank you."'}
          </div>
        </div>
      )}
      {tab === 'qa' && (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 10 }}>
          {[
            'Did you give written notice?', 'How did you return the keys?', 'Was there a move-out walkthrough?',
            'Have you tried to contact the landlord since you moved out?', 'Do you have the original deposit receipt?',
            'Did the lease address security-deposit handling?', 'Did the landlord ever inspect the unit while you lived there?',
            'Why are you asking for double damages?', 'What address did you give the landlord for the deposit return?',
            'Are there other tenants with the same complaint?', 'Did you cause any damage to the unit?',
            'Have you spoken with a lawyer?',
          ].map((q, i) => (
            <div key={i} style={{ padding: '12px 14px', borderRadius: 10, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}` }}>
              <div style={{ fontSize: 10.5, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 4 }}>Q{i + 1}</div>
              <div style={{ fontSize: 12.5, color: THEME.text, fontWeight: 500 }}>{q}</div>
            </div>
          ))}
        </div>
      )}
      {tab === 'behavior' && (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 10 }}>
          {[
            { l: 'Address the judge as "Your Honor"', t: 'Always. Even if they say "call me Bob." It signals respect for the process.' },
            { l: 'Stand when the judge enters and exits', t: 'Everyone in the room stands. Don\'t miss this.' },
            { l: 'Speak slowly. Pause between facts.', t: 'Court reporters and judges both need pacing. If you feel rushed, slow down on purpose.' },
            { l: 'When confused, say "May I clarify, Your Honor?"', t: 'Better than guessing. Judges respect litigants who admit uncertainty.' },
            { l: 'Don\'t interrupt the other side', t: 'You\'ll get your turn. Take notes; raise points when the judge asks for your response.' },
            { l: 'When you\'re done, stop talking', t: 'Resist the urge to keep adding. End on your strongest sentence.' },
          ].map((x, i) => (
            <div key={i} style={{ padding: '12px 14px', borderRadius: 10, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}`, borderLeft: `3px solid ${THEME.blue}` }}>
              <div style={{ fontSize: 12.5, color: THEME.text, fontWeight: 600, marginBottom: 3 }}>{x.l}</div>
              <div style={{ fontSize: 11.5, color: THEME.textDim, lineHeight: 1.5 }}>{x.t}</div>
            </div>
          ))}
        </div>
      )}
      {(tab === 'evidence' || tab === 'closing') && (
        <div style={{ padding: 18, borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}`, fontSize: 13, color: THEME.textDim, lineHeight: 1.6 }}>
          {tab === 'evidence' ? 'Walk the judge through your 7 exhibits in numbered order. State each one, what it shows, and which element it proves. Atticus has prepared per-exhibit narration. Tap any exhibit on the Evidence page to see it.' : 'Restate the legal claim, restate what the law allows, restate what you\'re asking for. Three sentences. Done.'}
        </div>
      )}
      {tab === 'expect' && (
        <div>
          <V3Atticus topic="What to expect (NY procedural framing)" jx={m.jurisdiction} body="Per NY UPL discipline, we don't write you a script. Instead, here's how NYC Civil Court small-claims hearings typically run. Generic-to-case-type, not specific-to-your-case. The judge will guide the order; you respond when asked."/>
          <div style={{ marginTop: 12, padding: 18, borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}`, fontSize: 13, color: THEME.text, lineHeight: 1.6 }}>
            <div style={{ fontFamily: 'Fraunces', fontSize: 16, color: THEME.text, fontWeight: 500, marginBottom: 8 }}>What happens in the courtroom</div>
            <div style={{ color: THEME.textDim, marginBottom: 10 }}>
              Most NYC Civil Court small-claims hearings run 15–20 minutes. The judge will have your packet (Statement of Claim + exhibits). Sessions are typically held in the evenings (5:30–9 PM) at the courthouse. Be prepared to wait for your case to be called.
            </div>
            <div style={{ fontFamily: 'Fraunces', fontSize: 16, color: THEME.text, fontWeight: 500, marginBottom: 8, marginTop: 14 }}>Order of events (typical)</div>
            <ol style={{ margin: 0, paddingLeft: 18, color: THEME.textDim, fontSize: 12.5, lineHeight: 1.7 }}>
              <li>Court calls your case. You and the defendant come forward.</li>
              <li>Judge asks the plaintiff (you) to summarize the case briefly.</li>
              <li>Judge asks the defendant for their response.</li>
              <li>Judge asks clarifying questions of both sides.</li>
              <li>Judge may request to see specific exhibits.</li>
              <li>Judge announces decision then-and-there OR reserves decision for written ruling.</li>
            </ol>
            <div style={{ fontFamily: 'Fraunces', fontSize: 16, color: THEME.text, fontWeight: 500, marginBottom: 8, marginTop: 14 }}>How to present your evidence</div>
            <div style={{ color: THEME.textDim }}>
              Bring 3 paper copies of every exhibit (one for the judge, one for the defendant, one for you). When the judge asks about an exhibit, hand it to the bailiff or court officer who will pass it up. Don't try to walk it to the bench yourself.
            </div>
          </div>
        </div>
      )}
      {tab === 'qa-generic' && (
        <div>
          <V3Atticus topic="Typical judge questions" jx={m.jurisdiction} body="These are the questions a NYC Civil Court judge typically asks in auto-repair / consumer-protection small-claims cases. Generic-to-case-type. Not specific-to-your-facts. Use them as a thinking aid, not a script."/>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 10, marginTop: 12 }}>
            {[
              'Did you receive a written estimate before any work was performed?',
              'What was the total amount on the original estimate?',
              'Did you authorize any additional work in writing?',
              'How did the shop communicate the additional work to you?',
              'When did you first see the final invoice?',
              'Have you tried to resolve this directly with the shop?',
              'Do you have evidence of the disputed work?',
              'What outcome are you seeking from the court?',
            ].map((q, i) => (
              <div key={i} style={{ padding: '12px 14px', borderRadius: 10, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}` }}>
                <div style={{ fontSize: 10.5, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 4 }}>Q{i + 1}</div>
                <div style={{ fontSize: 12.5, color: THEME.text, fontWeight: 500 }}>{q}</div>
              </div>
            ))}
          </div>
        </div>
      )}

      <div style={{ marginTop: 14 }}><V3Disclosure jx={m.jurisdiction} compact/></div>

      {/* Back to filing — Story 0.49 Phase C. Courtroom is the final step
          in the file-from-scratch flow; no Continue beyond this. */}
      {onScreen && (
        <div style={{ marginTop: 22, padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', border: `1px solid ${THEME.line}`, display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 13, color: THEME.text, fontWeight: 600, marginBottom: 2 }}>
              Your packet is ready and you know what to expect at the hearing.
            </div>
            <div style={{ fontSize: 12, color: THEME.textDim, lineHeight: 1.5 }}>
              From here you can return to your case home or revisit any earlier step.
            </div>
          </div>
          <Btn kind="secondary" onClick={() => onScreen('filing')}>Back</Btn>
          <Btn kind="primary" onClick={() => onScreen('home')}>Back to case home</Btn>
        </div>
      )}
    </div>
  );
};

/* Lawyer pitch packet. Tier 2 (Daniel) */
const V3LawyerPitch = ({ matter }) => {
  const m = V3_MATTERS[matter];
  const auditId = 'AL-7K3M9P2QX1FJ';
  return (
    <div style={{ padding: '24px 36px', overflow: 'auto', height: '100%' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6 }}>
        <V3TierBadge tier={2}/>
        <span style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase' }}>Lawyer-pitch packet</span>
      </div>
      <div style={{ fontFamily: 'Fraunces', fontSize: 26, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, marginBottom: 4 }}>
        Your case is bigger than what we self-file.
      </div>
      <div style={{ fontSize: 12.5, color: THEME.textDim, marginBottom: 14, maxWidth: 720, lineHeight: 1.55 }}>
        Above the $15,000 small-claims cap and with a threatened counterclaim, this case needs a lawyer. We've packaged everything you've organized into a structured packet you can send to attorneys, with watermarks and an access log so you know who opened it.
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 16 }}>
        {/* Packet preview with watermark */}
        <div style={{ position: 'relative', padding: 22, borderRadius: 12, background: THEME.mode === 'dark' ? '#23303D' : '#FBFAF5', border: `1px solid ${THEME.line}`, overflow: 'hidden', minHeight: 380 }}>
          {/* Watermark */}
          <div style={{
            position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
            transform: 'rotate(-30deg)', pointerEvents: 'none',
            fontFamily: 'Fraunces', fontSize: 22, color: THEME.blue, opacity: 0.08, letterSpacing: 4, textAlign: 'center', lineHeight: 1.6,
          }}>
            Recipient: Hannah Reed, Esq.<br/>Audit ID: {auditId}<br/>Recipient: Hannah Reed, Esq.<br/>Audit ID: {auditId}
          </div>
          <div style={{ position: 'relative' }}>
            <div style={{ fontSize: 10, color: THEME.textMute, letterSpacing: 1.2, textTransform: 'uppercase', fontWeight: 600, marginBottom: 4 }}>Hello Court Lawyer-Pitch Packet · Watermarked</div>
            <div style={{ fontFamily: 'Fraunces', fontSize: 20, color: THEME.text, fontWeight: 600, marginBottom: 4 }}>Romero v. Mountain View Homeowners Assoc.</div>
            <div style={{ fontSize: 11, color: THEME.textDim, marginBottom: 12 }}>Daniel Romero · Provo, UT · Independent contractor</div>

            <div style={{ padding: '8px 10px', borderRadius: 6, background: THEME.warn + '22', border: `1px solid ${THEME.warn}40`, fontSize: 11, color: THEME.text, marginBottom: 10 }}>
              <span style={{ fontWeight: 700, color: THEME.warnText }}>USER ASK:</span> Take the case (full representation). Contingency considered
            </div>

            <div style={{ display: 'grid', gridTemplateColumns: '110px 1fr', gap: '4px 12px', fontSize: 12, color: THEME.text, lineHeight: 1.6 }}>
              <span style={{ color: THEME.textMute }}>Amount:</span><span>$24,000 (kitchen remodel · Jan–Mar 2026)</span>
              <span style={{ color: THEME.textMute }}>Posture:</span><span>Plaintiff · contract claim · counterclaim threatened</span>
              <span style={{ color: THEME.textMute }}>Forum:</span><span>Utah District Court (above $15K SCC cap)</span>
              <span style={{ color: THEME.textMute }}>Statute:</span><span>Utah contract law · § 78A-5</span>
              <span style={{ color: THEME.textMute }}>Documents:</span><span>11-item structured bundle (signed contract, change orders, photo evidence, pay history, communications log, counterclaim threat)</span>
              <span style={{ color: THEME.textMute }}>Theory:</span><span>Breach of contract. Full performance, partial payment, no documented basis for withholding</span>
              <span style={{ color: THEME.textMute }}>Defenses noted:</span><span>HOA alleges fraudulent invoicing; user disputes. See Exhibit 7 (text logs)</span>
            </div>

            <div style={{ marginTop: 12, padding: '10px 12px', borderRadius: 6, background: THEME.mode === 'dark' ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.04)', fontSize: 10.5, color: THEME.textMute, lineHeight: 1.55 }}>
              <div style={{ display: 'flex', gap: 6, alignItems: 'flex-start', marginBottom: 6 }}>
                <Icon d={Icons.alert} size={11} stroke={THEME.warn}/>
                <div>
                  <span style={{ color: THEME.text, fontWeight: 600 }}>Voice mode override:</span> Peer / Professional (locked).{' '}
                  <span style={{ fontStyle: 'italic' }}>
                    This packet is for attorney readers, not for you. Your in-app voice setting (Plain &amp; Kind 12th-grade) doesn't apply here. Attorneys need terms-of-art, statute citations, and procedural posture in shorthand. The packet stays in Peer / Professional regardless of your settings.
                  </span>
                </div>
              </div>
              <span style={{ color: THEME.text, fontWeight: 600 }}>Disclaimer:</span> Hello Court is not a law firm. This packet was prepared by the user with platform tools. The user's case-specific decisions and affirmations are documented in the audit trail.
            </div>
          </div>
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {/* Access log */}
          <div style={{ padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}` }}>
            <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1.1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 8 }}>Access log</div>
            <div style={{ fontSize: 12, color: THEME.text, marginBottom: 8 }}>Sent to <span style={{ fontWeight: 600 }}>3 attorneys</span> · <span style={{ fontWeight: 600 }}>5 views</span> · Last opened <span style={{ fontWeight: 600 }}>2 days ago</span></div>
            {[
              { who: 'Hannah Reed, Esq.', firm: 'Reed Law Group', last: '2d ago', views: 3 },
              { who: 'Marco Vélez, Esq.', firm: 'Vélez & Tran PLLC', last: '4d ago', views: 1 },
              { who: 'Aisha Okonkwo, Esq.', firm: 'Okonkwo Solo', last: '6d ago', views: 1 },
            ].map((r, i) => (
              <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 0', borderTop: i === 0 ? 'none' : `1px solid ${THEME.lineSoft}` }}>
                <Avatar initials={r.who.split(' ').map(p => p[0]).join('').slice(0, 2)} size={26}/>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 11.5, color: THEME.text, fontWeight: 600 }}>{r.who}</div>
                  <div style={{ fontSize: 10, color: THEME.textMute }}>{r.firm} · {r.views} view{r.views > 1 ? 's' : ''} · {r.last}</div>
                </div>
              </div>
            ))}
            <div style={{ marginTop: 6, fontSize: 11, color: THEME.blue, fontWeight: 600, cursor: 'pointer' }}>See full activity log →</div>
          </div>

          {/* User ask selector */}
          <div style={{ padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}` }}>
            <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1.1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 8 }}>What are you asking the attorney for?</div>
            {['Review only', 'Advise', 'Take the case', 'Limited-scope'].map((x, i) => (
              <div key={i} style={{
                padding: '7px 10px', borderRadius: 7, marginBottom: 4,
                background: i === 2 ? THEME.blue + '22' : 'transparent',
                border: `1px solid ${i === 2 ? THEME.blue + '60' : THEME.line}`,
                fontSize: 12, color: i === 2 ? THEME.text : THEME.textDim, fontWeight: i === 2 ? 600 : 500,
                display: 'flex', alignItems: 'center', gap: 8,
              }}>
                {i === 2 && <Icon d={Icons.check} size={11} stroke={THEME.blue} sw={3}/>}
                {x}
              </div>
            ))}
          </div>
        </div>
      </div>

      <Avatar />
      <div style={{ marginTop: 14 }}><V3Disclosure jx={m.jurisdiction} compact/></div>
    </div>
  );
};

/* Decision log / audit trail with filter chips (Fix 3) */
const V3AuditLog = ({ matter }) => {
  const m = V3_MATTERS[matter];
  const ds = getMatterDataset(matter);
  const events = ds.auditEvents;

  const FILTERS = [
    { id: 'all', label: 'All' },
    { id: 'cause-of-action', label: 'Cause-of-action' },
    { id: 'evidence', label: 'Evidence' },
    { id: 'damages', label: 'Damages' },
    { id: 'filing', label: 'Filing' },
    { id: 'atticus', label: 'Atticus interactions' },
  ];

  const [filter, setFilter] = React.useState('all');
  const counts = FILTERS.reduce((acc, f) => {
    acc[f.id] = f.id === 'all' ? events.length : events.filter(e => e.kind === f.id).length;
    return acc;
  }, {});
  const visible = filter === 'all' ? events : events.filter(e => e.kind === filter);

  return (
    <div style={{ padding: '28px 36px', overflow: 'auto', height: '100%' }}>
      <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase', marginBottom: 4 }}>Decision log · Audit trail</div>
      <div style={{ fontFamily: 'Fraunces', fontSize: 26, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, marginBottom: 6 }}>
        Your decision log · {events.length} decisions
      </div>
      <div style={{ fontSize: 12.5, color: THEME.textDim, marginBottom: 14, maxWidth: 760, lineHeight: 1.55 }}>
        Every legally-significant decision you've made on this case is recorded here. For each: what we suggested, what you chose, and the explanation we showed you at the time. <span style={{ color: THEME.text, fontWeight: 600 }}>Decisions are immutable</span>. If you change your mind, the system creates a new entry; old entries are never overwritten.
      </div>

      {/* Filter chips */}
      <div style={{ display: 'flex', gap: 8, marginBottom: 14, flexWrap: 'wrap' }}>
        {FILTERS.map(f => {
          const active = filter === f.id;
          return (
            <div key={f.id} onClick={() => setFilter(f.id)} style={{
              padding: '7px 12px', borderRadius: 999, cursor: 'pointer',
              background: active ? THEME.blue : 'transparent',
              color: active ? '#D7DEE2' : THEME.textDim,
              border: `1px solid ${active ? THEME.blue : THEME.line}`,
              fontSize: 11.5, fontWeight: 600,
              display: 'inline-flex', alignItems: 'center', gap: 6,
              transition: 'all 0.15s ease',
            }}>
              <span>{f.label}</span>
              <span style={{
                fontSize: 10, fontWeight: 700, padding: '0px 6px', borderRadius: 999,
                background: active ? 'rgba(255,255,255,0.22)' : THEME.line,
                color: active ? '#D7DEE2' : THEME.textMute,
                minWidth: 16, textAlign: 'center',
              }}>{counts[f.id]}</span>
            </div>
          );
        })}
      </div>

      <div style={{ background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}`, borderRadius: 12 }}>
        {visible.length === 0 && (
          <div style={{ padding: '32px 16px', textAlign: 'center', color: THEME.textMute, fontSize: 12 }}>
            No events in this category.
          </div>
        )}
        {visible.map((e, i) => (
          <div key={i} style={{ padding: '14px 16px', borderTop: i === 0 ? 'none' : `1px solid ${THEME.lineSoft}`, display: 'grid', gridTemplateColumns: '140px 1fr', gap: 14 }}>
            <div>
              <div style={{ fontSize: 11, color: THEME.textMute, fontWeight: 600 }}>{e.ts}</div>
              <div style={{ fontSize: 9.5, fontWeight: 700, padding: '1px 5px', borderRadius: 3, background: THEME.line, color: THEME.textDim, letterSpacing: 0.4, textTransform: 'uppercase', display: 'inline-block', marginTop: 4 }}>{e.kind}</div>
            </div>
            <div>
              <div style={{ fontSize: 13, color: THEME.text, fontWeight: 600, marginBottom: 4 }}>{e.label}</div>
              <div style={{ fontSize: 11.5, color: THEME.textDim, lineHeight: 1.5 }}>
                <span style={{ color: THEME.blue, fontWeight: 600 }}>System:</span> {e.sys}<br/>
                <span style={{ color: THEME.success, fontWeight: 600 }}>You:</span> {e.user}
              </div>
            </div>
          </div>
        ))}
      </div>

      <div style={{ marginTop: 14 }}><V3Disclosure jx={m.jurisdiction} compact/></div>
    </div>
  );
};

/* Crisis routing overlay */
const V3CrisisOverlay = ({ onClose }) => (
  <div style={{
    position: 'absolute', inset: 0, zIndex: 50,
    background: THEME.mode === 'dark' ? 'rgba(18,25,33,0.95)' : 'rgba(246,244,238,0.97)',
    backdropFilter: 'blur(18px)', WebkitBackdropFilter: 'blur(18px)',
    display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 40,
  }}>
    <div style={{ maxWidth: 580, padding: '32px 36px', borderRadius: 18, background: THEME.mode === 'dark' ? '#23303D' : '#D7DEE2', border: `1px solid ${THEME.line}`, boxShadow: '0 20px 60px rgba(0,0,0,0.3)' }}>
      <div style={{ fontSize: 11, color: THEME.warnText, letterSpacing: 1.4, fontWeight: 700, textTransform: 'uppercase', marginBottom: 6 }}>We need to pause</div>
      <div style={{ fontFamily: 'Fraunces', fontSize: 24, color: THEME.text, fontWeight: 500, letterSpacing: -0.3, lineHeight: 1.2, marginBottom: 12 }}>
        What you've described sounds like it might involve safety. That's outside what Hello Court can help with directly. But here's what can help right now.
      </div>
      <div style={{ fontSize: 13, color: THEME.textDim, lineHeight: 1.6, marginBottom: 16 }}>
        We noticed wording in your last entry that suggested possible domestic violence or immediate physical danger. Before we continue, please consider reaching out to one of the resources below. Your case state is saved. You can come back any time.
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 18 }}>
        {[
          { l: 'National DV Hotline', n: '1-800-799-SAFE (7233)', sub: '24/7 · confidential · multilingual' },
          { l: 'Crisis Text Line', n: 'Text HOME to 741741', sub: 'Text-based crisis support' },
          { l: 'Childhelp', n: '1-800-422-4453', sub: 'Child safety + custody emergencies' },
        ].map((x, i) => (
          <div key={i} style={{ padding: '12px 14px', borderRadius: 10, background: THEME.warn + '14', border: `1px solid ${THEME.warn}40` }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <Icon d={Icons.bell} size={16} stroke={THEME.warn}/>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 13, color: THEME.text, fontWeight: 600 }}>{x.l}</div>
                <div style={{ fontSize: 11, color: THEME.textDim, marginTop: 1 }}>{x.sub}</div>
              </div>
              <div style={{ fontFamily: 'Fraunces', fontSize: 15, color: THEME.warnText, fontWeight: 600 }}>{x.n}</div>
            </div>
          </div>
        ))}
      </div>

      <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
        <Btn kind="secondary" onClick={onClose}>Continue intake anyway</Btn>
        <Btn kind="accent" tone={THEME.warn} onClick={onClose}>Pause and call this</Btn>
      </div>
    </div>
  </div>
);

/* ================================ WELCOME ================================ */

/* Demo welcome screen. First thing partners see when they enter the prototype.
   Hero with Hello Court lockup + tagline + theme toggle, three case-card buttons that
   launch each walkthrough, MVP / Phase 1 / Phase 2 roadmap chips. Renders inside
   the iPad frame; switches to a matter screen when a card is clicked. */
/* Resume banner — visual-only stub. Demonstrates the "save & quit, come
   back later" story for the partner demo. Real persistence layer (local
   storage now, backend later) will populate this from a pending session
   record. Today: hardcoded fake state showing a 60%-complete newcase
   wage / overtime matter. */
const V3ResumeBanner = ({ onChooseMatter, onOpenTriage, isDark }) => {
  const [dismissed, setDismissed] = React.useState(false);
  if (dismissed) return null;

  // Fake pending session — demo only. Real impl reads from storage.
  const pending = {
    matter: 'newcase',
    matterNoun: 'wage / overtime case',
    title: "Maria Vargas, wage / overtime",
    step: 'Documents · Final review',
    progressPct: 60,
    stepIdx: 4,
    stepCount: 7,
    lastTouched: '2 hours ago',
  };

  const accent = THEME.blue;
  const accentSoft = THEME.blueSoft;

  return (
    <div style={{
      position: 'relative',
      padding: '18px 22px',
      borderRadius: 14,
      background: isDark ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.72)',
      backdropFilter: 'blur(28px) saturate(1.6)',
      WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
      border: `1px solid ${THEME.line}`,
      borderLeft: `3px solid ${accent}`,
      display: 'flex', flexDirection: 'column', gap: 14,
    }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 16 }}>
        <div style={{
          width: 38, height: 38, borderRadius: 19,
          background: accentSoft, border: `1px solid ${accent}40`,
          display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
        }}>
          <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 20, color: accent }}>history</span>
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4, flexWrap: 'wrap' }}>
            <span style={{ fontSize: 10.5, color: accent, letterSpacing: 1.2, fontWeight: 700, textTransform: 'uppercase' }}>
              Welcome back
            </span>
            <Pill soft tone={THEME.success}>Saved automatically</Pill>
          </div>
          <div style={{ fontFamily: 'Fraunces', fontSize: 20, color: THEME.text, fontWeight: 500, letterSpacing: -0.3, lineHeight: 1.25, marginBottom: 4 }}>
            Pick up where you left off: {pending.title}
          </div>
          <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.55 }}>
            You're on <strong style={{ color: THEME.text }}>{pending.step}</strong> · Step {pending.stepIdx} of {pending.stepCount} · last touched {pending.lastTouched}
          </div>
        </div>
        <Btn kind="ghost" onClick={() => setDismissed(true)} ariaLabel="Dismiss resume banner" style={{ padding: '6px 10px', fontSize: 11 }}>
          Start fresh
        </Btn>
      </div>

      {/* Progress bar */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
        <div style={{ flex: 1, height: 5, borderRadius: 3, background: isDark ? 'rgba(255,255,255,0.06)' : 'rgba(20,30,50,0.08)', overflow: 'hidden' }}>
          <div style={{
            width: pending.progressPct + '%', height: '100%',
            background: `linear-gradient(90deg, ${accent}, ${THEME.success})`,
            borderRadius: 3,
            transition: 'width 0.6s cubic-bezier(0.22, 1, 0.36, 1)',
          }}/>
        </div>
        <span style={{ fontSize: 11, color: THEME.textDim, fontVariantNumeric: 'tabular-nums', minWidth: 30 }}>
          {pending.progressPct}%
        </span>
        <Btn kind="primary" onClick={() => onChooseMatter && onChooseMatter(pending.matter)}>
          Continue
        </Btn>
      </div>
    </div>
  );
};

const V3Welcome = ({ onChooseMatter, onOpenTriage, mode, onModeChange }) => {
  const isDark = THEME.mode === 'dark';
  const cards = [
    {
      key: 'maria',
      eyebrow: 'WALKTHROUGH 1 · UTAH · TIER 1',
      title: 'Maria Vargas',
      sub: 'Security deposit recovery, Salt Lake County',
      body: 'Walk a Utah pro-se litigant from intake through the file-ready packet. Atticus pressure-tested. Sandbox-authorized courtroom prep included.',
      cta: 'Start Maria walkthrough',
      tone: THEME.blue,
      toneSoft: THEME.blueSoft,
      onClick: () => onChooseMatter && onChooseMatter('maria'),
    },
    {
      key: 'new',
      eyebrow: 'WALKTHROUGH 2 · ANY JURISDICTION',
      title: 'Start a new case',
      sub: 'Universal "what\'s going on?" intake demo',
      body: 'See how the platform routes a fresh case to Tier 1, 2, or 3 based on facts and posture, with crisis-routing precedence baked in.',
      cta: 'Open new-case intake',
      tone: THEME.highlight,
      toneSoft: 'rgba(242,232,87,0.18)',
      onClick: () => onOpenTriage && onOpenTriage(),
    },
    {
      key: 'daniel',
      eyebrow: 'WALKTHROUGH 3 · UTAH · TIER 2 HANDOFF',
      title: 'Daniel Romero',
      sub: 'Contract dispute above small-claims cap',
      body: 'Walk the scope-honest handoff: same intake structure as Tier 1, but routes to a lawyer-pitch packet instead of a self-file packet.',
      cta: 'Start Daniel walkthrough',
      tone: THEME.danger,
      toneSoft: THEME.dangerSoft,
      onClick: () => onChooseMatter && onChooseMatter('daniel'),
    },
  ];
  const roadmap = [
    { label: 'MVP', sub: 'Maria UT + Sarah NY end-to-end', state: 'active' },
    { label: 'Phase 1', sub: 'Sandbox launch + telemetry + sandbox quarterly reporting', state: 'planned' },
    { label: 'Phase 2', sub: 'E-filing, lawyer-scan automation, voice modes beyond Plain & Kind', state: 'planned' },
  ];
  return (
    <div style={{
      padding: '40px 48px 32px',
      overflow: 'auto', height: '100%',
      display: 'flex', flexDirection: 'column', gap: 36,
    }}>
      {/* Resume banner — visual stub for the demo. Real implementation will
          read pending session state from localStorage / backend. For now,
          shows an in-progress newcase wage matter so partners see the
          "save & quit, resume any time" story. Click Start fresh to dismiss. */}
      <V3ResumeBanner
        onChooseMatter={onChooseMatter}
        onOpenTriage={onOpenTriage}
        isDark={isDark}
      />

      {/* Hero: lockup + theme toggle */}
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24 }}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10, flex: '1 1 auto', minWidth: 0 }}>
          <div style={{
            display: 'inline-flex', alignItems: 'center', gap: 8, alignSelf: 'flex-start',
            padding: '5px 11px', borderRadius: 999, background: THEME.blueSoft,
            border: `1px solid ${THEME.blue}40`,
          }}>
            <div style={{ width: 6, height: 6, borderRadius: 3, background: THEME.blue }}/>
            <span style={{ fontSize: 10.5, color: THEME.blue, letterSpacing: 1.4, fontWeight: 700, textTransform: 'uppercase' }}>
              Partner demo prototype
            </span>
          </div>
          <div style={{
            fontFamily: 'Fraunces', fontSize: 42, color: THEME.text, fontWeight: 500,
            letterSpacing: -0.8, lineHeight: 1.05, marginTop: 4,
          }}>
            Hello Court
          </div>
          <div style={{ fontSize: 16, color: THEME.textDim, lineHeight: 1.55, maxWidth: 620 }}>
            A walkthrough of how a pro-se litigant builds a case end-to-end, with Atticus enforcing UPL discipline at every step. Pick a story below to see the platform in action.
          </div>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
          <div onClick={() => onModeChange && onModeChange(isDark ? 'light' : 'dark')} style={{
            cursor: 'pointer', padding: '8px 12px', borderRadius: 8, fontSize: 12, fontWeight: 600,
            color: THEME.text, border: `1px solid ${THEME.line}`, background: THEME.glass,
            display: 'flex', alignItems: 'center', gap: 6,
          }}>
            <span>{isDark ? '☾' : '☀'}</span>
            <span>{isDark ? 'Dark' : 'Light'}</span>
          </div>
        </div>
      </div>

      {/* Three case cards */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
        {cards.map(c => (
          <div key={c.key} onClick={c.onClick} style={{
            cursor: 'pointer', position: 'relative',
            padding: '20px 22px', borderRadius: 16,
            background: isDark ? 'rgba(36,42,52,0.55)' : 'rgba(255,255,255,0.65)',
            border: `1px solid ${THEME.line}`,
            backdropFilter: 'blur(28px) saturate(1.4)', WebkitBackdropFilter: 'blur(28px) saturate(1.4)',
            display: 'flex', flexDirection: 'column', gap: 12,
            transition: 'transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease',
          }}
          onMouseEnter={(e) => { e.currentTarget.style.borderColor = c.tone; e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = `0 18px 38px -16px ${c.tone}55`; }}
          onMouseLeave={(e) => { e.currentTarget.style.borderColor = THEME.line; e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = 'none'; }}
          >
            <div style={{
              fontSize: 9.5, color: c.tone, letterSpacing: 1.4, fontWeight: 700,
              textTransform: 'uppercase',
            }}>{c.eyebrow}</div>
            <div style={{ fontFamily: 'Fraunces', fontSize: 22, color: THEME.text, fontWeight: 500, letterSpacing: -0.3, lineHeight: 1.15 }}>
              {c.title}
            </div>
            <div style={{ fontSize: 13, color: THEME.textDim, fontWeight: 500 }}>
              {c.sub}
            </div>
            <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.55, flex: '1 1 auto' }}>
              {c.body}
            </div>
            <div style={{
              marginTop: 6,
              display: 'inline-flex', alignItems: 'center', gap: 8, alignSelf: 'flex-start',
              padding: '7px 12px', borderRadius: 999,
              background: c.toneSoft, color: c.tone,
              fontSize: 11.5, fontWeight: 700, letterSpacing: 0.4,
              border: `1px solid ${c.tone}40`,
            }}>
              <span>{c.cta}</span>
              <span style={{ fontSize: 14, lineHeight: 1 }}>→</span>
            </div>
          </div>
        ))}
      </div>

      {/* Roadmap chips */}
      <div>
        <div style={{
          fontSize: 10.5, color: THEME.textMute, letterSpacing: 1.4, fontWeight: 700,
          textTransform: 'uppercase', marginBottom: 10,
        }}>Roadmap</div>
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
          {roadmap.map(r => {
            const active = r.state === 'active';
            return (
              <div key={r.label} style={{
                display: 'flex', alignItems: 'center', gap: 10,
                padding: '10px 14px', borderRadius: 12,
                background: active ? THEME.blueSoft : (isDark ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.55)'),
                border: `1px ${active ? 'solid' : 'dashed'} ${active ? THEME.blue + '60' : THEME.line}`,
                flex: '1 1 240px',
              }}>
                <div style={{
                  width: 10, height: 10, borderRadius: 5,
                  background: active ? THEME.blue : THEME.textMute,
                  opacity: active ? 1 : 0.5,
                  flexShrink: 0,
                }}/>
                <div style={{ flex: '1 1 auto', minWidth: 0 }}>
                  <div style={{
                    fontSize: 11, color: active ? THEME.blue : THEME.textDim,
                    letterSpacing: 1.2, fontWeight: 700, textTransform: 'uppercase',
                  }}>{r.label}</div>
                  <div style={{ fontSize: 12.5, color: THEME.text, fontWeight: 500, marginTop: 2 }}>
                    {r.sub}
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {/* V3FirstRun — non-attorney disclosure shown before the user enters any
          walkthrough. Anchors the NY-floor disclosure (stricter jurisdiction)
          since no jurisdiction is selected yet on the welcome screen. */}
      <div style={{
        padding: '14px 18px', borderRadius: 12,
        background: isDark ? 'rgba(36,42,52,0.55)' : 'rgba(255,255,255,0.65)',
        border: `1px dashed ${THEME.line}`,
        display: 'flex', gap: 10, alignItems: 'flex-start',
        fontSize: 12, color: THEME.textDim, lineHeight: 1.55,
      }}>
        <Icon d={Icons.shield} size={14} stroke={THEME.textMute}/>
        <div style={{ flex: 1 }}>
          <span style={{ color: THEME.text, fontWeight: 600 }}>What Hello Court does</span>
          {' '}
          Hello Court is not a law firm. We help you organize your case and generate documents you can file yourself. This is legal information and tools, not legal advice for your specific situation. For advice tailored to your case, talk to a licensed attorney.
        </div>
      </div>
    </div>
  );
};

/* ================================ ROOT BODY ================================ */

/* Coming-soon stub for case-types in the case-selector that don't have full screens
   yet (wage / fraud / breach in V3_MATTERS with comingSoon: true).
   Communicates breadth without fake content. */
const V3ComingSoon = ({ matter }) => {
  const m = V3_MATTERS[matter];
  return (
    <div style={{ padding: '40px 36px', overflow: 'auto', height: '100%', maxWidth: 760 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
        <V3TierBadge tier={m.tier}/>
        <span style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase' }}>{m.type}</span>
      </div>
      <div style={{ fontFamily: 'Fraunces', fontSize: 28, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, marginBottom: 8 }}>
        {m.title}
      </div>
      <div style={{ fontSize: 13, color: THEME.textDim, lineHeight: 1.55, marginBottom: 18, maxWidth: 640 }}>
        {m.summary}
      </div>

      <div style={{ padding: '18px 20px', borderRadius: 14, background: THEME.warnSoft, border: `1px solid ${THEME.warn}40`, marginBottom: 18, display: 'flex', gap: 12, alignItems: 'flex-start' }}>
        <Icon d={Icons.alert} size={16} stroke={THEME.warn}/>
        <div>
          <div style={{ fontSize: 13.5, color: THEME.text, fontWeight: 600, marginBottom: 4 }}>This case-type ships in Phase 1</div>
          <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.6 }}>
            The partner demo flows two cases end-to-end: <span style={{ color: THEME.text, fontWeight: 600 }}>Maria Vargas</span> (UT security deposit) and <span style={{ color: THEME.text, fontWeight: 600 }}>Sarah Patel</span> (NY auto repair). The other Tier 1 case-types appear here as stubs so the full MVP envelope is visible. Their element frameworks, intake flows, and Atticus content land in Phase 1 alongside sandbox authorization.
          </div>
        </div>
      </div>

      <div style={{ padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}`, marginBottom: 12 }}>
        <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 8 }}>Statutory anchor</div>
        <div style={{ fontSize: 14, color: THEME.text, fontWeight: 500, marginBottom: 4 }}>{m.statute}</div>
        <div style={{ fontSize: 12, color: THEME.textDim }}>{m.statuteNote}</div>
      </div>

      <div style={{ padding: '14px 16px', borderRadius: 12, background: THEME.mode === 'dark' ? 'rgba(36,42,52,0.60)' : 'rgba(255,255,255,0.60)', backdropFilter: 'blur(28px) saturate(1.6)', WebkitBackdropFilter: 'blur(28px) saturate(1.6)', border: `1px solid ${THEME.line}`, marginBottom: 18 }}>
        <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1, textTransform: 'uppercase', fontWeight: 600, marginBottom: 8 }}>What's coming in Phase 1</div>
        <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.6 }}>
          • Element framework + factual intake authored by Atticus<br/>
          • Per-evidence Atticus reads and affirmation gates<br/>
          • Damages decomposition matching this statute's remedy framework<br/>
          • Document templates for {m.court}<br/>
          • Sandbox-authorized courtroom prep (UT cases) or NY procedural framing (NY cases)
        </div>
      </div>

      <div style={{ fontSize: 12, color: THEME.textMute, lineHeight: 1.6, padding: '12px 14px', borderRadius: 10, border: `1px dashed ${THEME.lineSoft}` }}>
        <span style={{ color: THEME.text, fontWeight: 600 }}>Try a working demo:</span> open the case switcher and pick <span style={{ color: THEME.blue, fontWeight: 600 }}>Vargas v. Westhaven</span> (UT Tier 1) or <span style={{ color: THEME.blue, fontWeight: 600 }}>Patel v. East Side Auto</span> (NY Tier 1). For the guided tour, click <span style={{ color: THEME.blue, fontWeight: 600 }}>Take the walkthrough</span> in the sidebar after switching.
      </div>

      <div style={{ marginTop: 14 }}><V3Disclosure jx={m.jurisdiction} compact/></div>
    </div>
  );
};

/* ─── V3Deliverables ─────────────────────────────────────────────────────────
   Three packet preview cards (FilingPacket / ReferencePacket / LawyerMatchPacket)
   as paper-styled placeholder mockups. Each shows what the deliverable IS in
   prose + bullet list. Carries a "FOR DEMO PURPOSES" banner since production
   visual design ships in Story 2.46 under Epic 2. */
const V3Deliverables = ({ matter }) => {
  const m = V3_MATTERS[matter];
  const ds = getMatterDataset(matter);
  const isUT = m.jurisdiction === 'Utah';
  const courtName = isUT ? 'Salt Lake County Justice Court' : 'NYC Civil Court (NY County)';
  // Brand-attribution toggle for the Filing Packet cover. The non-attorney
  // disclosure stays on the document either way (UPL discipline). Default ON
  // because transparency is the safer posture; pending Atticus pressure-test.
  const [includeAttribution, setIncludeAttribution] = React.useState(true);
  const filingPacketHref = includeAttribution
    ? 'assets/maria-filing-packet.pdf'
    : 'assets/maria-filing-packet-private.pdf';

  const paper = THEME.mode === 'dark' ? '#23303D' : '#FBFAF5';
  const banner = (
    <div style={{
      padding: '6px 12px', borderRadius: 999, alignSelf: 'flex-start',
      background: 'rgba(242,197,61,0.16)',
      border: `1px solid ${THEME.warn}40`,
      fontSize: 9.5, letterSpacing: 1.4, textTransform: 'uppercase',
      color: THEME.warnText, fontWeight: 700,
      marginBottom: 12,
    }}>For demo purposes · Placeholder rendering</div>
  );

  const cardStyle = {
    padding: 24, borderRadius: 14,
    background: paper, border: `1px solid ${THEME.line}`,
    fontFamily: 'Fraunces',
    display: 'flex', flexDirection: 'column',
    boxShadow: THEME.mode === 'dark' ? '0 8px 24px -8px rgba(0,0,0,0.40)' : '0 8px 24px -10px rgba(36,42,52,0.18)',
    position: 'relative',
  };

  const downloadBtn = (label, href) => {
    const sharedStyle = {
      marginTop: 16,
      alignSelf: 'flex-end',
      display: 'inline-flex', alignItems: 'center', gap: 8,
      padding: '9px 16px', borderRadius: 999,
      background: THEME.blue, color: '#D7DEE2',
      fontSize: 12, fontWeight: 600, letterSpacing: -0.05,
      fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
      border: '1px solid rgba(255,255,255,0.12)',
      cursor: 'pointer',
      textDecoration: 'none',
      boxShadow: '0 4px 12px -4px rgba(105,134,191,0.55)',
    };
    if (href) {
      return (
        <a
          href={href}
          download
          title="Download mock filing packet (PDF)"
          style={sharedStyle}
        >
          <Icon d={Icons.download || Icons.send} size={13} stroke="#D7DEE2" sw={2.2}/>
          {label}
        </a>
      );
    }
    return (
      <div
        role="button"
        title="Mock button · production build will export the actual PDF"
        onClick={(e) => e.preventDefault()}
        style={sharedStyle}
      >
        <Icon d={Icons.download || Icons.send} size={13} stroke="#D7DEE2" sw={2.2}/>
        {label}
      </div>
    );
  };

  const sectionLabel = (text) => (
    <div style={{
      fontSize: 10, color: THEME.textMute, letterSpacing: 1.4,
      textTransform: 'uppercase', fontWeight: 700, marginBottom: 8,
    }}>{text}</div>
  );

  const titleStyle = {
    fontFamily: 'Fraunces', fontSize: 22, color: THEME.text,
    fontWeight: 500, letterSpacing: -0.4, marginBottom: 6,
  };

  const subStyle = {
    fontSize: 12, color: THEME.textDim, lineHeight: 1.55, marginBottom: 14,
    fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
  };

  const bulletStyle = {
    fontSize: 12, color: THEME.text, lineHeight: 1.7,
    fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
    paddingLeft: 18, margin: 0,
  };

  return (
    <div style={{ padding: '28px 36px', overflow: 'auto', height: '100%' }}>
      {/* Heading wrapper — right-padding reserves space for the floating
          V3MatterSwitcher (absolute top:36 right:18 ~290 px wide) so the
          title doesn't underflow it on narrow widths. Cards below extend
          full-width since they sit below the switcher's vertical zone. */}
      <div style={{ paddingRight: 320, maxWidth: '100%' }}>
        <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase', marginBottom: 4 }}>Your packets · Three deliverables</div>
        <div style={{ fontFamily: 'Fraunces', fontSize: 26, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, marginBottom: 6 }}>
          Three packets, one case. Each one does a different job.
        </div>
        <div style={{ fontSize: 13, color: THEME.textDim, lineHeight: 1.6, marginBottom: 22, maxWidth: 720 }}>
          Hello Court produces three court-related PDFs from your case work. The first goes to the court. The second stays with you for hearing day. The third helps you find a lawyer if you decide you want one.
        </div>
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
        {/* ── FilingPacket ─────────────────────────────────────────────── */}
        <div style={cardStyle}>
          {banner}
          {sectionLabel('Deliverable 1 of 3 · Filing Packet')}
          <div style={titleStyle}>Filing Packet</div>
          <div style={subStyle}>
            What the court keeps. What opposing counsel sees. The Statement of Claim, your evidence inventory, the damages calculation, and every required court form for your case-type in {m.jurisdiction}, all assembled into one bundled PDF that you walk into the {courtName} ready to file.
          </div>
          <ul style={bulletStyle}>
            <li>Cover Page with case caption + plaintiff contact</li>
            <li>Table of Contents</li>
            <li>Exhibit Index (Reference Copy) for quick navigation</li>
            <li>Case Snapshot &mdash; nature of claim, key issues, evidence highlights, relief</li>
            <li>Case Summary &mdash; narrative</li>
            <li>Chronological Timeline of Events with exhibit references</li>
            <li>Statement of Claim &mdash; numbered Roman sections</li>
            <li>Additional Documentation &mdash; Legal Grounds, Damages Summary, Relief Requested</li>
            <li>Key Admissions and Undisputed Facts</li>
            <li>Exhibit Index and Descriptions &mdash; detailed</li>
            <li>Fee-Waiver Application if you qualify (auto-attached)</li>
          </ul>
          {matter === 'maria' && (
            <div style={{
              marginTop: 14, padding: '12px 14px', borderRadius: 10,
              background: THEME.mode === 'dark' ? 'rgba(105,134,191,0.08)' : 'rgba(105,134,191,0.06)',
              border: `1px solid ${THEME.line}`,
              fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
            }}>
              <label
                onClick={() => setIncludeAttribution(v => !v)}
                style={{ display: 'flex', alignItems: 'flex-start', gap: 10, cursor: 'pointer' }}>
                <span style={{
                  /* Canonical checkbox (small) — matches Btn primary aesthetic. */
                  width: 18, height: 18, borderRadius: 4, marginTop: 2, flexShrink: 0,
                  border: `1.5px solid ${includeAttribution ? 'rgba(242,244,248,0.18)' : THEME.lineStrong}`,
                  background: includeAttribution ? (THEME.mode === 'dark' ? '#1A2230' : '#2A3640') : 'transparent',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  transition: 'background 0.18s ease, border-color 0.18s ease',
                }}>{includeAttribution && <Icon d={Icons.check} size={11} stroke="#F2F4F8" sw={3}/>}</span>
                <span style={{ flex: 1 }}>
                  <span style={{ fontSize: 12.5, color: THEME.text, fontWeight: 600 }}>
                    Include &ldquo;Prepared with Hello Court&rdquo; attribution on the cover
                  </span>
                  <span style={{ display: 'block', fontSize: 11.5, color: THEME.textDim, marginTop: 4, lineHeight: 1.5 }}>
                    {includeAttribution
                      ? 'Cover credits Hello Court as the drafting tool. Default for transparency.'
                      : 'Cover names you as the sole pro se filer. No attorney is named on the document because no attorney represents you in this matter.'}
                  </span>
                </span>
              </label>
            </div>
          )}
          {matter === 'maria'
            ? downloadBtn('Download Filing Packet', filingPacketHref)
            : downloadBtn('Download Filing Packet')}
        </div>

        {/* ── ReferencePacket ─────────────────────────────────────────── */}
        <div style={cardStyle}>
          {banner}
          {sectionLabel('Deliverable 2 of 3 · Reference Packet')}
          <div style={titleStyle}>Reference Packet · Plaintiff confidential</div>
          <div style={subStyle}>
            Your courtroom-day companion. You bring it with you. The judge never sees it. The other side never sees it. It&rsquo;s structured for live courtroom use under stress, written in plain language, and designed to keep you grounded if the moment gets hard.
          </div>
          <ul style={bulletStyle}>
            <li>Opening statement script (with your factual hooks)</li>
            <li>Oral outline &middot; your case in delivery order</li>
            <li>Exhibit walk-through &middot; which exhibit when, and how to introduce it</li>
            <li>Rebuttals &middot; anticipated defense arguments + prepared responses</li>
            <li>Cross-examination prep &middot; questions the other party may ask you, with one way you might frame each answer</li>
            <li>Anticipated judge questions with structured response frames</li>
            <li>Legal rules and citations you&rsquo;re standing on</li>
            <li>Damages math walked step by step</li>
            <li>What to expect inside the courtroom &middot; including recovery patterns if you get confused</li>
          </ul>
          {matter === 'maria'
            ? downloadBtn('Download Reference Packet', 'assets/maria-reference-packet.pdf')
            : downloadBtn('Download Reference Packet')}
        </div>

        {/* ── LawyerMatchPacket ───────────────────────────────────────── */}
        <div style={cardStyle}>
          {banner}
          {sectionLabel('Deliverable 3 of 3 · Lawyer Match Packet · Optional')}
          <div style={titleStyle}>Lawyer Match Packet</div>
          <div style={subStyle}>
            A curated list of attorneys near you whose practice fits your case. Public ratings (Avvo, Martindale-Hubbell, peer-review) appear with their original sources cited. Platform-derived match signals (case-type fit, fee-tier fit, language fit) are clearly labeled as match signals, not endorsements. We never invent ratings.
          </div>
          <ul style={bulletStyle}>
            <li>Geographically scoped to your jurisdiction (with optional radius)</li>
            <li>Per attorney: name, firm, contact, bar-verified credentials, years in practice</li>
            <li>Disciplinary history (clean or flagged · sourced from state bar)</li>
            <li>Public ratings with source attribution next to each rating</li>
            <li>Fee structure where the attorney publishes it</li>
            <li>Languages spoken · office distance from you</li>
            <li>Platform fit signals · case-type match, fee-tier match, language match</li>
            <li>Watermarked + access-logged · so we know if a packet leaks</li>
          </ul>
          <div style={{
            marginTop: 14, padding: '8px 12px', borderRadius: 8,
            background: THEME.mode === 'dark' ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.03)',
            fontSize: 10.5, color: THEME.textMute, lineHeight: 1.5, fontStyle: 'italic',
            fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
          }}>
            Public-directory matches. Not platform partners. Hello Court does not endorse any individual attorney.
          </div>
          {downloadBtn('Download Lawyer Match Packet')}
        </div>
      </div>

      <div style={{ marginTop: 18 }}><V3Disclosure jx={m.jurisdiction} compact/></div>
    </div>
  );
};

const V3NewcaseEmpty = ({ screen, onScreen }) => {
  const labels = {
    timeline: 'Timeline',
    audit: 'Decision log',
    deliverables: 'Your packets',
    readiness: 'Case readiness',
    documents: 'Documents',
    filing: 'Filing & service',
    courtroom: 'Courtroom prep',
  };
  const title = labels[screen] || 'This screen';
  return (
    <div style={{ padding: '32px 40px', overflow: 'auto', height: '100%' }}>
      <div style={{ maxWidth: 640 }}>
        <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase', marginBottom: 6 }}>Just started</div>
        <div style={{ fontFamily: 'Fraunces', fontSize: 26, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, marginBottom: 10, lineHeight: 1.2 }}>
          {title} fills in as your case takes shape.
        </div>
        <div style={{ fontSize: 13.5, color: THEME.textDim, lineHeight: 1.65, marginBottom: 24 }}>
          You're at the beginning. Pick a legal theory and connect some evidence first; this screen comes alive as you make progress. Nothing's missing. Just nothing to show yet.
        </div>
        <Btn kind="secondary" onClick={() => onScreen('home')}>Back to case home</Btn>
      </div>
    </div>
  );
};

/* First-run launchpad. The MAIN content area for a brand-new user who has no
   case yet (the `emptyState` signal — same noMatters / ?empty=1 source Story
   0.64 uses to lock the sidebar). Completes the empty-state experience: 0.64
   locked the nav, this fills the middle with a calm getting-started screen
   instead of a seeded case home.

   Grounded in competitive-product-surfaces-research §5 (Home/Launchpad):
   "Lead with reassurance + a single focal action, not density" and
   "New-user empty state = the getting-started launchpad: warm copy, one bold
   CTA, a 5-item pre-progressed checklist (start ~20%): pick case type → tell
   us what happened → add evidence → see readiness → review options."

   Audience is stressed, often first-time-legal, possibly vulnerable — so the
   tone is calm and kind, never salesy. One bold CTA ("Start a new case")
   wired to onStartCase, which is the SAME action the sidebar "Start a new
   case" item triggers (onScreen('triage') → opens TriageIntake in demo.html).
   The 5-step preview is light/scannable, not a dashboard. Reuses the existing
   V3 design system (Btn, Card, Icon, periwinkle tokens, Fraunces headings) so
   it's on-brand in both dark + light.

   KINSLEY REVIEW: hero spacing, step-card treatment, and the gentle
   first-step "pre-progressed" highlight (Zeigarnik ~20%-started cue) are
   design calls open to refinement. */
const V3FirstRun = ({ onStartCase, jx = 'ny' }) => {
  const isDark = THEME.mode === 'dark';
  const [openGuide, setOpenGuide] = React.useState(null);
  const isUT = jx === 'utah';
  const peopleWord = isUT ? 'Utahns' : 'New Yorkers';

  const tile = (extra) => ({
    background: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(255,255,255,0.42)',
    border: `1px solid ${isDark ? 'rgba(255,255,255,0.075)' : 'rgba(20,30,50,0.10)'}`,
    borderRadius: 16, padding: '18px 18px',
    backdropFilter: 'blur(22px) saturate(1.5)', WebkitBackdropFilter: 'blur(22px) saturate(1.5)',
    boxShadow: isDark ? 'inset 0 1px 0 rgba(255,255,255,0.06), 0 8px 28px -12px rgba(0,0,0,0.4)' : 'inset 0 1px 0 rgba(255,255,255,0.6), 0 8px 24px -14px rgba(20,30,50,0.20)',
    ...(extra || {}),
  });
  const kLabel = { fontSize: 10.5, letterSpacing: 1.1, textTransform: 'uppercase', color: THEME.textMute, fontWeight: 600, margin: '0 0 9px', display: 'flex', alignItems: 'center', gap: 7 };
  const link = { color: THEME.accent, fontSize: 12.5, cursor: 'pointer' };
  const mi = (name, size = 14, color = THEME.textMute) => (
    <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: size, lineHeight: 1, color, fontVariationSettings: "'opsz' 24" }}>{name}</span>
  );
  const gchip = {
    display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 12.5, color: THEME.text,
    background: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(20,30,50,0.05)',
    border: `1px solid ${isDark ? 'rgba(255,255,255,0.075)' : 'rgba(20,30,50,0.10)'}`,
    borderRadius: 999, padding: '8px 13px', cursor: 'pointer', fontFamily: 'inherit',
  };
  const gchipMore = { ...gchip, background: 'rgba(105,134,191,0.22)', border: `1px solid ${isDark ? 'rgba(143,164,208,0.52)' : 'rgba(78,109,168,0.45)'}`, color: isDark ? '#EAEFF9' : '#26324B', fontWeight: 600 };
  const guideNames = ['Hearing-day prep checklist', "Can't afford the fee? Fee waiver", 'What to bring and wear', 'After you win: collecting'];

  const howSteps = [
    { n: 1, title: 'Tell us what happened', body: 'In your own words, no legal jargon. We listen and turn it into a real case.' },
    { n: 2, title: 'Build your case', body: 'Add your evidence, watch your readiness score climb, and get clear next steps.' },
    { n: 3, title: 'File and go to court', body: 'We prepare your packet and walk you through filing and your hearing day.' },
  ];

  return (
    <div style={{ padding: '40px 40px 90px', overflow: 'auto', height: '100%' }}>
      <style>{`
        .v3-fr-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
        .v3-fr-grid .v3-span2 { grid-column: 1 / -1; }
        @media (max-width: 1100px) { .v3-fr-grid { grid-template-columns: 1fr; } }
      `}</style>
      <div style={{ width: '100%', maxWidth: 1100, margin: 0 }}>

        {/* Welcome header */}
        <div className="v3-matter-banner" style={{ marginBottom: 18 }}>
          <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '5px 11px', borderRadius: 999, background: THEME.blueSoft, border: `1px solid ${THEME.blue}40`, marginBottom: 12 }}>
            <div style={{ width: 6, height: 6, borderRadius: 3, background: THEME.blueOnSoft }}/>
            <span style={{ fontSize: 10.5, color: THEME.blueOnSoft, letterSpacing: 1.4, fontWeight: 700, textTransform: 'uppercase' }}>Welcome</span>
          </div>
          <div style={{ fontFamily: 'Fraunces', fontSize: 32, color: THEME.text, fontWeight: 600, letterSpacing: -0.5, lineHeight: 1.12, marginBottom: 8 }}>
            Let's start your first case.
          </div>
          <div style={{ fontSize: 14, color: THEME.textDim, lineHeight: 1.6, maxWidth: 600 }}>
            You don't need to know the law to begin. We explain everything in plain language and help you organize your case, one step at a time. You stay in control of every decision.
          </div>
        </div>

        {/* Prominent Start CTA card */}
        <div style={tile({
          marginBottom: 14, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 18, flexWrap: 'wrap', padding: '22px 24px',
          background: isDark ? 'linear-gradient(150deg, rgba(105,134,191,0.18), rgba(78,109,168,0.08))' : 'linear-gradient(150deg, rgba(105,134,191,0.14), rgba(78,109,168,0.06))',
          border: `1px solid ${isDark ? 'rgba(143,164,208,0.28)' : 'rgba(78,109,168,0.24)'}`,
        })}>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: 'Fraunces', fontSize: 20, fontWeight: 600, color: THEME.text, letterSpacing: -0.2, marginBottom: 4 }}>Ready when you are.</div>
            <div style={{ fontSize: 13, color: THEME.textDim, lineHeight: 1.5 }}>Getting started takes about 10 minutes. You can pause anytime and pick up right where you left off.</div>
          </div>
          <Btn kind="primary" icon={Icons.briefcase} onClick={() => onStartCase && onStartCase()} style={{ fontSize: 15, padding: '14px 28px' }}>Start a new case</Btn>
        </div>

        {/* Grid: how it works, self-check, not alone, free guides */}
        <div className="v3-fr-grid">
          <div className="v3-span2" style={tile()}>
            <p style={kLabel}>{mi('list_alt')} How it works</p>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 14 }}>
              {howSteps.map((s) => (
                <div key={s.n} style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                  <span style={{ width: 30, height: 30, borderRadius: 15, background: THEME.blueSoft, border: `1px solid ${THEME.blue}40`, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'Fraunces', fontWeight: 600, fontSize: 14, color: THEME.accent }}>{s.n}</span>
                  <div style={{ fontSize: 13.5, fontWeight: 600, color: THEME.text }}>{s.title}</div>
                  <div style={{ fontSize: 12, color: THEME.textDim, lineHeight: 1.5 }}>{s.body}</div>
                </div>
              ))}
            </div>
          </div>

          <div style={tile({ display: 'flex', flexDirection: 'column' })}>
            <p style={kLabel}>{mi('quiz')} Is small claims right for you?</p>
            <div style={{ fontSize: 13, color: THEME.textDim, lineHeight: 1.6 }}>Not sure your situation fits small claims? Take a quick self-check before you start. It is free and takes about 2 minutes.</div>
            <span onClick={() => onStartCase && onStartCase()} style={{ ...link, marginTop: 'auto', alignSelf: 'flex-start', paddingTop: 14 }}>Take the self-check ›</span>
          </div>

          <div style={tile({
            background: isDark ? 'linear-gradient(150deg, rgba(105,134,191,0.16), rgba(78,109,168,0.08))' : 'linear-gradient(150deg, rgba(105,134,191,0.12), rgba(78,109,168,0.06))',
            border: `1px solid ${isDark ? 'rgba(143,164,208,0.25)' : 'rgba(78,109,168,0.22)'}`,
          })}>
            <p style={{ ...kLabel, color: THEME.accent }}>{mi('group', 14, THEME.accent)} You're not alone</p>
            <div style={{ fontFamily: 'Fraunces', fontSize: 17, fontWeight: 600, color: THEME.text, margin: '0 0 5px' }}>People handle these every day.</div>
            <div style={{ color: THEME.accent, fontSize: 13, lineHeight: 1.5 }}>Thousands of {peopleWord} resolve disputes in small claims court themselves each year, without a lawyer. You can too, and we will be right beside you.</div>
          </div>

          <div className="v3-span2" style={tile()}>
            <p style={kLabel}>{mi('description')} Free guides you can read right now</p>
            <div style={{ display: 'flex', flexWrap: 'wrap', columnGap: 9, rowGap: 12, marginTop: 4 }}>
              {guideNames.map(g => (
                <button key={g} type="button" onClick={() => setOpenGuide(g)} style={gchip}>{mi('check_circle', 14, THEME.accent)}<span>{g}</span></button>
              ))}
              <button type="button" onClick={() => setOpenGuide('Your complete guide library')} style={gchipMore}>{mi('library_books', 14, isDark ? '#C9D6F0' : '#26324B')}<span>Your complete guide library</span></button>
            </div>
          </div>
        </div>

        {/* Reassurance + non-attorney disclosure */}
        <div style={{ marginTop: 18, fontSize: 12, color: THEME.textDim, lineHeight: 1.6, display: 'flex', alignItems: 'center', gap: 8 }}>
          <Icon d={Icons.shield} size={14} stroke={THEME.textDim} sw={1.6}/>
          <span>Your information is saved automatically. You can pause and pick up right where you left off.</span>
        </div>
        <div style={{ marginTop: 12, fontSize: 11.5, color: THEME.textDim, lineHeight: 1.55, maxWidth: 600 }}>
          {(JURISDICTIONS[jx] || JURISDICTIONS.ny).disclosureShort}
        </div>

        <V3GuidePreviewModal name={openGuide} onClose={() => setOpenGuide(null)}/>
      </div>
    </div>
  );
};

/* ─── Account-level screens (profile / settings / help) ─────────────────────
   Reached from the floating glass nav profile dropdown (V3GlassNav ddRow
   onClick → onScreen('profile' | 'settings' | 'help')). Demo-real: look
   complete, mock data is fine. All three are matter-agnostic and route via the
   onScreen callback that is already threaded through V3Body. They render as the
   sibling AFTER .v3-glass-nav inside .v3-main-col, so the nav-overlap-safe
   container query in demo.html (`.v3-main-col > .v3-glass-nav ~ *`) covers them
   automatically — they just need the standard inline `height:100%` +
   `overflow:auto` screen-root so the flex-fill override has something to fill. */

/* Shared "Language" row. Spanish is NOT available yet, so it renders as a
   visibly DISABLED coming-soon affordance (muted, not clickable, a small "Soon"
   tag) and routes NOWHERE. Shaped so that when ES ships this becomes a real
   EN/ES toggle: flip `esReady` to true and the two options become live,
   selectable buttons driving a single `lang` value. Mirrors the dead-Español
   treatment used in the nav dropdown. */
const V3LanguageRow = ({ kLabel, mi, hideLabel }) => {
  const isDark = THEME.mode === 'dark';
  // TODO(i18n): when Spanish ships, set esReady = true and wire `lang` to the
  // app language state; the markup below already renders both options as a
  // segmented EN/ES toggle, so no structural change is needed — only the
  // disabled/Soon treatment on the ES pill drops away.
  const esReady = false;
  const lang = 'en';
  const segBase = {
    fontFamily: 'inherit', fontSize: 13, fontWeight: 600, padding: '8px 16px',
    borderRadius: 999, border: `1px solid ${THEME.line}`, lineHeight: 1,
    display: 'inline-flex', alignItems: 'center', gap: 7,
  };
  const active = {
    ...segBase, background: THEME.blueSoft, color: THEME.blueOnSoft,
    border: `1px solid ${isDark ? 'rgba(143,164,208,0.40)' : 'rgba(78,109,168,0.36)'}`,
    cursor: esReady ? 'pointer' : 'default',
  };
  const enActive = lang === 'en';
  return (
    <div>
      {!hideLabel && <p style={{ ...kLabel, marginBottom: 12 }}>{mi('translate')} Language</p>}
      <div role="group" aria-label="Language" style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
        <button type="button" aria-pressed={enActive} disabled={!esReady}
          style={enActive ? active : { ...segBase, background: 'transparent', color: THEME.textDim, cursor: esReady ? 'pointer' : 'default' }}>
          English
        </button>
        {/* Español: disabled / coming-soon. Not a link, routes nowhere. */}
        <button type="button" disabled aria-disabled="true"
          title="Español is coming soon" aria-label="Español, coming soon, not yet available"
          style={{
            ...segBase, background: 'transparent', color: THEME.textMute,
            cursor: 'not-allowed', opacity: 0.7,
          }}>
          Español
          <span style={{
            fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: 'uppercase',
            padding: '2px 6px', borderRadius: 5, background: THEME.warnSoft, color: THEME.warnText,
          }}>Soon</span>
        </button>
      </div>
      <div style={{ fontSize: 11.5, color: THEME.textMute, marginTop: 10, lineHeight: 1.5 }}>
        Hello Court is in English today. Spanish is on the way.
      </div>
    </div>
  );
};

/* Small "Saved" inline confirm used by the profile + settings save actions.
   Demo no-op: shows a calm green confirm for a moment, no real persistence. */
const V3SavedToast = ({ show }) => {
  if (!show) return null;
  const isDark = THEME.mode === 'dark';
  return (
    <span role="status" style={{
      display: 'inline-flex', alignItems: 'center', gap: 7, marginLeft: 14,
      fontSize: 12.5, fontWeight: 600, color: THEME.success,
      animation: 'v3SavedIn 0.22s cubic-bezier(0.16,1,0.3,1)',
    }}>
      <style>{`@keyframes v3SavedIn { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: translateY(0); } }`}</style>
      <span className="material-symbols-rounded" aria-hidden="true"
        style={{ fontSize: 17, color: THEME.success, fontVariationSettings: "'opsz' 24" }}>check_circle</span>
      Saved
    </span>
  );
};

/* Shared screen-scoped helpers (tile / kLabel / mi) matching the V3Home set, so
   the account screens read as the same surface. */
const useV3AccountKit = () => {
  const isDark = THEME.mode === 'dark';
  const tile = (extra) => ({
    background: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(255,255,255,0.42)',
    border: `1px solid ${isDark ? 'rgba(255,255,255,0.075)' : 'rgba(20,30,50,0.10)'}`,
    borderRadius: 16, padding: '20px 20px',
    backdropFilter: 'blur(22px) saturate(1.5)', WebkitBackdropFilter: 'blur(22px) saturate(1.5)',
    boxShadow: isDark ? 'inset 0 1px 0 rgba(255,255,255,0.06), 0 8px 28px -12px rgba(0,0,0,0.4)' : 'inset 0 1px 0 rgba(255,255,255,0.6), 0 8px 24px -14px rgba(20,30,50,0.20)',
    ...(extra || {}),
  });
  const kLabel = { fontSize: 10.5, letterSpacing: 1.1, textTransform: 'uppercase', color: THEME.textMute, fontWeight: 600, margin: '0 0 9px', display: 'flex', alignItems: 'center', gap: 7 };
  const mi = (name, size = 14, color = THEME.textMute) => (
    <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: size, lineHeight: 1, color, fontVariationSettings: "'opsz' 24" }}>{name}</span>
  );
  return { isDark, tile, kLabel, mi };
};

/* Eyebrow + Fraunces title + lead intro, shared by the three account screens. */
const V3AccountHeader = ({ eyebrow, title, intro }) => (
  <div style={{ marginBottom: 22 }}>
    <div style={{ fontSize: 11, color: THEME.textMute, letterSpacing: 1.2, fontWeight: 600, textTransform: 'uppercase', marginBottom: 6 }}>{eyebrow}</div>
    <div style={{ fontFamily: 'Fraunces', fontSize: 28, color: THEME.text, fontWeight: 500, letterSpacing: -0.5, lineHeight: 1.15, marginBottom: intro ? 10 : 0 }}>
      {title}
    </div>
    {intro && <div style={{ fontSize: 13.5, color: THEME.textDim, lineHeight: 1.6, maxWidth: 600 }}>{intro}</div>}
  </div>
);

/* ── V3Profile ──────────────────────────────────────────────────────────────
   Avatar + name + email header (from the active `person`), a form built from the
   real form-fields (TextField for name/email/phone, Switch rows for notification
   prefs), a Save button (demo no-op with an inline "Saved" confirm), and a "Back
   to your case" text link. */
const V3Profile = ({ person, onScreen }) => {
  const { tile, kLabel, mi } = useV3AccountKit();
  const [saved, setSaved] = React.useState(false);
  const avatarGrad = `linear-gradient(150deg, ${THEME.blue}, ${THEME.blueDim})`;
  const p = person || { name: 'Your account', initials: 'You', email: '' };
  const guessedPhone = '(555) 014-2390';
  const onSave = () => { setSaved(true); window.setTimeout(() => setSaved(false), 2400); };

  return (
    <div className="v3-account-root" style={{ padding: '28px 36px 80px', overflow: 'hidden auto', height: '100%' }}>
      <style>{`
        .v3-account-form { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
        @container (max-width: 640px) { .v3-account-form { grid-template-columns: 1fr; } }
      `}</style>

      {/* Avatar + name + email header */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 24 }}>
        <span style={{ width: 64, height: 64, borderRadius: '50%', background: avatarGrad, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 22, fontWeight: 700, color: '#fff', flexShrink: 0 }}>
          {p.initials}
        </span>
        <div style={{ minWidth: 0 }}>
          <div style={{ fontFamily: 'Fraunces', fontSize: 26, color: THEME.text, fontWeight: 500, letterSpacing: -0.4, lineHeight: 1.15 }}>{p.name}</div>
          {p.email && <div style={{ fontSize: 13, color: THEME.textDim, marginTop: 2 }}>{p.email}</div>}
        </div>
      </div>

      <div style={tile({ marginBottom: 16 })}>
        <p style={kLabel}>{mi('badge')} Your details</p>
        <div className="v3-account-form" style={{ marginTop: 4 }}>
          <TextField label="Full name" defaultValue={p.name}/>
          <TextField label="Email" type="email" defaultValue={p.email} iconLeading="mail"/>
          <TextField label="Phone" type="tel" defaultValue={guessedPhone} iconLeading="call"/>
          <TextField label="ZIP code" defaultValue={'10456'}/>
        </div>
      </div>

      <div style={tile({ marginBottom: 20 })}>
        <p style={kLabel}>{mi('notifications')} How we reach you</p>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 6 }}>
          <Switch label="Email me case updates" helperText="Deadlines, new documents, and Guide replies." defaultChecked/>
          <Switch label="Text me reminders" helperText="A short SMS before a target date." defaultChecked/>
          <Switch label="Product news" helperText="Occasional notes about new Hello Court features."/>
        </div>
      </div>

      <div style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' }}>
        <Btn kind="primary" onClick={onSave}>Save changes</Btn>
        <V3SavedToast show={saved}/>
      </div>

      <div style={{ marginTop: 22 }}>
        <span onClick={() => onScreen && onScreen('home')}
          style={{ color: THEME.accent, fontSize: 13, cursor: 'pointer', fontWeight: 600 }}>
          Back to your case
        </span>
      </div>
    </div>
  );
};

/* ── V3Settings ─────────────────────────────────────────────────────────────
   Account + app settings: notification channels (email/SMS Switches),
   accessibility prefs (reduce motion Switch), a privacy/data section, the
   Language row (same dead-Español treatment as the nav), and Sign out at the
   bottom. */
const V3Settings = ({ onScreen }) => {
  const { isDark, tile, kLabel, mi } = useV3AccountKit();
  const [saved, setSaved] = React.useState(false);
  const onSave = () => { setSaved(true); window.setTimeout(() => setSaved(false), 2400); };

  return (
    <div className="v3-account-root" style={{ padding: '28px 36px 80px', overflow: 'hidden auto', height: '100%' }}>
      <style>{`
        .v3-settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
        @container (max-width: 720px) { .v3-settings-grid { grid-template-columns: 1fr; } }
      `}</style>

      <V3AccountHeader eyebrow="Account" title="Settings"
        intro="Control how Hello Court reaches you, how it looks and feels, and what happens to your information."/>

      <div className="v3-settings-grid">
        <div style={tile()}>
          <p style={kLabel}>{mi('notifications')} Notifications</p>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 6 }}>
            <Switch label="Email notifications" helperText="Deadlines, documents, and Guide replies." defaultChecked/>
            <Switch label="SMS notifications" helperText="Short text reminders before a target date." defaultChecked/>
            <Switch label="Weekly case summary" helperText="A Monday recap of where your case stands."/>
          </div>
        </div>

        <div style={tile()}>
          <p style={kLabel}>{mi('accessibility_new')} Accessibility</p>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 6 }}>
            <Switch label="Reduce motion" helperText="Calmer transitions and fewer animations."/>
            <Switch label="Larger text" helperText="Increase the base text size across screens."/>
            <Switch label="High-contrast mode" helperText="Stronger borders and bolder text."/>
          </div>
        </div>

        <div style={tile()}>
          <p style={kLabel}>{mi('translate')} Language</p>
          <div style={{ marginTop: 4 }}>
            <V3LanguageRow kLabel={kLabel} mi={mi} hideLabel/>
          </div>
        </div>

        <div style={tile()}>
          <p style={kLabel}>{mi('lock')} Privacy and data</p>
          <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.6, marginBottom: 14 }}>
            Your case information is yours. Download a copy any time, or close your account and remove your data.
          </div>
          <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
            <Btn kind="secondary" onClick={() => {}}>Download my data</Btn>
            <Btn kind="ghost" onClick={() => {}}>Close account</Btn>
          </div>
        </div>
      </div>

      <div style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap', marginTop: 20 }}>
        <Btn kind="primary" onClick={onSave}>Save settings</Btn>
        <V3SavedToast show={saved}/>
      </div>

      {/* Sign out — mirrors the nav dropdown's danger sign-out affordance. */}
      <div style={{ marginTop: 26 }}>
        <button type="button"
          onClick={() => onScreen && onScreen('welcome')}
          onMouseEnter={(e) => { e.currentTarget.style.background = isDark ? 'rgba(231,138,138,0.10)' : 'rgba(192,68,76,0.08)'; }}
          onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
          style={{
            display: 'inline-flex', alignItems: 'center', gap: 9, border: `1px solid ${isDark ? 'rgba(231,138,138,0.34)' : 'rgba(192,68,76,0.30)'}`,
            background: 'transparent', color: isDark ? '#E78A8A' : '#C0444C',
            fontFamily: 'inherit', fontSize: 13, fontWeight: 600, padding: '10px 16px',
            borderRadius: 999, cursor: 'pointer', transition: 'background 0.15s ease',
          }}>
          <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 17, fontVariationSettings: "'opsz' 24" }}>logout</span>
          Sign out
        </button>
      </div>
    </div>
  );
};

/* ── V3Help ─────────────────────────────────────────────────────────────────
   Help and guides hub: a short Fraunces intro, a responsive grid of topic
   tiles/links (how Hello Court works, filing basics, the free guides,
   contact/support), and the disclosure footer. Links to existing guide pages
   where they exist (the survival guide lives at /guides/survival-guide.html;
   partner-demo IS the web root, so the path is root-absolute, never
   /partner-demo/...). */
const V3Help = ({ onScreen }) => {
  const { isDark, kLabel, mi } = useV3AccountKit();

  // tiles: internal screens route via onScreen; external pages open by href.
  const topics = [
    { icon: 'explore', title: 'How Hello Court works', sub: 'A 2-minute tour of the whole flow, start to finish.', onClick: () => onScreen && onScreen('home') },
    { icon: 'gavel', title: 'Filing basics', sub: 'What filing and serving the other side actually means.', onClick: () => onScreen && onScreen('filing') },
    { icon: 'menu_book', title: 'Free survival guide', sub: 'The plain-language guide to handling your case. Printable.', href: '/guides/survival-guide.html' },
    { icon: 'folder_open', title: 'Gathering evidence', sub: 'What helps prove your case, and how to keep it organized.', onClick: () => onScreen && onScreen('evidence') },
    { icon: 'forum', title: 'Ask your Guide', sub: 'Open the chat any time for a plain-language answer.', onClick: () => onScreen && onScreen('home') },
    { icon: 'support_agent', title: 'Contact support', sub: 'Reach a real person at help@hellocourt.app.', href: 'mailto:help@hellocourt.app' },
  ];

  const tileStyle = (hover) => ({
    background: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(255,255,255,0.42)',
    border: `1px solid ${hover ? THEME.blue : (isDark ? 'rgba(255,255,255,0.075)' : 'rgba(20,30,50,0.10)')}`,
    borderRadius: 16, padding: '18px 18px', textAlign: 'left', cursor: 'pointer',
    fontFamily: 'inherit', color: 'inherit', width: '100%',
    backdropFilter: 'blur(22px) saturate(1.5)', WebkitBackdropFilter: 'blur(22px) saturate(1.5)',
    boxShadow: isDark ? 'inset 0 1px 0 rgba(255,255,255,0.06), 0 8px 28px -12px rgba(0,0,0,0.4)' : 'inset 0 1px 0 rgba(255,255,255,0.6), 0 8px 24px -14px rgba(20,30,50,0.20)',
    transition: 'border-color 0.18s ease, transform 0.18s ease',
    display: 'flex', flexDirection: 'column', gap: 10,
  });

  const Tile = ({ t }) => {
    const [hover, setHover] = React.useState(false);
    const open = () => { if (t.href) window.open(t.href, t.href.startsWith('mailto:') ? '_self' : '_blank'); else if (t.onClick) t.onClick(); };
    return (
      <button type="button" onClick={open}
        onMouseEnter={(e) => { setHover(true); e.currentTarget.style.transform = 'translateY(-2px)'; }}
        onMouseLeave={(e) => { setHover(false); e.currentTarget.style.transform = 'none'; }}
        style={tileStyle(hover)}>
        <span style={{
          width: 38, height: 38, borderRadius: 11, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          background: THEME.blueSoft, flexShrink: 0,
        }}>
          <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 20, color: THEME.accent, fontVariationSettings: "'opsz' 24" }}>{t.icon}</span>
        </span>
        <div>
          <div style={{ fontSize: 14.5, color: THEME.text, fontWeight: 600, marginBottom: 4 }}>{t.title}</div>
          <div style={{ fontSize: 12.5, color: THEME.textDim, lineHeight: 1.5 }}>{t.sub}</div>
        </div>
      </button>
    );
  };

  return (
    <div className="v3-account-root" style={{ padding: '28px 36px 80px', overflow: 'hidden auto', height: '100%' }}>
      <style>{`
        .v3-help-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
        @container (max-width: 900px) { .v3-help-grid { grid-template-columns: 1fr 1fr; } }
        @container (max-width: 560px) { .v3-help-grid { grid-template-columns: 1fr; } }
      `}</style>

      <V3AccountHeader eyebrow="Support" title="Help and guides"
        intro="Short, plain-language answers and step-by-step guides for handling your case yourself. Start anywhere, and your Guide is always one click away."/>

      <div className="v3-help-grid" style={{ marginBottom: 22 }}>
        {topics.map((t, i) => <Tile key={i} t={t}/>)}
      </div>

      <V3Disclosure/>
    </div>
  );
};

const V3Body = ({ matter, screen, jx = 'ny', emptyState = false, onChooseMatter, onOpenTriage, mode, onModeChange, onScreen, selectedCause, onCauseSelect }) => {
  // First-run / empty-state experience (Story 0.64 continuation). When the
  // user has no case yet, the MAIN content area shows the getting-started
  // launchpad instead of a seeded case home — short-circuited BEFORE any
  // matter/screen routing so it never falls through to V3Home. The primary
  // CTA reuses the sidebar's "Start a new case" action (onOpenTriage →
  // opens TriageIntake), keeping a single source of truth for that action.
  if (emptyState) {
    return <V3FirstRun jx={jx} onStartCase={onOpenTriage}/>;
  }
  if (screen === 'welcome') {
    return <V3Welcome onChooseMatter={onChooseMatter} onOpenTriage={onOpenTriage} mode={mode} onModeChange={onModeChange}/>;
  }
  // Account-level screens (reached from the floating nav profile dropdown).
  // Routed BEFORE matter-specific branches so they are matter-agnostic and not
  // intercepted by the newcase / tier-2 short-circuits below.
  if (screen === 'profile' || screen === 'settings' || screen === 'help') {
    // Derive the active user the same way V3GlassNav does (active matter's
    // client → name/initials/email), so the profile header matches the nav.
    const mm = V3_MATTERS[matter];
    const person = (mm && mm.client)
      ? {
          name: mm.client.name || 'Client',
          initials: mm.client.initials || 'C',
          email: mm.client.name
            ? mm.client.name.toLowerCase().replace(/[^a-z]+/g, '.').replace(/^\.|\.$/g, '') + '@email.com'
            : '',
        }
      : { name: 'Your account', initials: 'You', email: '' };
    if (screen === 'profile') return <V3Profile person={person} onScreen={onScreen}/>;
    if (screen === 'settings') return <V3Settings onScreen={onScreen}/>;
    return <V3Help onScreen={onScreen}/>;
  }
  const m = V3_MATTERS[matter];
  if (m.comingSoon) return <V3ComingSoon matter={matter}/>;
  // Newcase only has content for home + the workflow steps (theory, evidence,
  // damages). Other sidebar nav items render a "coming alive as you make
  // progress" placeholder instead of falling through to V3Home, which would
  // re-render the focused new-case home regardless of screen.
  if (matter === 'newcase') {
    // Story 0.49 Phase D — wage-NY end-to-end now wired for the full file-
    // from-scratch flow: theory → evidence → damages → documents → filing →
    // courtroom. Per-newcase datasets render real wage content (was Patel
    // auto-repair data, wrong domain). Other surfaces still placeholder.
    const newcaseSupported = ['home', 'matter', 'theory', 'evidence', 'damages', 'documents', 'filing', 'courtroom'];
    if (!newcaseSupported.includes(screen)) {
      return <V3NewcaseEmpty screen={screen} onScreen={onScreen}/>;
    }
  }
  if (m.tier === 2) {
    if (screen === 'pitch') return <V3LawyerPitch matter={matter}/>;
    if (screen === 'audit') return <V3AuditLog matter={matter}/>;
    if (screen === 'evidence') return <V3Evidence matter={matter} onScreen={onScreen}/>;
    return <V3Home matter={matter} screen={screen} onScreen={onScreen}/>;
  }
  switch (screen) {
    case 'theory': return <V3Theory matter={matter} onScreen={onScreen} selectedCause={selectedCause} onCauseSelect={onCauseSelect}/>;
    case 'evidence': return <V3Evidence matter={matter} onScreen={onScreen}/>;
    case 'damages': return <V3Damages matter={matter} onScreen={onScreen} selectedCause={selectedCause}/>;
    case 'documents': return <V3Documents matter={matter} onScreen={onScreen} selectedCause={selectedCause}/>;
    case 'filing': return <V3Filing matter={matter} onScreen={onScreen}/>;
    case 'courtroom': return <V3Courtroom matter={matter} onScreen={onScreen} selectedCause={selectedCause}/>;
    case 'deliverables': return <V3Deliverables matter={matter}/>;
    case 'audit': return <V3AuditLog matter={matter}/>;
    case 'readiness':
    case 'matter':
    case 'home':
    default: return <V3Home matter={matter} screen={screen} onScreen={onScreen}/>;
  }
};

window.V3_MATTERS = V3_MATTERS;
window.V3_NAV = V3_NAV;
window.V3Sidebar = V3Sidebar;
window.V3MatterSwitcher = V3MatterSwitcher;
window.V3GlassNav = V3GlassNav;
window.V3Body = V3Body;
window.V3Welcome = V3Welcome;
window.V3FirstRun = V3FirstRun;
window.V3CrisisOverlay = V3CrisisOverlay;
