// idea-3-squads.jsx - Study Squads MVP.
// Invite-only structured sessions: no chat, no DMs, no video.

const SQ_PHASES = [
  { icon: 'target', label: 'Check-in', copy: 'Each student sets one exam-practice goal for the session.' },
  { icon: 'pencil', label: 'Attempt', copy: 'Students answer the same question pack individually first.' },
  { icon: 'book', label: 'Reveal', copy: 'Model answers and rubrics unlock after the attempt window.' },
  { icon: 'check', label: 'Repair', copy: 'Everyone rewrites one weak response before the session closes.' },
];

const SQ_SESSION_PHASES = [
  { key: 'scheduled', label: 'Scheduled', icon: 'clock', copy: 'The session is ready, but students have not checked in yet.' },
  { key: 'check_in', label: 'Check-in', icon: 'flag', copy: 'Students arrive and set one goal for the question pack.' },
  { key: 'attempt', label: 'Attempt', icon: 'pencil', copy: 'Students answer independently before seeing anyone else.' },
  { key: 'reveal', label: 'Reveal', icon: 'book', copy: 'Responses and model-answer comparison can unlock.' },
  { key: 'repair', label: 'Repair', icon: 'pencil', copy: 'Students rewrite one weak response using model and peer feedback.' },
  { key: 'complete', label: 'Complete', icon: 'check', copy: 'The session is closed and ready for review.' },
];

const SQ_GUARDS = [
  'Invite-only squads of 3-5 students (target 4)',
  'Avatar/display-name identity only',
  'No open chat, direct messages, voice, or video',
  'Answers stay private until reveal phase',
];

const SQ_SUBJECTS = [
  { value: 'biology', label: 'Biology' },
  { value: 'chemistry', label: 'Chemistry' },
  { value: 'physics', label: 'Physics' },
  { value: 'maths-standard', label: 'Maths Standard' },
  { value: 'maths-advanced', label: 'Maths Advanced' },
];

const SQ_DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];

// Timed attempt window (client-side). 2 MC + 2 SA under exam-like time pressure.
const SQ_ATTEMPT_MINUTES = 12;

// Answer JSON budget. The DB column check is char_length(answer_text) 1..8000,
// so the SERIALISED JSON (attempt + repair) must fit within 8000. Field caps are
// chosen so a full attempt (2 SA) plus a repair always serialises under budget
// without ever slicing the JSON string. Textarea maxLengths match these caps.
const SQ_MAX_ANSWER_JSON = 8000;
const SQ_SA_CAP = 2500;      // per short-answer
const SQ_REPAIR_CAP = 2000;  // repaired answer

const SQ_REAL_PACKS = [
  { ref: 'biology-y11-module1', title: 'Biology Y11 Module 1', path: 'subjects/biology/year11/module1/question-bank-data.js', prefix: 'bio-y11-m1-l' },
  { ref: 'biology-y11-module2', title: 'Biology Y11 Module 2', path: 'subjects/biology/year11/module2/question-bank-data.js', prefix: 'bio-y11-m2-l' },
  { ref: 'biology-y11-module3', title: 'Biology Y11 Module 3', path: 'subjects/biology/year11/module3/question-bank-data.js', prefix: 'bio-y11-m3-l' },
  { ref: 'biology-y11-module4', title: 'Biology Y11 Module 4', path: 'subjects/biology/year11/module4/question-bank-data.js', prefix: 'bio-y11-m4-l' },
  { ref: 'biology-y12-module5', title: 'Biology Y12 Module 5', path: 'subjects/biology/year12/module5/question-bank-data.js', prefix: 'bio-y12-m5-l' },
  { ref: 'biology-y12-module6', title: 'Biology Y12 Module 6', path: 'subjects/biology/year12/module6/question-bank-data.js', prefix: 'bio-y12-m6-l' },
  { ref: 'biology-y12-module7', title: 'Biology Y12 Module 7', path: 'subjects/biology/year12/module7/question-bank-data.js', prefix: 'bio-y12-m7-l' },
  { ref: 'biology-y12-module8', title: 'Biology Y12 Module 8', path: 'subjects/biology/year12/module8/question-bank-data.js', prefix: 'bio-y12-m8-l' },
  { ref: 'chemistry-y11-module1', title: 'Chemistry Y11 Module 1', path: 'subjects/chemistry/year11/module1/question-bank-data.js', prefix: 'chem-y11-m1-l' },
  { ref: 'chemistry-y11-module2', title: 'Chemistry Y11 Module 2', path: 'subjects/chemistry/year11/module2/question-bank-data.js', prefix: 'chem-y11-m2-l' },
  { ref: 'chemistry-y11-module3', title: 'Chemistry Y11 Module 3', path: 'subjects/chemistry/year11/module3/question-bank-data.js', prefix: 'chem-y11-m3-l' },
  { ref: 'chemistry-y11-module4', title: 'Chemistry Y11 Module 4', path: 'subjects/chemistry/year11/module4/question-bank-data.js', prefix: 'chem-y11-m4-l' },
  { ref: 'chemistry-y12-module5', title: 'Chemistry Y12 Module 5', path: 'subjects/chemistry/year12/module5/question-bank-data.js', prefix: 'chem-y12-m5-l' },
  { ref: 'chemistry-y12-module6', title: 'Chemistry Y12 Module 6', path: 'subjects/chemistry/year12/module6/question-bank-data.js', prefix: 'chem-y12-m6-l' },
  { ref: 'chemistry-y12-module7', title: 'Chemistry Y12 Module 7', path: 'subjects/chemistry/year12/module7/question-bank-data.js', prefix: 'chem-y12-m7-l' },
  { ref: 'chemistry-y12-module8', title: 'Chemistry Y12 Module 8', path: 'subjects/chemistry/year12/module8/question-bank-data.js', prefix: 'chem-y12-m8-l' },
  { ref: 'maths-advanced-y11-module1', title: 'Maths Advanced Y11 Module 1', path: 'subjects/maths-advanced/year11/module1/question-bank-data.js', prefix: 'maths-adv-y11-m1' },
  { ref: 'maths-advanced-y11-module2', title: 'Maths Advanced Y11 Module 2', path: 'subjects/maths-advanced/year11/module2/question-bank-data.js', prefix: 'maths-adv-y11-m2' },
  { ref: 'maths-advanced-y11-module3', title: 'Maths Advanced Y11 Module 3', path: 'subjects/maths-advanced/year11/module3/question-bank-data.js', prefix: 'maths-adv-y11-m3-l' },
  { ref: 'maths-y7-unit3', title: 'Maths Y7 Unit 3', path: 'subjects/maths/year7/unit3/question-bank-data.js', prefix: 'maths-y7-u3' },
  { ref: 'maths-y8-unit1', title: 'Maths Y8 Unit 1', path: 'subjects/maths/year8/unit1/question-bank-data.js', prefix: 'maths-y8-u1' },
  { ref: 'maths-standard-y11-module1', title: 'Maths Standard Y11 Module 1', path: 'subjects/maths-standard/year11/module1/question-bank-data.js', prefix: 'maths-std-y11-m1-l' },
  { ref: 'maths-standard-y11-module2', title: 'Maths Standard Y11 Module 2', path: 'subjects/maths-standard/year11/module2/question-bank-data.js', prefix: 'ms-y11-m2' },
  { ref: 'maths-standard-y11-module3', title: 'Maths Standard Y11 Module 3', path: 'subjects/maths-standard/year11/module3/question-bank-data.js', prefix: 'maths-std-y11-m3-l' },
  { ref: 'maths-standard-y11-module4', title: 'Maths Standard Y11 Module 4', path: 'subjects/maths-standard/year11/module4/question-bank-data.js', prefix: 'ms-y11-m4' },
  { ref: 'maths-standard-y12-module5', title: 'Maths Standard Y12 Module 5', path: 'subjects/maths-standard/year12/module5/question-bank-data.js', prefix: 'maths-std-y12-m5-l' },
  { ref: 'maths-standard-y12-module6', title: 'Maths Standard Y12 Module 6', path: 'subjects/maths-standard/year12/module6/question-bank-data.js', prefix: 'maths-std-y12-m6-l' },
  { ref: 'maths-standard-y12-module7', title: 'Maths Standard Y12 Module 7', path: 'subjects/maths-standard/year12/module7/question-bank-data.js', prefix: 'ms-y12-m7' },
  { ref: 'maths-standard-y12-module8', title: 'Maths Standard Y12 Module 8', path: 'subjects/maths-standard/year12/module8/question-bank-data.js', prefix: 'ms-y12-m8' },
  { ref: 'physics-y11-module1', title: 'Physics Y11 Module 1', path: 'subjects/physics/year11/module1/question-bank-data.js', prefix: 'phys-y11-m1' },
  { ref: 'physics-y11-module2', title: 'Physics Y11 Module 2', path: 'subjects/physics/year11/module2/question-bank-data.js', prefix: 'phys-y11-m2-l' },
  { ref: 'physics-y11-module3', title: 'Physics Y11 Module 3', path: 'subjects/physics/year11/module3/question-bank-data.js', prefix: 'phys-y11-m3-l' },
  { ref: 'physics-y11-module4', title: 'Physics Y11 Module 4', path: 'subjects/physics/year11/module4/question-bank-data.js', prefix: 'phys-y11-m4-l' },
  { ref: 'physics-y12-module5', title: 'Physics Y12 Module 5', path: 'subjects/physics/year12/module5/question-bank-data.js', prefix: 'phys-y12-m5-l' },
  { ref: 'physics-y12-module6', title: 'Physics Y12 Module 6', path: 'subjects/physics/year12/module6/question-bank-data.js', prefix: 'phys-y12-m6-l' },
  { ref: 'physics-y12-module7', title: 'Physics Y12 Module 7', path: 'subjects/physics/year12/module7/question-bank-data.js', prefix: 'phys-y12-m7-l' },
  { ref: 'physics-y12-module8', title: 'Physics Y12 Module 8', path: 'subjects/physics/year12/module8/question-bank-data.js', prefix: 'phys-y12-m8-l' },
];

const SQ_PACK_ALIASES = {
  'bio-y12-m5': 'biology-y12-module5',
  'bio-y12-m7': 'biology-y12-module7',
  'chem-y11-m3': 'chemistry-y11-module3',
  'chem-y12-m5': 'chemistry-y12-module5',
  'chem-y12-m6': 'chemistry-y12-module6',
  'physics-y12-m5': 'physics-y12-module5',
  'maths-advanced-y11-m3': 'maths-advanced-y11-module3',
  'maths-standard-y11-m3': 'maths-standard-y11-module3',
};

const SQ_QUESTION_PACKS = {
  'manual-pack-1': {
    title: 'Practice pack',
    question: 'Answer the assigned practice questions, then choose one response to compare during reveal.',
    model: 'Use the official class notes or teacher-provided solution as the model response for this pack.',
    rubric: ['Addresses the command verb', 'Uses relevant syllabus terms', 'Links evidence to the final judgement'],
  },
  'bio-y12-m7-pack-01': {
    title: 'Immune response short answer',
    question: 'Explain how antibodies help the body respond to a pathogen.',
    model: 'Antibodies are specific proteins produced by B cells that bind to antigens on a pathogen. This can neutralise the pathogen, tag it for phagocytosis, or activate other immune responses.',
    rubric: ['Identifies antibodies as specific proteins', 'Links antibody binding to antigens', 'Explains at least one outcome such as neutralisation or phagocytosis'],
  },
};

function StudySquads() {
  const db = window.HSC && window.HSC.supabase;
  const auth = window.HSC && window.HSC.auth;
  const [authReady, setAuthReady] = React.useState(!auth);
  const [user, setUser] = React.useState(null);
  const [squads, setSquads] = React.useState([]);
  const [sessions, setSessions] = React.useState([]);
  const [answers, setAnswers] = React.useState([]);
  const [invites, setInvites] = React.useState([]);
  const [attendance, setAttendance] = React.useState([]);
  const [peerReviews, setPeerReviews] = React.useState([]);
  const [memberBlocks, setMemberBlocks] = React.useState([]);
  const [aiFeedback, setAiFeedback] = React.useState([]);
  const [questionPack, setQuestionPack] = React.useState(null);
  const [questionPackState, setQuestionPackState] = React.useState('');
  const [loadingSquads, setLoadingSquads] = React.useState(false);
  const [loadingSessions, setLoadingSessions] = React.useState(false);
  const [saveState, setSaveState] = React.useState('');
  const [sessionSaveState, setSessionSaveState] = React.useState('');
  const [phaseSaveState, setPhaseSaveState] = React.useState('');
  const [answerSaveState, setAnswerSaveState] = React.useState('');
  const [inviteSaveState, setInviteSaveState] = React.useState('');
  const [attendanceSaveState, setAttendanceSaveState] = React.useState('');
  const [peerSaveState, setPeerSaveState] = React.useState('');
  const [reportSaveState, setReportSaveState] = React.useState('');
  const [blockSaveState, setBlockSaveState] = React.useState('');
  const [aiSaveState, setAiSaveState] = React.useState('');
  const [joinState, setJoinState] = React.useState('');
  const [selectedSessionId, setSelectedSessionId] = React.useState('');
  const [error, setError] = React.useState('');
  const [answerText, setAnswerText] = React.useState('');
  const [commitmentText, setCommitmentText] = React.useState('');
  const [peerDrafts, setPeerDrafts] = React.useState({});
  // Structured attempt (2 MC + 2 SA): drafts keyed by question id.
  const [attemptDraft, setAttemptDraft] = React.useState({ mc: {}, sa: {} });
  const [nowTs, setNowTs] = React.useState(Date.now());
  // Repair phase: which of the student's own answers they rewrite, and the rewrite.
  const [repairTarget, setRepairTarget] = React.useState('');
  const [repairText, setRepairText] = React.useState('');
  const [repairSaveState, setRepairSaveState] = React.useState('');
  const [joinCode, setJoinCode] = React.useState('');
  const [reportForm, setReportForm] = React.useState({
    category: 'safety',
    detail: '',
  });
  const [form, setForm] = React.useState({
    name: 'Exam practice squad',
    subject: 'biology',
    year_group: '12',
    module_label: 'Module focus',
    max_members: '4',
    recurring_days: ['Tue', 'Thu'],
    recurring_time_local: '19:30',
  });
  const [sessionForm, setSessionForm] = React.useState({
    squad_id: '',
    title: 'Question pack practice',
    date: nextLocalDate(),
    time: '19:30',
    question_pack_ref: 'manual-pack-1',
  });
  const [inviteForm, setInviteForm] = React.useState({
    squad_id: '',
    max_uses: '4',
  });

  React.useEffect(() => {
    let alive = true;
    async function init() {
      if (!auth) {
        setAuthReady(true);
        return;
      }
      const resolvedUser = await auth.ready;
      if (!alive) return;
      setUser(resolvedUser || null);
      setAuthReady(true);
    }
    init();
    return () => { alive = false; };
  }, []);

  React.useEffect(() => {
    if (!user || !db) {
      setSquads([]);
      return;
    }
    loadSquads();
  }, [user, db]);

  React.useEffect(() => {
    if (!user || !db) {
      setSessions([]);
      return;
    }
    loadSessions();
    loadInvites();
  }, [user, db]);

  React.useEffect(() => {
    if (!user || !db || !selectedSessionId) {
      setAnswers([]);
      setAnswerText('');
      setAttendance([]);
      setPeerReviews([]);
      setMemberBlocks([]);
      setAiFeedback([]);
      setCommitmentText('');
      setPeerDrafts({});
      setRepairTarget('');
      setRepairText('');
      return;
    }
    // Clear repair state up front so nothing stale flashes across a session
    // switch; loadAnswers re-hydrates it deterministically for this session.
    setRepairTarget('');
    setRepairText('');
    loadAnswers(selectedSessionId);
    loadAttendance(selectedSessionId);
    loadPeerReviews(selectedSessionId);
    loadMemberBlocks(selectedSessionId);
    loadAiFeedback(selectedSessionId);
  }, [user, db, selectedSessionId, sessions]);

  React.useEffect(() => {
    if (!selectedSessionId || !sessions.length) {
      setQuestionPack(null);
      setQuestionPackState('');
      return;
    }
    const session = sessions.find(item => item.id === selectedSessionId) || sessions[0];
    loadQuestionPack(session && session.question_pack_ref);
  }, [selectedSessionId, sessions]);

  async function loadSquads() {
    if (!db || !user) return;
    setLoadingSquads(true);
    const result = await db.from('study_squads')
      .select('id, name, subject, year_group, module_label, status, max_members, recurring_days, recurring_time_local, created_at, created_by')
      .order('created_at', { ascending: false })
      .limit(12);
    setLoadingSquads(false);
    if (result.error) {
      setError('Could not load your squads yet.');
      return;
    }
    const nextSquads = result.data || [];
    setSquads(nextSquads);
    setSessionForm(prev => prev.squad_id || !nextSquads.length
      ? prev
      : Object.assign({}, prev, {
        squad_id: nextSquads[0].id,
        question_pack_ref: chooseDefaultPackForSquad(nextSquads[0]) || prev.question_pack_ref
      }));
    setInviteForm(prev => prev.squad_id || !nextSquads.length
      ? prev
      : Object.assign({}, prev, { squad_id: nextSquads[0].id, max_uses: String(nextSquads[0].max_members || 4) }));
  }

  async function loadSessions() {
    if (!db || !user) return;
    setLoadingSessions(true);
    const result = await db.from('study_sessions')
      .select('id, squad_id, title, scheduled_for, phase, question_pack_ref, created_at, updated_at')
      .order('scheduled_for', { ascending: true, nullsFirst: false })
      .limit(20);
    setLoadingSessions(false);
    if (result.error) {
      setError('Could not load squad sessions yet.');
      return;
    }
    const nextSessions = result.data || [];
    setSessions(nextSessions);
    setSelectedSessionId(prev => prev || (nextSessions[0] && nextSessions[0].id) || '');
  }

  async function loadAnswers(sessionId) {
    if (!db || !user || !sessionId) return;
    const result = await db.from('study_session_answers')
      .select('id, session_id, user_id, answer_text, submitted_at')
      .eq('session_id', sessionId)
      .order('submitted_at', { ascending: true });

    if (result.error) {
      setAnswers([]);
      return;
    }

    const nextAnswers = result.data || [];
    setAnswers(nextAnswers);
    const ownAnswer = nextAnswers.find(answer => answer.user_id === user.id);
    setAnswerText(ownAnswer ? ownAnswer.answer_text : '');
    // Hydrate the structured attempt draft + any existing repair from own row.
    // Repair state is set deterministically for THIS session (never carried over
    // stale from a previously-selected session), and the target is validated
    // against the current SA ids so a stale/invalid target can't persist.
    if (ownAnswer) {
      const parsed = parseSquadAnswer(ownAnswer.answer_text);
      const mc = {};
      const sa = {};
      (parsed.mc || []).forEach(m => { if (m.choiceIndex != null) mc[m.qid] = m.choiceIndex; });
      (parsed.sa || []).forEach(s => { sa[s.qid] = s.text || ''; });
      setAttemptDraft({ mc: mc, sa: sa });

      const validTargets = (parsed.sa || []).map(s => 'sa:' + s.qid);
      const repairTargetValid = parsed.repair && parsed.repair.text
        && validTargets.indexOf(parsed.repair.target) !== -1;
      setRepairTarget(repairTargetValid ? parsed.repair.target : '');
      setRepairText(repairTargetValid ? (parsed.repair.text || '') : '');
    } else {
      setAttemptDraft({ mc: {}, sa: {} });
      setRepairTarget('');
      setRepairText('');
    }
  }

  async function loadAttendance(sessionId) {
    if (!db || !user || !sessionId) return;
    const result = await db.from('study_session_attendance')
      .select('id, session_id, user_id, status, checked_in_at, completed_at, commitment_text')
      .eq('session_id', sessionId)
      .order('updated_at', { ascending: false });

    if (result.error) {
      setAttendance([]);
      return;
    }

    const nextAttendance = result.data || [];
    setAttendance(nextAttendance);
    const ownAttendance = nextAttendance.find(row => row.user_id === user.id);
    setCommitmentText(ownAttendance && ownAttendance.commitment_text ? ownAttendance.commitment_text : '');
  }

  async function loadPeerReviews(sessionId) {
    if (!db || !user || !sessionId) return;
    const result = await db.from('study_session_peer_reviews')
      .select('id, session_id, answer_id, reviewer_id, reviewed_user_id, rubric_hits, improvement_text, updated_at')
      .eq('session_id', sessionId)
      .order('updated_at', { ascending: false });

    if (result.error) {
      setPeerReviews([]);
      return;
    }

    const nextReviews = result.data || [];
    setPeerReviews(nextReviews);
    setPeerDrafts(prev => {
      const next = Object.assign({}, prev);
      nextReviews.filter(review => review.reviewer_id === user.id).forEach(review => {
        next[review.answer_id] = {
          rubric_hits: review.rubric_hits || {},
          improvement_text: review.improvement_text || ''
        };
      });
      return next;
    });
  }

  async function loadMemberBlocks(sessionId) {
    if (!db || !user || !sessionId) return;
    const session = sessions.find(item => item.id === sessionId);
    if (!session) return;
    const result = await db.from('study_squad_member_blocks')
      .select('id, blocker_id, blocked_user_id, squad_id, session_id, reason, created_at')
      .eq('blocker_id', user.id)
      .eq('squad_id', session.squad_id);

    if (result.error) {
      setMemberBlocks([]);
      return;
    }

    setMemberBlocks(result.data || []);
  }

  async function loadAiFeedback(sessionId) {
    if (!db || !user || !sessionId) return;
    const result = await db.from('study_session_ai_feedback')
      .select('id, session_id, answer_id, user_id, requested_by, model, mark_estimate, confidence, strengths, missing_criteria, improvement, exemplar, safety_note, created_at, updated_at')
      .eq('session_id', sessionId)
      .eq('requested_by', user.id)
      .order('updated_at', { ascending: false });

    if (result.error) {
      setAiFeedback([]);
      return;
    }

    setAiFeedback(result.data || []);
  }

  async function loadInvites() {
    if (!db || !user) return;
    const result = await db.from('study_squad_invites')
      .select('id, squad_id, invite_code, status, max_uses, uses_count, expires_at, created_at')
      .order('created_at', { ascending: false })
      .limit(20);

    if (result.error) {
      setInvites([]);
      return;
    }

    setInvites(result.data || []);
  }

  function updateForm(key, value) {
    setForm(prev => Object.assign({}, prev, { [key]: value }));
  }

  function updateSessionForm(key, value) {
    setSessionForm(prev => Object.assign({}, prev, { [key]: value }));
  }

  function updateSessionSquad(squadId) {
    const squad = squads.find(item => item.id === squadId);
    setSessionForm(prev => Object.assign({}, prev, {
      squad_id: squadId,
      question_pack_ref: chooseDefaultPackForSquad(squad) || prev.question_pack_ref
    }));
  }

  function updateInviteForm(key, value) {
    setInviteForm(prev => Object.assign({}, prev, { [key]: value }));
  }

  function toggleDay(day) {
    setForm(prev => {
      const days = prev.recurring_days.includes(day)
        ? prev.recurring_days.filter(d => d !== day)
        : prev.recurring_days.concat(day).slice(0, 2);
      return Object.assign({}, prev, { recurring_days: days });
    });
  }

  async function createSquad() {
    if (!db || !user) return;
    setError('');
    setSaveState('saving');

    const payload = {
      created_by: user.id,
      name: cleanSquadText(form.name, 60) || 'Exam practice squad',
      subject: form.subject,
      year_group: Number(form.year_group) || 12,
      module_label: cleanSquadText(form.module_label, 40) || null,
      visibility: 'invite_only',
      status: 'forming',
      max_members: Math.max(3, Math.min(5, Number(form.max_members) || 4)),
      recurring_days: form.recurring_days,
      recurring_time_local: form.recurring_time_local || null,
      timezone: 'Australia/Sydney'
    };

    const created = await db.from('study_squads').insert(payload).select('id').single();
    if (created.error || !created.data) {
      setSaveState('');
      setError('Could not create the squad. Try again in a moment.');
      return;
    }

    const member = await db.from('study_squad_members').insert({
      squad_id: created.data.id,
      user_id: user.id,
      role: 'owner',
      status: 'active',
      joined_at: new Date().toISOString()
    });

    if (member.error) {
      setSaveState('');
      setError('Squad was created, but owner membership could not be saved yet.');
      await loadSquads();
      return;
    }

    setSaveState('saved');
    await loadSquads();
    await loadSessions();
  }

  async function createSession() {
    if (!db || !user || !sessionForm.squad_id) return;
    setError('');
    setSessionSaveState('saving');

    const scheduledFor = makeScheduledIso(sessionForm.date, sessionForm.time);
    const payload = {
      squad_id: sessionForm.squad_id,
      created_by: user.id,
      title: cleanSquadText(sessionForm.title, 120) || 'Question pack practice',
      scheduled_for: scheduledFor,
      phase: 'scheduled',
      question_pack_ref: cleanSquadText(sessionForm.question_pack_ref, 80) || null
    };

    const result = await db.from('study_sessions').insert(payload);
    if (result.error) {
      setSessionSaveState('');
      setError('Could not schedule that session yet.');
      return;
    }

    setSessionSaveState('saved');
    await loadSessions();
  }

  async function createInvite() {
    if (!db || !user || !inviteForm.squad_id) return;
    setError('');
    setInviteSaveState('saving');

    const payload = {
      squad_id: inviteForm.squad_id,
      created_by: user.id,
      invite_code: makeInviteCode(),
      status: 'active',
      max_uses: Math.max(1, Math.min(5, Number(inviteForm.max_uses) || 4)),
      uses_count: 0
    };

    const result = await db.from('study_squad_invites').insert(payload);
    if (result.error) {
      setInviteSaveState('');
      setError('Could not create that invite code yet.');
      return;
    }

    setInviteSaveState('saved');
    await loadInvites();
  }

  async function revokeInvite(invite) {
    if (!db || !user || !invite) return;
    setError('');
    setInviteSaveState('saving');
    const result = await db.from('study_squad_invites')
      .update({ status: 'revoked', updated_at: new Date().toISOString() })
      .eq('id', invite.id);

    if (result.error) {
      setInviteSaveState('');
      setError('Could not revoke that invite yet.');
      return;
    }

    setInviteSaveState('saved');
    await loadInvites();
  }

  async function joinSquadByCode() {
    if (!db || !user) return;
    const code = cleanInviteCode(joinCode);
    if (!code) return;
    setError('');
    setJoinState('joining');

    const result = await db.rpc('join_study_squad_by_code', { p_invite_code: code });
    if (result.error) {
      setJoinState('');
      setError('That invite code could not be used.');
      return;
    }

    setJoinState('joined');
    setJoinCode('');
    await loadSquads();
    await loadSessions();
    await loadInvites();
  }

  async function updateSessionPhase(session, phase) {
    if (!db || !user || !session || !phase) return;
    setError('');
    setPhaseSaveState('saving');

    const stampedAt = new Date().toISOString();
    const result = await db.from('study_sessions')
      .update({ phase: phase, updated_at: stampedAt })
      .eq('id', session.id);

    if (result.error) {
      setPhaseSaveState('');
      setError('Could not update that session phase yet.');
      return;
    }

    // Keep updated_at in sync locally so the shared attempt timer (derived from
    // updated_at) starts immediately for the owner without a full reload.
    setSessions(prev => prev.map(item => item.id === session.id
      ? Object.assign({}, item, { phase: phase, updated_at: stampedAt })
      : item));
    setSelectedSessionId(session.id);
    setPhaseSaveState('saved');
    await loadAnswers(session.id);
    await loadAttendance(session.id);
    await loadPeerReviews(session.id);
    await loadAiFeedback(session.id);
  }

  async function submitAnswer() {
    if (!db || !user || !selectedSession || selectedSession.phase !== 'attempt') return;
    // Serialise the structured 2 MC + 2 SA attempt to JSON in answer_text.
    // SEAM: photo capture → AI transcription. Today students TYPE their SA
    // answers into attemptDraft.sa. When the transcription edge function is
    // added later, it fills the same attemptDraft.sa[qid] strings from a photo
    // and this submit path is unchanged.
    const q = attemptQuestions;
    const answerObj = {
      v: 1,
      mc: q.mc.map(m => ({ qid: m.qid, choiceIndex: attemptDraft.mc[m.qid] == null ? null : attemptDraft.mc[m.qid] })),
      sa: q.sa.map(s => ({ qid: s.qid, text: cleanAnswerText(attemptDraft.sa[s.qid] || '') })),
      repair: null
    };
    const serialised = serialiseSquadAnswer(answerObj);
    if (!answerHasContent(answerObj)) return;
    setError('');
    setAnswerSaveState('saving');

    // The UPDATE privilege is now column-scoped to answer_text only, so we must
    // not upsert (which would try to write visibility/submitted_at on conflict).
    // Insert on first submit (other columns take their DB defaults); update only
    // answer_text on re-submit. Fall back to update if a row already exists.
    const existing = answers.find(a => a.user_id === user.id);
    let result;
    if (existing) {
      result = await db.from('study_session_answers')
        .update({ answer_text: serialised })
        .eq('session_id', selectedSession.id)
        .eq('user_id', user.id);
    } else {
      result = await db.from('study_session_answers')
        .insert({ session_id: selectedSession.id, user_id: user.id, answer_text: serialised });
      if (result.error && result.error.code === '23505') {
        result = await db.from('study_session_answers')
          .update({ answer_text: serialised })
          .eq('session_id', selectedSession.id)
          .eq('user_id', user.id);
      }
    }

    if (result.error) {
      setAnswerSaveState('');
      setError('Could not save your answer yet.');
      return;
    }

    setAnswerSaveState('saved');
    await loadAnswers(selectedSession.id);
  }

  // Repair phase: rewrite ONE weak answer using the feedback received. Stored
  // inside the same answer row's JSON (original attempt preserved), written via
  // the repair-phase UPDATE policy.
  async function saveRepair() {
    if (!db || !user || !selectedSession || selectedSession.phase !== 'repair') return;
    const rewrite = cleanAnswerText(repairText);
    if (!repairTarget || !rewrite) return;
    const ownRow = answers.find(a => a.user_id === user.id);
    if (!ownRow) {
      setError('Submit an answer during the attempt phase before repairing.');
      return;
    }
    const parsed = parseSquadAnswer(ownRow.answer_text);
    // Validate the target against the current answer's SA ids so a stale target
    // can never be written. The server trigger also preserves mc/sa regardless.
    const validTargets = (parsed.sa || []).map(s => 'sa:' + s.qid);
    if (validTargets.indexOf(repairTarget) === -1) {
      setError('Choose which short answer you are repairing.');
      return;
    }
    parsed.repair = { target: repairTarget, text: rewrite, at: new Date().toISOString() };

    setError('');
    setRepairSaveState('saving');
    const result = await db.from('study_session_answers')
      .update({ answer_text: serialiseSquadAnswer(parsed) })
      .eq('session_id', selectedSession.id)
      .eq('user_id', user.id);

    if (result.error) {
      setRepairSaveState('');
      setError('Could not save your repaired answer yet.');
      return;
    }
    setRepairSaveState('saved');
    await loadAnswers(selectedSession.id);
  }

  function updateAttemptMc(qid, choiceIndex) {
    setAttemptDraft(prev => Object.assign({}, prev, { mc: Object.assign({}, prev.mc, { [qid]: choiceIndex }) }));
  }
  function updateAttemptSa(qid, text) {
    setAttemptDraft(prev => Object.assign({}, prev, { sa: Object.assign({}, prev.sa, { [qid]: text }) }));
  }

  async function markAttendance(status) {
    if (!db || !user || !selectedSession) return;
    setError('');
    setAttendanceSaveState('saving');

    const existing = attendance.find(row => row.user_id === user.id);
    const now = new Date().toISOString();
    const payload = {
      session_id: selectedSession.id,
      user_id: user.id,
      status: status,
      checked_in_at: existing && existing.checked_in_at ? existing.checked_in_at : now,
      completed_at: status === 'completed' ? now : existing ? existing.completed_at : null,
      commitment_text: status === 'completed' ? cleanCommitmentText(commitmentText) || null : existing ? existing.commitment_text : null,
      updated_at: now
    };

    const result = await db.from('study_session_attendance')
      .upsert(payload, { onConflict: 'session_id,user_id' });

    if (result.error) {
      setAttendanceSaveState('');
      setError('Could not save your attendance yet.');
      return;
    }

    setAttendanceSaveState('saved');
    await loadAttendance(selectedSession.id);
  }

  function togglePeerHit(answerId, index) {
    setPeerDrafts(prev => {
      const draft = prev[answerId] || { rubric_hits: {}, improvement_text: '' };
      const hits = Object.assign({}, draft.rubric_hits || {});
      hits[index] = !hits[index];
      return Object.assign({}, prev, {
        [answerId]: Object.assign({}, draft, { rubric_hits: hits })
      });
    });
  }

  function updatePeerImprovement(answerId, value) {
    setPeerDrafts(prev => {
      const draft = prev[answerId] || { rubric_hits: {}, improvement_text: '' };
      return Object.assign({}, prev, {
        [answerId]: Object.assign({}, draft, { improvement_text: cleanCommitmentText(value) })
      });
    });
  }

  async function submitPeerReview(answer) {
    if (!db || !user || !selectedSession || !canSeeSharedAnswers || !answer || answer.user_id === user.id) return;
    // Client guard: only submit for answers this reviewer is actually assigned.
    // The server RPC re-derives the same ring and rejects anything unassigned,
    // so this is a UX guard, not the security boundary.
    if (!reviewAssignment[answer.id]) {
      setError('You are not assigned to review this answer.');
      return;
    }
    const draft = peerDrafts[answer.id] || { rubric_hits: {}, improvement_text: '' };

    setError('');
    setPeerSaveState(answer.id);
    const result = await db.rpc('submit_study_session_peer_review', {
      p_answer_id: answer.id,
      p_rubric_hits: draft.rubric_hits || {},
      p_improvement_text: cleanCommitmentText(draft.improvement_text) || null
    });

    if (result.error) {
      setPeerSaveState('');
      setError('Could not save that peer review yet.');
      return;
    }

    setPeerSaveState('saved');
    await loadPeerReviews(selectedSession.id);
  }

  async function submitSafetyReport() {
    if (!db || !user) return;
    const detail = cleanReportText(reportForm.detail);
    if (!detail) return;
    const payload = {
      reporter_id: user.id,
      squad_id: selectedSession ? selectedSession.squad_id : (squads[0] && squads[0].id) || null,
      session_id: selectedSession ? selectedSession.id : null,
      category: reportForm.category,
      detail: detail
    };

    setError('');
    setReportSaveState('saving');
    const result = await db.from('study_squad_safety_reports').insert(payload);
    if (result.error) {
      setReportSaveState('');
      setError('Could not send that report yet.');
      return;
    }

    setReportSaveState('saved');
    setReportForm({ category: reportForm.category, detail: '' });
  }

  async function blockMember(answer) {
    if (!db || !user || !selectedSession || !answer || answer.user_id === user.id) return;
    const payload = {
      blocker_id: user.id,
      blocked_user_id: answer.user_id,
      squad_id: selectedSession.squad_id,
      session_id: selectedSession.id,
      reason: 'Hidden from revealed answer view'
    };

    setError('');
    setBlockSaveState(answer.user_id);
    const result = await db.from('study_squad_member_blocks')
      .insert(payload)
      .select('id, blocker_id, blocked_user_id, squad_id, session_id, reason, created_at')
      .single();
    if (result.error) {
      setBlockSaveState('');
      setError('Could not hide that student yet.');
      return;
    }

    setMemberBlocks(prev => prev.concat(result.data || Object.assign({ id: 'local-' + answer.user_id + '-' + Date.now(), created_at: new Date().toISOString() }, payload)));
    setBlockSaveState('');
  }

  async function unblockMember(block) {
    if (!db || !user || !block) return;
    setError('');
    setBlockSaveState(block.blocked_user_id);
    const result = await db.from('study_squad_member_blocks')
      .delete()
      .eq('id', block.id);
    if (result.error) {
      setBlockSaveState('');
      setError('Could not unhide that student yet.');
      return;
    }

    setMemberBlocks(prev => prev.filter(item => item.id !== block.id));
    setBlockSaveState('');
  }

  async function requestAiFeedback(answer) {
    if (!db || !user || !selectedSession || !answer || answer.user_id !== user.id || !canSeeSharedAnswers) return;
    if (!db.functions || typeof db.functions.invoke !== 'function') {
      setError('AI feedback is not available in this environment yet.');
      return;
    }

    setError('');
    setAiSaveState(answer.id);
    const payload = {
      answer_id: answer.id,
      question_pack_title: questionPack ? questionPack.title : selectedSession.question_pack_ref,
      question_prompt: questionPack ? questionPack.question : '',
      rubric: questionPack && questionPack.rubric ? questionPack.rubric : []
    };
    const result = await db.functions.invoke('study-squad-ai-marker', { body: payload });
    if (result.error) {
      setAiSaveState('');
      setError('Could not generate AI feedback yet.');
      return;
    }

    const feedback = result.data && result.data.feedback;
    if (feedback) {
      setAiFeedback(prev => [feedback].concat(prev.filter(item => item.answer_id !== feedback.answer_id)));
    } else {
      await loadAiFeedback(selectedSession.id);
    }
    setAiSaveState('saved');
  }

  const signedIn = Boolean(user);
  const selectedSession = sessions.find(session => session.id === selectedSessionId) || sessions[0] || null;
  const currentPhaseIndex = selectedSession ? getSessionPhaseIndex(selectedSession.phase) : 0;
  const nextPhase = selectedSession ? SQ_SESSION_PHASES[currentPhaseIndex + 1] : null;
  const canSubmitAnswer = selectedSession && selectedSession.phase === 'attempt';
  const canSeeSharedAnswers = selectedSession && (selectedSession.phase === 'reveal' || selectedSession.phase === 'repair' || selectedSession.phase === 'complete');
  const ownAttendance = user ? attendance.find(row => row.user_id === user.id) : null;
  const attendanceSummary = getAttendanceSummary(attendance);
  const scheduleSquad = squads.find(squad => squad.id === sessionForm.squad_id) || squads[0] || null;
  const packGroups = getPackGroupsForSquad(scheduleSquad);
  const blockByUserId = memberBlocks.reduce((map, block) => {
    map[block.blocked_user_id] = block;
    return map;
  }, {});
  const aiByAnswerId = aiFeedback.reduce((map, feedback) => {
    map[feedback.answer_id] = feedback;
    return map;
  }, {});

  // The 2 MC + 2 SA question set for this session, built from the loaded pack.
  const attemptQuestions = React.useMemo(() => buildAttemptQuestions(questionPack), [questionPack]);
  const selectedSquad = selectedSession ? (squads.find(s => s.id === selectedSession.squad_id) || null) : null;
  const reviewSubject = selectedSquad ? selectedSquad.subject : 'default';
  const activeMemberCount = attendance.filter(r => r.status === 'checked_in' || r.status === 'completed').length;
  // Peer-review assignment: each answer gets 2 reviewers where the squad is ≥4
  // (STUDY_ROUTINE_DESIGN.md — "each answer gets 2 reviewers"). Deterministic
  // ring over the submitted answers so coverage is even and stable.
  const reviewAssignment = React.useMemo(
    () => computeReviewAssignment(answers, user && user.id),
    [answers, user && user.id]
  );

  // Attempt countdown derived from the SERVER timestamp: study_sessions.updated_at
  // is stamped when the owner moves the session into 'attempt', so every member
  // and device shares one authoritative window. This avoids the per-device
  // localStorage deadline (which could be reset by clearing storage, differed
  // across devices, and leaked stale keys), and a re-opened attempt phase gets a
  // fresh window because updated_at is re-stamped on the phase change.
  const attemptDeadline = (selectedSession && selectedSession.phase === 'attempt' && selectedSession.updated_at)
    ? new Date(selectedSession.updated_at).getTime() + SQ_ATTEMPT_MINUTES * 60 * 1000
    : null;

  React.useEffect(() => {
    if (!selectedSession || selectedSession.phase !== 'attempt') return undefined;
    const timer = window.setInterval(() => setNowTs(Date.now()), 1000);
    return () => window.clearInterval(timer);
  }, [selectedSession && selectedSession.id, selectedSession && selectedSession.phase]);

  const attemptSecondsLeft = attemptDeadline ? Math.max(0, Math.floor((attemptDeadline - nowTs) / 1000)) : null;
  const attemptTimeUp = attemptSecondsLeft === 0;

  const ownAnswerRow = user ? answers.find(a => a.user_id === user.id) : null;
  const ownAnswerObj = ownAnswerRow ? parseSquadAnswer(ownAnswerRow.answer_text) : null;
  const ownReviews = ownAnswerRow ? peerReviews.filter(r => r.answer_id === ownAnswerRow.id) : [];
  const reviewLabelMap = (window.HSC && window.HSC.squadReview) ? window.HSC.squadReview.labelMap() : {};
  // Expected reviewers per answer under the ring: 2 where ≥3 submitted, else 1.
  const expectedReviewsPerAnswer = answers.length >= 3 ? 2 : (answers.length === 2 ? 1 : 0);
  // Owner-only squad-weakness summary: which checkbox reasons fired most across
  // all peer reviews this session (only the owner can read every review under
  // RLS). Turns the peer-review round into a teaching signal for the tutor.
  const isSessionOwner = Boolean(selectedSquad && user && selectedSquad.created_by === user.id);
  const squadWeaknessSummary = React.useMemo(() => {
    if (!isSessionOwner) return [];
    // Count how often each NEGATIVE checkbox fired (ignore pos_* reinforcement).
    const counts = {};
    (peerReviews || []).forEach(r => {
      const hits = r.rubric_hits || {};
      Object.keys(hits).forEach(id => {
        if (hits[id] && id.indexOf('pos_') !== 0) counts[id] = (counts[id] || 0) + 1;
      });
    });
    return Object.keys(counts)
      .map(id => ({ id: id, label: reviewLabelMap[id] || id, count: counts[id] }))
      .sort((a, b) => b.count - a.count)
      .slice(0, 5);
  }, [isSessionOwner, peerReviews, reviewLabelMap]);

  async function loadQuestionPack(ref) {
    const cleanedRef = cleanSquadText(ref, 80);
    if (!cleanedRef) {
      setQuestionPack(null);
      setQuestionPackState('');
      return;
    }
    setQuestionPackState('loading');
    try {
      const pack = await resolveQuestionPack(cleanedRef);
      setQuestionPack(pack);
      setQuestionPackState(pack && pack.questions && pack.questions.length ? 'loaded' : 'fallback');
    } catch (err) {
      setQuestionPack(getQuestionPack(cleanedRef));
      setQuestionPackState('fallback');
    }
  }

  return (
    <div className="sq-wrap">
      <div className="sq-main">
        <div className="sq-head">
          <div>
            <h2 className="sq-name">
              <Icon name="users"/> Study Squads
            </h2>
            <div className="sq-meta">Invite-only question sessions · no chat · no public discovery</div>
          </div>
          <a className="sq-invite" href="focus.html"><Icon name="timer"/> Focus Rooms</a>
        </div>

        <div className="sq-goal">
          <div className="sq-goal-head">
            <div className="sq-goal-title"><Icon name="flag" style={{ width: 14, height: 14, marginRight: 6, verticalAlign: 'middle' }}/> Your accountability squad</div>
            <div className="sq-goal-num">{squads.length ? squads.length + ' squad' + (squads.length === 1 ? '' : 's') : 'Create'}</div>
          </div>
          <div className="sq-goal-bar">
            <i style={{ width: signedIn ? '72%' : '36%' }}/>
          </div>
          <div className="sq-goal-sub">
            Same 3–5 students, same year and subject, every week. Attempt 2 MC + 2 SA under time, reveal to the squad, mark each other with the rubric, then repair one weak answer — where the real learning happens.
          </div>
        </div>

        {!authReady && <div className="sq-notice">Checking sign-in...</div>}
        {authReady && !signedIn && (
          <div className="sq-notice">
            Sign in to create a private Study Squad. <a href="auth/login.html">Sign in</a>
          </div>
        )}

        {signedIn && (
          <div className="sq-form">
            <div className="sq-form-head">
              <div>
                <h3>Create a squad</h3>
                <p>Create a private squad, then use invite codes to add members.</p>
              </div>
              <span>invite-only</span>
            </div>

            <label className="sq-label" htmlFor="sq-name">Squad name</label>
            <input id="sq-name" className="sq-input" maxLength="60" value={form.name} onChange={e => updateForm('name', e.target.value)}/>

            <div className="sq-form-grid">
              <div>
                <label className="sq-label" htmlFor="sq-subject">Subject</label>
                <select id="sq-subject" className="sq-input" value={form.subject} onChange={e => updateForm('subject', e.target.value)}>
                  {SQ_SUBJECTS.map(subject => <option key={subject.value} value={subject.value}>{subject.label}</option>)}
                </select>
              </div>
              <div>
                <label className="sq-label" htmlFor="sq-year">Year</label>
                <select id="sq-year" className="sq-input" value={form.year_group} onChange={e => updateForm('year_group', e.target.value)}>
                  {[12, 11, 10, 9, 8, 7].map(year => <option key={year} value={String(year)}>Year {year}</option>)}
                </select>
              </div>
              <div>
                <label className="sq-label" htmlFor="sq-size">Size cap</label>
                <select id="sq-size" className="sq-input" value={form.max_members} onChange={e => updateForm('max_members', e.target.value)}>
                  {[3, 4, 5].map(n => <option key={n} value={String(n)}>{n} students{n === 4 ? ' (target)' : ''}</option>)}
                </select>
              </div>
            </div>

            <label className="sq-label" htmlFor="sq-module">Module focus</label>
            <input id="sq-module" className="sq-input" maxLength="40" value={form.module_label} onChange={e => updateForm('module_label', e.target.value)} placeholder="Example: Module 7 immunity"/>

            <div className="sq-form-grid two">
              <div>
                <label className="sq-label">Weekly days</label>
                <div className="sq-day-row">
                  {SQ_DAYS.map(day => (
                    <button key={day} type="button" className={form.recurring_days.includes(day) ? 'active' : ''} onClick={() => toggleDay(day)}>{day}</button>
                  ))}
                </div>
              </div>
              <div>
                <label className="sq-label" htmlFor="sq-time">Time</label>
                <input id="sq-time" className="sq-input" type="time" value={form.recurring_time_local} onChange={e => updateForm('recurring_time_local', e.target.value)}/>
              </div>
            </div>

            <div className="sq-form-actions">
              <button className="sq-create-btn" type="button" disabled={saveState === 'saving' || !cleanSquadText(form.name, 60)} onClick={createSquad}>
                <Icon name="check"/> {saveState === 'saving' ? 'Creating...' : 'Create squad'}
              </button>
              {saveState === 'saved' && <span>Squad created.</span>}
              {error && <em>{error}</em>}
            </div>
          </div>
        )}

        {signedIn && (
          <div className="sq-form sq-join-form">
            <div className="sq-form-head">
              <div>
                <h3>Join with invite code</h3>
                <p>Codes only work for active invite-only squads. No public squad browsing.</p>
              </div>
              <span>private</span>
            </div>
            <div className="sq-join-row">
              <input className="sq-input" maxLength="24" value={joinCode} onChange={e => setJoinCode(cleanInviteCode(e.target.value))} placeholder="example: sq-7k3m9p2q"/>
              <button className="sq-create-btn" type="button" disabled={joinState === 'joining' || !cleanInviteCode(joinCode)} onClick={joinSquadByCode}>
                <Icon name="users"/> {joinState === 'joining' ? 'Joining...' : 'Join squad'}
              </button>
            </div>
            {joinState === 'joined' && <div className="sq-mini-ok">Joined squad.</div>}
          </div>
        )}

        {signedIn && squads.length > 0 && (
          <div className="sq-form">
            <div className="sq-form-head">
              <div>
                <h3>Schedule a session</h3>
                <p>Create the next timed practice block for this squad.</p>
              </div>
              <span>scheduled</span>
            </div>

            <label className="sq-label" htmlFor="sq-session-squad">Squad</label>
            <select id="sq-session-squad" className="sq-input" value={sessionForm.squad_id} onChange={e => updateSessionSquad(e.target.value)}>
              {squads.map(squad => <option key={squad.id} value={squad.id}>{squad.name}</option>)}
            </select>

            <label className="sq-label" htmlFor="sq-session-title">Session title</label>
            <input id="sq-session-title" className="sq-input" maxLength="120" value={sessionForm.title} onChange={e => updateSessionForm('title', e.target.value)}/>

            <div className="sq-form-grid two">
              <div>
                <label className="sq-label" htmlFor="sq-session-date">Date</label>
                <input id="sq-session-date" className="sq-input" type="date" value={sessionForm.date} onChange={e => updateSessionForm('date', e.target.value)}/>
              </div>
              <div>
                <label className="sq-label" htmlFor="sq-session-time">Time</label>
                <input id="sq-session-time" className="sq-input" type="time" value={sessionForm.time} onChange={e => updateSessionForm('time', e.target.value)}/>
              </div>
            </div>

            <label className="sq-label" htmlFor="sq-pack-ref">Question pack reference</label>
            <select id="sq-pack-ref" className="sq-input" value={sessionForm.question_pack_ref} onChange={e => updateSessionForm('question_pack_ref', e.target.value)}>
              <option value="manual-pack-1">Manual practice pack</option>
              {packGroups.map(group => (
                <optgroup key={group.label} label={group.label}>
                  {group.packs.map(pack => <option key={pack.ref} value={pack.ref}>{getPackShortTitle(pack)}</option>)}
                </optgroup>
              ))}
            </select>
            {scheduleSquad && <div className="sq-field-hint">Showing {packGroups[0] && packGroups[0].priority ? 'matching packs first' : 'all available packs'} for {formatSquadMeta(scheduleSquad)}.</div>}

            <div className="sq-form-actions">
              <button className="sq-create-btn" type="button" disabled={sessionSaveState === 'saving' || !sessionForm.squad_id || !cleanSquadText(sessionForm.title, 120)} onClick={createSession}>
                <Icon name="timer"/> {sessionSaveState === 'saving' ? 'Scheduling...' : 'Schedule session'}
              </button>
              {sessionSaveState === 'saved' && <span>Session scheduled.</span>}
            </div>
          </div>
        )}

        {signedIn && squads.length > 0 && (
          <div className="sq-form">
            <div className="sq-form-head">
              <div>
                <h3>Invite codes</h3>
                <p>Generate short-lived-style codes for private testing. Only squad owners can create or revoke them.</p>
              </div>
              <span>owner</span>
            </div>

            <div className="sq-form-grid two">
              <div>
                <label className="sq-label" htmlFor="sq-invite-squad">Squad</label>
                <select id="sq-invite-squad" className="sq-input" value={inviteForm.squad_id} onChange={e => updateInviteForm('squad_id', e.target.value)}>
                  {squads.map(squad => <option key={squad.id} value={squad.id}>{squad.name}</option>)}
                </select>
              </div>
              <div>
                <label className="sq-label" htmlFor="sq-invite-uses">Uses</label>
                <select id="sq-invite-uses" className="sq-input" value={inviteForm.max_uses} onChange={e => updateInviteForm('max_uses', e.target.value)}>
                  {[1, 2, 3, 4, 5].map(n => <option key={n} value={String(n)}>{n}</option>)}
                </select>
              </div>
            </div>

            <div className="sq-form-actions">
              <button className="sq-create-btn" type="button" disabled={inviteSaveState === 'saving' || !inviteForm.squad_id} onClick={createInvite}>
                <Icon name="share"/> {inviteSaveState === 'saving' ? 'Saving...' : 'Generate code'}
              </button>
              {inviteSaveState === 'saved' && <span>Invite updated.</span>}
            </div>

            <div className="sq-invite-list">
              {invites.length ? invites.map(invite => (
                <div key={invite.id} className={'sq-invite-row ' + invite.status}>
                  <div>
                    <strong>{invite.invite_code}</strong>
                    <small>{formatInviteMeta(invite, squads)}</small>
                  </div>
                  <button className="sq-weak-btn" type="button" disabled={invite.status !== 'active' || inviteSaveState === 'saving'} onClick={() => revokeInvite(invite)}>
                    {invite.status === 'active' ? 'Revoke' : 'Revoked'}
                  </button>
                </div>
              )) : (
                <div className="sq-empty-row">No owner invite codes yet.</div>
              )}
            </div>
          </div>
        )}

        <div className="sq-radar">
          <h4>
            <span><Icon name="target" style={{ width: 12, height: 12, marginRight: 4, verticalAlign: '-1px' }}/> My squads</span>
            <em>{loadingSquads ? 'loading' : squads.length ? 'private' : 'none yet'}</em>
          </h4>

          {squads.length ? squads.map(squad => (
            <div key={squad.id} className="sq-weak-row">
              <div className="sq-weak-topic">
                {squad.name}
                <small>{formatSquadMeta(squad)}</small>
              </div>
              <div className="sq-weak-avs">
                <Icon name="users" style={{ width: 26, height: 26 }}/>
              </div>
              <button className="sq-weak-btn" type="button" disabled>{squad.status}</button>
            </div>
          )) : (
            SQ_PHASES.map((phase) => (
              <div key={phase.label} className="sq-weak-row">
                <div className="sq-weak-topic">
                  {phase.label}
                  <small>{phase.copy}</small>
                </div>
                <div className="sq-weak-avs">
                  <Icon name={phase.icon} style={{ width: 26, height: 26 }}/>
                </div>
                <button className="sq-weak-btn" type="button" disabled>LOCKED</button>
              </div>
            ))
          )}
        </div>

        <div className="sq-radar sq-session-panel">
          <h4>
            <span><Icon name="timer" style={{ width: 12, height: 12, marginRight: 4, verticalAlign: '-1px' }}/> Scheduled sessions</span>
            <em>{loadingSessions ? 'loading' : sessions.length ? 'upcoming' : 'none yet'}</em>
          </h4>

          {sessions.length ? sessions.map(session => (
            <div key={session.id} className="sq-weak-row">
              <div className="sq-weak-topic">
                {session.title}
                <small>{formatSessionMeta(session, squads)}</small>
              </div>
              <div className="sq-weak-avs">
                <Icon name="timer" style={{ width: 26, height: 26 }}/>
              </div>
              <button className="sq-weak-btn" type="button" disabled>{session.phase}</button>
            </div>
          )) : (
            <div className="sq-empty-row">
              Schedule a session after creating a squad, then run it through check-in, attempt, reveal, repair and complete.
            </div>
          )}
        </div>

        {signedIn && selectedSession && (
          <div className="sq-session-room">
            <div className="sq-form-head">
              <div>
                <h3>Session room</h3>
                <p>Move the group through the structured loop: check-in → attempt → reveal → repair → complete.</p>
              </div>
              <span>{selectedSession.phase}</span>
            </div>

            {sessions.length > 1 && (
              <div>
                <label className="sq-label" htmlFor="sq-selected-session">Active session</label>
                <select id="sq-selected-session" className="sq-input" value={selectedSession.id} onChange={e => setSelectedSessionId(e.target.value)}>
                  {sessions.map(session => (
                    <option key={session.id} value={session.id}>{session.title}</option>
                  ))}
                </select>
              </div>
            )}

            <div className="sq-room-summary">
              <div>
                <strong>{selectedSession.title}</strong>
                <small>{formatSessionMeta(selectedSession, squads)}</small>
              </div>
              <Icon name={getSessionPhase(selectedSession.phase).icon}/>
            </div>

            <div className="sq-phase-track">
              {SQ_SESSION_PHASES.map((phase, index) => {
                const phaseClass = index < currentPhaseIndex ? 'done' : index === currentPhaseIndex ? 'active' : 'locked';
                return (
                  <div key={phase.key} className={'sq-phase-step ' + phaseClass}>
                    <div className="sq-phase-dot"><Icon name={phase.icon}/></div>
                    <div>
                      <strong>{phase.label}</strong>
                      <small>{phase.copy}</small>
                    </div>
                  </div>
                );
              })}
            </div>

            <div className="sq-workflow-panel">
              <div className="sq-answer-head">
                <div>
                  <strong>{getSessionPhase(selectedSession.phase).label} workflow</strong>
                  <small>{getSessionPhase(selectedSession.phase).copy}</small>
                </div>
                <em>{selectedSession.phase}</em>
              </div>
              <div className="sq-workflow-grid">
                {getPhaseTasks(selectedSession.phase).map(task => (
                  <div key={task.title} className="sq-workflow-task">
                    <Icon name={task.icon}/>
                    <div>
                      <strong>{task.title}</strong>
                      <small>{task.copy}</small>
                    </div>
                  </div>
                ))}
              </div>
            </div>

            <div className="sq-phase-actions">
              <div>
                <strong>Owner controls</strong>
                <small>Only the squad owner should be able to advance phases under RLS.</small>
              </div>
              {nextPhase ? (
                <button className="sq-create-btn" type="button" disabled={phaseSaveState === 'saving'} onClick={() => updateSessionPhase(selectedSession, nextPhase.key)}>
                  <Icon name="chevronR"/> {phaseSaveState === 'saving' ? 'Updating...' : 'Start ' + nextPhase.label.toLowerCase()}
                </button>
              ) : (
                <button className="sq-create-btn" type="button" disabled><Icon name="check"/> Complete</button>
              )}
              {phaseSaveState === 'saved' && <span>Phase updated.</span>}
            </div>

            <div className="sq-attendance-panel">
              <div className="sq-answer-head">
                <div>
                  <strong>Attendance</strong>
                  <small>{ownAttendance ? formatAttendanceMeta(ownAttendance) : 'Mark yourself present when you join the session.'}</small>
                </div>
                <em>{attendanceSummary}</em>
              </div>
              <div className="sq-attendance-actions">
                <button className="sq-create-btn" type="button" disabled={attendanceSaveState === 'saving' || Boolean(ownAttendance)} onClick={() => markAttendance('checked_in')}>
                  <Icon name="flag"/> {ownAttendance ? 'Present' : 'Mark present'}
                </button>
                <input className="sq-input" maxLength="240" value={commitmentText} onChange={e => setCommitmentText(e.target.value)} placeholder="Next commitment before the next session"/>
                <button className="sq-create-btn" type="button" disabled={attendanceSaveState === 'saving' || !selectedSession || selectedSession.phase !== 'complete'} onClick={() => markAttendance('completed')}>
                  <Icon name="check"/> {ownAttendance && ownAttendance.status === 'completed' ? 'Completed' : 'Complete'}
                </button>
              </div>
              {attendanceSaveState === 'saved' && <div className="sq-mini-ok">Attendance saved.</div>}
            </div>

            <div className="sq-pack-panel">
              <div className="sq-answer-head">
                <div>
                  <strong>Question pack</strong>
                  <small>{questionPack ? questionPack.title : 'Loading assigned questions'}</small>
                </div>
                <em>{questionPackState || 'ready'}</em>
              </div>
              <div className="sq-field-hint">
                {(attemptQuestions.mc.length || 2)} multiple-choice + {(attemptQuestions.sa.length || 2)} short-answer · {SQ_ATTEMPT_MINUTES}-minute timed attempt.
                {' '}Answers stay private until the owner starts reveal.
              </div>
            </div>

            <div className="sq-answer-panel">
              <div className="sq-answer-head">
                <div>
                  <strong>Answer pad</strong>
                  <small>{canSubmitAnswer ? 'Answer under timed conditions — private until reveal.' : canSeeSharedAnswers ? 'Answers are unlocked to the squad.' : 'Locked until the attempt phase starts.'}</small>
                </div>
                <em>{answers.length ? answers.length + ' submitted' : 'no answers'}</em>
              </div>

              {canSubmitAnswer ? (
                <AttemptForm
                  questions={attemptQuestions}
                  draft={attemptDraft}
                  onMc={updateAttemptMc}
                  onSa={updateAttemptSa}
                  secondsLeft={attemptSecondsLeft}
                  timeUp={attemptTimeUp}
                  saving={answerSaveState === 'saving'}
                  saved={answerSaveState === 'saved'}
                  onSubmit={submitAnswer}
                  submitted={Boolean(ownAnswerRow)}
                />
              ) : canSeeSharedAnswers ? (
                <div className="sq-answer-list">
                  {answers.length ? answers.map((answer, index) => {
                    const isOwn = answer.user_id === user.id;
                    const blocked = !isOwn && blockByUserId[answer.user_id];
                    const parsed = parseSquadAnswer(answer.answer_text);
                    const assigned = Boolean(reviewAssignment[answer.id]);
                    return (
                      <div key={answer.id || answer.user_id || index} className={'sq-answer-card ' + (blocked ? 'blocked' : '')}>
                        <div className="sq-answer-title-row">
                          <strong>{isOwn ? 'Your answer' : 'Squad member ' + (index + 1)}{!isOwn && assigned ? ' · your review task' : ''}</strong>
                          {!isOwn && (
                            blocked ? (
                              <button className="sq-weak-btn" type="button" disabled={blockSaveState === answer.user_id} onClick={() => unblockMember(blocked)}>
                                {blockSaveState === answer.user_id ? 'Saving...' : 'Unhide'}
                              </button>
                            ) : (
                              <button className="sq-weak-btn" type="button" disabled={blockSaveState === answer.user_id} onClick={() => blockMember(answer)}>
                                {blockSaveState === answer.user_id ? 'Saving...' : 'Hide student'}
                              </button>
                            )
                          )}
                        </div>
                        {blocked ? (
                          <div className="sq-blocked-note">This student's answer is hidden for you. Peer review prompts from this student are muted in this session view.</div>
                        ) : (
                          <React.Fragment>
                            <StructuredAnswerView parsed={parsed} questions={attemptQuestions}/>
                            {isOwn ? (
                              <React.Fragment>
                                <div className="sq-peer-note">
                                  {expectedReviewsPerAnswer
                                    ? countReviewsForAnswer(peerReviews, answer.id) + ' of ' + expectedReviewsPerAnswer + ' reviews received'
                                    : countReviewsForAnswer(peerReviews, answer.id) + ' peer review' + (countReviewsForAnswer(peerReviews, answer.id) === 1 ? '' : 's') + ' received'}
                                </div>
                                <AiFeedbackBox
                                  answer={answer}
                                  feedback={aiByAnswerId[answer.id]}
                                  saving={aiSaveState === answer.id}
                                  saved={aiSaveState === 'saved'}
                                  onRequest={requestAiFeedback}
                                />
                              </React.Fragment>
                            ) : assigned ? (
                              <PeerReviewBox
                                answer={answer}
                                subject={reviewSubject}
                                draft={peerDrafts[answer.id] || { rubric_hits: {}, improvement_text: '' }}
                                saving={peerSaveState === answer.id}
                                saved={peerSaveState === 'saved'}
                                onToggle={togglePeerHit}
                                onImprove={updatePeerImprovement}
                                onSubmit={submitPeerReview}
                              />
                            ) : (
                              <div className="sq-peer-note">Not assigned to you for review — each answer gets two reviewers so the load is shared.</div>
                            )}
                          </React.Fragment>
                        )}
                      </div>
                    );
                  }) : (
                    <div className="sq-empty-row">No submitted answers yet.</div>
                  )}
                </div>
              ) : (
                <div className="sq-empty-row">
                  Students answer 2 MC + 2 SA independently here once the owner starts the attempt phase.
                </div>
              )}
            </div>

            {selectedSession.phase === 'repair' && ownAnswerObj && (
              <RepairPanel
                questions={attemptQuestions}
                ownAnswer={ownAnswerObj}
                reviews={ownReviews}
                labelMap={reviewLabelMap}
                target={repairTarget}
                text={repairText}
                onTarget={setRepairTarget}
                onText={setRepairText}
                onSave={saveRepair}
                saving={repairSaveState === 'saving'}
                saved={repairSaveState === 'saved'}
                submitted={Boolean(ownAnswerRow)}
              />
            )}

            {canSeeSharedAnswers && (
              <ExemplarPanel subject={reviewSubject} questions={attemptQuestions}/>
            )}

            {canSeeSharedAnswers && isSessionOwner && (
              <SquadWeaknessPanel items={squadWeaknessSummary} reviewCount={peerReviews.length}/>
            )}
          </div>
        )}
      </div>

      <div className="sq-chat">
        <div className="sq-chat-head">
          <Icon name="flag"/> Safety locks <em>required</em>
        </div>
        <div className="sq-chat-body">
          <div className="sq-pinned">
            <strong><Icon name="bulb"/> HOW THIS STAYS SAFE</strong>
            Study Squads are structured question sessions. There is no open chat, DMs, or video by design — you communicate only through constrained rubric feedback, and reporting, blocking and moderation are always on.
          </div>

          {SQ_GUARDS.map((guard) => (
            <div key={guard} className="sq-msg">
              <CharAv charId="prism" size={30}/>
              <div className="sq-msg-bubble">
                <strong>Guardrail</strong>
                {guard}
              </div>
            </div>
          ))}

          {signedIn && (
            <div className="sq-report-box">
              <div className="sq-peer-title">Report an issue</div>
              <select className="sq-input" value={reportForm.category} onChange={e => setReportForm(prev => Object.assign({}, prev, { category: e.target.value }))}>
                <option value="safety">Safety concern</option>
                <option value="privacy">Privacy concern</option>
                <option value="content">Question/content issue</option>
                <option value="technical">Technical problem</option>
                <option value="other">Other</option>
              </select>
              <textarea className="sq-answer-input sq-report-input" rows="4" maxLength="1000" value={reportForm.detail} onChange={e => setReportForm(prev => Object.assign({}, prev, { detail: e.target.value }))} placeholder="Briefly describe what needs attention."/>
              <button className="sq-create-btn" type="button" disabled={reportSaveState === 'saving' || cleanReportText(reportForm.detail).length < 5} onClick={submitSafetyReport}>
                <Icon name="flag"/> {reportSaveState === 'saving' ? 'Sending...' : 'Send report'}
              </button>
              {reportSaveState === 'saved' && <div className="sq-mini-ok">Report sent.</div>}
            </div>
          )}
        </div>
        <div className="sq-chat-input">
          <div className="sq-chat-field">No open chat by design — feedback goes through the rubric</div>
          <button className="sq-chat-send" type="button" disabled><Icon name="x"/></button>
        </div>
      </div>
    </div>
  );
}

/* ── Structured attempt form: 2 MC (radios) + 2 SA (textareas), timed ── */
function AttemptForm({ questions, draft, onMc, onSa, secondsLeft, timeUp, saving, saved, onSubmit, submitted }) {
  const hasContent = questions.mc.some(q => draft.mc[q.qid] != null)
    || questions.sa.some(q => cleanAnswerText(draft.sa[q.qid] || ''));
  return (
    <div className="sq-attempt">
      <div className={'sq-attempt-timer ' + (timeUp ? 'up' : (secondsLeft != null && secondsLeft < 120 ? 'low' : ''))}>
        <Icon name="clock"/>
        <strong>{secondsLeft == null ? '--:--' : formatCountdown(secondsLeft)}</strong>
        <span>{timeUp ? 'Time up — submit what you have' : 'timed attempt'}</span>
      </div>

      {questions.mc.map((q, i) => (
        <div key={q.qid} className="sq-attempt-q">
          <div className="sq-attempt-qhead"><em>MC {i + 1}</em> {q.prompt}</div>
          <div className="sq-attempt-options">
            {(q.options || []).map((opt, oi) => (
              <label key={oi} className={'sq-attempt-option ' + (draft.mc[q.qid] === oi ? 'active' : '')}>
                <input
                  type="radio"
                  name={'mc-' + q.qid}
                  checked={draft.mc[q.qid] === oi}
                  disabled={timeUp}
                  onChange={() => onMc(q.qid, oi)}
                />
                <span className="sq-attempt-letter">{String.fromCharCode(65 + oi)}</span>
                <span>{opt}</span>
              </label>
            ))}
          </div>
        </div>
      ))}

      {questions.sa.map((q, i) => (
        <div key={q.qid} className="sq-attempt-q">
          <div className="sq-attempt-qhead"><em>SA {i + 1}</em> {q.prompt}</div>
          <textarea
            className="sq-answer-input"
            rows="5"
            maxLength={SQ_SA_CAP}
            disabled={timeUp}
            value={draft.sa[q.qid] || ''}
            onChange={e => onSa(q.qid, e.target.value)}
            placeholder="Write your short answer here."
          />
          {/* SEAM: photo capture → AI transcription slots in here later. Today the
              student types; a future edge function would fill this same field
              from a photo of their handwritten answer, leaving submit unchanged. */}
        </div>
      ))}

      <div className="sq-attempt-seam">
        <Icon name="bulb"/> Type your book answers for now. Photo capture with AI transcription is coming to this step.
      </div>

      <div className="sq-form-actions">
        <button className="sq-create-btn" type="button" disabled={saving || !hasContent} onClick={onSubmit}>
          <Icon name="check"/> {saving ? 'Saving...' : submitted ? 'Update answer' : 'Submit answer'}
        </button>
        {saved && <span>Answer submitted.</span>}
      </div>
    </div>
  );
}

/* ── Read-only structured answer view (reveal/repair/complete) ── */
function StructuredAnswerView({ parsed, questions }) {
  if (parsed.legacy) {
    const legacy = (parsed.sa && parsed.sa[0] && parsed.sa[0].text) || '';
    return <p className="sq-answer-body">{legacy}</p>;
  }
  const mcById = {};
  (questions.mc || []).forEach(q => { mcById[q.qid] = q; });
  const saById = {};
  (questions.sa || []).forEach(q => { saById[q.qid] = q; });
  return (
    <div className="sq-struct-answer">
      {(parsed.mc || []).map((m, i) => {
        const q = mcById[m.qid];
        const opts = q ? q.options : [];
        const chosen = m.choiceIndex;
        const correct = q ? q.correctIndex : null;
        const isCorrect = chosen != null && chosen === correct;
        return (
          <div key={'mc' + i} className="sq-struct-line">
            <strong>MC {i + 1}</strong>
            <span className={'sq-struct-mc ' + (chosen == null ? 'blank' : isCorrect ? 'right' : 'wrong')}>
              {chosen == null ? 'No answer' : String.fromCharCode(65 + chosen) + '. ' + (opts[chosen] || '')}
              {chosen != null && (isCorrect ? ' ✓' : ' ✗')}
            </span>
          </div>
        );
      })}
      {(parsed.sa || []).map((s, i) => (
        <div key={'sa' + i} className="sq-struct-line col">
          <strong>SA {i + 1}</strong>
          <p className="sq-answer-body">{cleanAnswerText(s.text || '') || <em>No answer</em>}</p>
        </div>
      ))}
      {parsed.repair && parsed.repair.text && (
        <div className="sq-struct-repair">
          <strong>Repaired answer</strong>
          <p className="sq-answer-body">{parsed.repair.text}</p>
        </div>
      )}
    </div>
  );
}

/* ── Peer review: the elaborate checkbox taxonomy + exemplars from config ── */
function PeerReviewBox({ answer, subject, draft, saving, saved, onToggle, onImprove, onSubmit }) {
  const cfg = window.HSC && window.HSC.squadReview;
  const groups = cfg ? cfg.groupsForSubject(subject) : [{ heading: 'Feedback', kind: 'negative', items: [
    { id: 'answers', label: 'Answers the question' },
    { id: 'language', label: 'Uses accurate course language' },
    { id: 'cause', label: 'Explains cause and effect' }
  ] }];
  const hits = draft.rubric_hits || {};
  const hasAnyHit = Object.keys(hits).some(key => hits[key]);
  const improvement = cleanCommitmentText(draft.improvement_text);

  return (
    <div className="sq-peer-review">
      <div className="sq-peer-title">Mark this answer — tick what applies</div>
      {groups.map(group => (
        <div key={group.heading} className={'sq-peer-group ' + group.kind}>
          <div className="sq-peer-group-head">{group.heading}</div>
          <div className="sq-peer-checks">
            {group.items.map(item => (
              <button key={item.id} type="button" className={(hits[item.id] ? 'active ' : '') + group.kind} onClick={() => onToggle(answer.id, item.id)}>
                <Icon name="check"/>{item.label}
              </button>
            ))}
          </div>
        </div>
      ))}
      <input className="sq-input" maxLength="240" value={draft.improvement_text || ''} onChange={e => onImprove(answer.id, e.target.value)} placeholder="One specific improvement that would lift this answer"/>
      <div className="sq-form-actions">
        <button className="sq-create-btn" type="button" disabled={saving || (!hasAnyHit && !improvement)} onClick={() => onSubmit(answer)}>
          <Icon name="check"/> {saving ? 'Saving...' : 'Save peer review'}
        </button>
        {saved && <span>Peer review saved.</span>}
      </div>
    </div>
  );
}

/* ── Worked exemplars shown during review (raises novice-marking reliability) ── */
function ExemplarPanel({ subject }) {
  const cfg = window.HSC && window.HSC.squadReview;
  const ex = cfg ? cfg.exemplarForSubject(subject) : null;
  if (!ex) return null;
  return (
    <div className="sq-exemplar-panel">
      <div className="sq-answer-head">
        <div>
          <strong>What good looks like</strong>
          <small>Use these to calibrate your marking.</small>
        </div>
        <em>exemplars</em>
      </div>
      <div className="sq-exemplar-q">{ex.question}</div>
      <div className="sq-exemplar-grid">
        <div className="sq-exemplar sq-exemplar-strong">
          <div className="sq-exemplar-tag">Band 6 model</div>
          <p>{ex.strong.text}</p>
          <div className="sq-exemplar-why"><Icon name="check"/> {ex.strong.why}</div>
        </div>
        <div className="sq-exemplar sq-exemplar-weak">
          <div className="sq-exemplar-tag">Weak model</div>
          <p>{ex.weak.text}</p>
          <div className="sq-exemplar-why"><Icon name="x"/> {ex.weak.why}</div>
        </div>
      </div>
    </div>
  );
}

/* ── Owner/tutor-only: which marking gaps fired most across the squad this
   session — turns the peer-review round into a targeted teaching signal. ── */
function SquadWeaknessPanel({ items, reviewCount }) {
  return (
    <div className="sq-weakness-panel">
      <div className="sq-answer-head">
        <div>
          <strong>Squad weakness this session</strong>
          <small>Owner view — where to focus the next tutor session.</small>
        </div>
        <em>{reviewCount} review{reviewCount === 1 ? '' : 's'}</em>
      </div>
      {items && items.length ? (
        <div className="sq-weakness-list">
          {items.map((item, i) => {
            const max = items[0].count || 1;
            return (
              <div key={item.id} className="sq-weakness-row">
                <div className="sq-weakness-label"><span className="sq-weakness-rank">{i + 1}</span>{item.label}</div>
                <div className="sq-weakness-bar"><i style={{ width: Math.round((item.count / max) * 100) + '%' }}/></div>
                <div className="sq-weakness-count">×{item.count}</div>
              </div>
            );
          })}
        </div>
      ) : (
        <div className="sq-empty-row">No checkbox feedback recorded yet — the summary appears once peers mark the answers.</div>
      )}
    </div>
  );
}

/* ── Repair: rewrite ONE weak answer using the feedback received (the key mechanic) ── */
function RepairPanel({ questions, ownAnswer, reviews, labelMap, target, text, onTarget, onText, onSave, saving, saved, submitted }) {
  const saList = (ownAnswer.sa || []).map((s, i) => {
    const q = (questions.sa || []).find(x => x.qid === s.qid);
    return { key: 'sa:' + s.qid, qid: s.qid, index: i, prompt: q ? q.prompt : 'Short answer ' + (i + 1), text: s.text || '' };
  });
  // Aggregate the feedback this student received.
  const firedIds = {};
  const improvements = [];
  (reviews || []).forEach(r => {
    const hits = r.rubric_hits || {};
    Object.keys(hits).forEach(id => { if (hits[id]) firedIds[id] = (firedIds[id] || 0) + 1; });
    if (r.improvement_text) improvements.push(r.improvement_text);
  });
  const firedList = Object.keys(firedIds).map(id => ({ id: id, label: labelMap[id] || id, count: firedIds[id] }))
    .sort((a, b) => b.count - a.count);

  if (!submitted) {
    return (
      <div className="sq-repair-panel">
        <div className="sq-repair-head"><Icon name="pencil"/> Repair</div>
        <div className="sq-empty-row">You did not submit an answer in the attempt phase, so there is nothing to repair for this session.</div>
      </div>
    );
  }

  return (
    <div className="sq-repair-panel">
      <div className="sq-repair-head">
        <Icon name="pencil"/> Repair — rewrite one weak answer
      </div>
      <p className="sq-repair-intro">This is where the real learning happens: pick your weakest short answer, read the feedback, and rewrite it properly.</p>

      <div className="sq-repair-feedback">
        <strong>Feedback you received</strong>
        {firedList.length ? (
          <div className="sq-repair-hits">
            {firedList.map(f => <span key={f.id} className="sq-repair-hit">{f.label}{f.count > 1 ? ' ×' + f.count : ''}</span>)}
          </div>
        ) : (
          <div className="sq-peer-note">No checkbox feedback yet — use the model exemplars above to spot your weakest answer.</div>
        )}
        {improvements.length > 0 && (
          <ul className="sq-repair-improvements">
            {improvements.map((imp, i) => <li key={i}>{imp}</li>)}
          </ul>
        )}
      </div>

      <label className="sq-label">Which answer are you repairing?</label>
      <select className="sq-input" value={target} onChange={e => onTarget(e.target.value)}>
        <option value="">Choose one short answer…</option>
        {saList.map(s => <option key={s.key} value={s.key}>SA {s.index + 1}: {s.prompt.slice(0, 60)}</option>)}
      </select>

      {target && (() => {
        const chosen = saList.find(s => s.key === target);
        return chosen ? <div className="sq-repair-original"><strong>Your original</strong><p>{chosen.text || 'No answer written.'}</p></div> : null;
      })()}

      <label className="sq-label">Your rewritten answer</label>
      <textarea className="sq-answer-input" rows="6" maxLength={SQ_REPAIR_CAP} value={text} onChange={e => onText(e.target.value)} placeholder="Rewrite the answer using the feedback and the model exemplars."/>
      <div className="sq-form-actions">
        <button className="sq-create-btn" type="button" disabled={saving || !target || !cleanAnswerText(text)} onClick={onSave}>
          <Icon name="check"/> {saving ? 'Saving...' : 'Save repaired answer'}
        </button>
        {saved && <span>Repaired answer saved.</span>}
      </div>
    </div>
  );
}

function AiFeedbackBox({ answer, feedback, saving, saved, onRequest }) {
  return (
    <div className="sq-ai-feedback">
      <div className="sq-ai-head">
        <div>
          <strong>AI practice feedback</strong>
          <small>Rubric-based guidance, not an official exam mark.</small>
        </div>
        <button className="sq-create-btn" type="button" disabled={saving} onClick={() => onRequest(answer)}>
          <Icon name="sparkles"/> {saving ? 'Generating...' : feedback ? 'Refresh feedback' : 'Generate feedback'}
        </button>
      </div>
      {feedback ? (
        <div className="sq-ai-body">
          <div className="sq-ai-score">
            <span>{feedback.mark_estimate == null ? '—' : feedback.mark_estimate + '/5'}</span>
            <em>{feedback.confidence || 'low'} confidence</em>
          </div>
          <AiFeedbackList title="Strengths" items={feedback.strengths}/>
          <AiFeedbackList title="Missing criteria" items={feedback.missing_criteria}/>
          {feedback.improvement && (
            <div className="sq-ai-callout">
              <strong>Improve by one mark</strong>
              <p>{feedback.improvement}</p>
            </div>
          )}
          {feedback.exemplar && (
            <div className="sq-ai-callout">
              <strong>Example rewrite</strong>
              <p>{feedback.exemplar}</p>
            </div>
          )}
          {feedback.safety_note && <div className="sq-peer-note">{feedback.safety_note}</div>}
        </div>
      ) : (
        <div className="sq-peer-note">{saved ? 'Feedback saved.' : 'Generate feedback after reveal to get a short rubric check.'}</div>
      )}
    </div>
  );
}

function AiFeedbackList({ title, items }) {
  const list = Array.isArray(items) ? items.filter(Boolean) : [];
  if (!list.length) return null;
  return (
    <div className="sq-ai-list">
      <strong>{title}</strong>
      {list.map((item, index) => <span key={index}>{item}</span>)}
    </div>
  );
}

/* ── Structured answer (2 MC + 2 SA) serialisation ──
   Stored as JSON inside study_session_answers.answer_text (<= 8000 chars) so we
   reuse the existing schema with no new table. Falls back gracefully for legacy
   plain-text answers created before this format. */
function parseSquadAnswer(text) {
  if (text) {
    try {
      const obj = JSON.parse(text);
      if (obj && obj.v === 1 && (Array.isArray(obj.mc) || Array.isArray(obj.sa))) {
        return { v: 1, mc: obj.mc || [], sa: obj.sa || [], repair: obj.repair || null };
      }
    } catch (e) { /* not JSON → legacy */ }
  }
  return { v: 1, legacy: true, mc: [], sa: [{ qid: 'legacy', text: String(text || '') }], repair: null };
}

function capAnswerText(value, max) {
  return cleanAnswerText(value).slice(0, max);
}

// Serialise the structured answer to JSON that ALWAYS fits the 8000-char column
// and is ALWAYS valid JSON. Fields are capped before stringify (never slice the
// final string). If the whole thing still overflows, the repair text is trimmed
// first (protecting the original attempt), then SA as a last resort.
function serialiseSquadAnswer(obj) {
  function build(saCap, repairCap, includeRepair) {
    const out = {
      v: 1,
      mc: (obj.mc || []).map(m => ({ qid: m.qid, choiceIndex: (m.choiceIndex == null ? null : m.choiceIndex) })),
      sa: (obj.sa || []).map(s => ({ qid: s.qid, text: capAnswerText(s.text || '', saCap) })),
      repair: null
    };
    if (includeRepair && obj.repair && obj.repair.text) {
      out.repair = {
        target: obj.repair.target || null,
        text: capAnswerText(obj.repair.text, repairCap),
        at: obj.repair.at || null
      };
    }
    return out;
  }

  let out = build(SQ_SA_CAP, SQ_REPAIR_CAP, true);
  let str = JSON.stringify(out);
  if (str.length <= SQ_MAX_ANSWER_JSON) return str;

  // Overflow with a repair present: trim the repair text to fit, keeping the
  // original attempt intact (the repair is additive, never destructive).
  if (out.repair) {
    const overflow = str.length - SQ_MAX_ANSWER_JSON;
    const currentRepairLen = out.repair.text.length;
    const newRepairCap = Math.max(0, currentRepairLen - overflow - 16);
    out = build(SQ_SA_CAP, newRepairCap, newRepairCap > 0);
    str = JSON.stringify(out);
    if (str.length <= SQ_MAX_ANSWER_JSON) return str;
  }

  // Pathological (SA alone exceeds budget): shrink SA caps until it fits.
  let saCap = SQ_SA_CAP;
  while (str.length > SQ_MAX_ANSWER_JSON && saCap > 200) {
    saCap = Math.floor(saCap * 0.8);
    out = build(saCap, 0, false);
    str = JSON.stringify(out);
  }
  return str; // guaranteed valid JSON, within budget
}

function answerHasContent(obj) {
  const mcAnswered = (obj.mc || []).some(m => m.choiceIndex != null);
  const saAnswered = (obj.sa || []).some(s => cleanAnswerText(s.text || ''));
  return mcAnswered || saAnswered;
}

// Build 2 MC + 2 SA from the resolved pack. MC use real options + correctIndex;
// SA reuse higher-value questions presented as short-answer, with the question
// bank's own explanation serving as the model answer at reveal (no new content).
function buildAttemptQuestions(pack) {
  const qs = (pack && Array.isArray(pack.questions)) ? pack.questions : [];
  const withOptions = qs.filter(q => q && q.options && q.options.length >= 2);
  const mc = withOptions.slice(0, 2).map(q => ({
    qid: q.id, prompt: q.prompt, options: q.options, correctIndex: q.correctIndex, explanation: q.explanation
  }));
  const usedIds = {};
  mc.forEach(m => { usedIds[m.qid] = true; });
  const saPool = qs.filter(q => q && !usedIds[q.id] && (q.prompt || q.stem));
  const sa = saPool.slice(0, 2).map(q => ({
    qid: q.id,
    prompt: q.prompt || q.stem,
    model: q.explanation || (pack && pack.model) || '',
    topic: q.topic || ''
  }));
  // Manual/fallback packs may have no options at all — ensure at least one SA.
  if (!sa.length && pack && pack.question) {
    sa.push({ qid: (pack.ref || 'pack') + '-sa', prompt: pack.question, model: pack.model || '', topic: '' });
  }
  return { mc: mc, sa: sa };
}

// Deterministic 2-reviewer ring over submitted answers. Returns the set of
// answer ids the given user is assigned to review. Each answer ends up with 2
// reviewers when there are >= 3 submissions (so squads of 4+ get full coverage).
// Ordering MUST match the server RPC submit_study_session_peer_review, which
// orders by user_id::text collate "C" (byte order) — so we compare by raw
// codepoint here, NOT localeCompare (whose order can diverge from C collation).
function computeReviewAssignment(answers, userId) {
  const assigned = {};
  if (!userId) return assigned;
  const ordered = (answers || []).slice().sort((a, b) => {
    const x = String(a.user_id), y = String(b.user_id);
    return x < y ? -1 : x > y ? 1 : 0;
  });
  const n = ordered.length;
  const myIndex = ordered.findIndex(a => a.user_id === userId);
  if (myIndex === -1 || n < 2) return assigned;
  const targets = n >= 3 ? [1, 2] : [1];
  targets.forEach(offset => {
    const t = ordered[(myIndex + offset) % n];
    if (t && t.user_id !== userId) assigned[t.id] = true;
  });
  return assigned;
}

function formatCountdown(sec) {
  const s = Math.max(0, Number(sec) || 0);
  const m = Math.floor(s / 60);
  const r = s % 60;
  return m + ':' + String(r).padStart(2, '0');
}

function cleanSquadText(value, max) {
  return String(value || '').replace(/\s+/g, ' ').trim().slice(0, max || 80);
}

function cleanAnswerText(value) {
  return String(value || '').replace(/\s+$/g, '').slice(0, 8000);
}

function cleanCommitmentText(value) {
  return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 240);
}

function cleanReportText(value) {
  return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 1000);
}

function cleanInviteCode(value) {
  return String(value || '').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 24);
}

function makeInviteCode() {
  const alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
  const bytes = new Uint8Array(8);
  if (window.crypto && window.crypto.getRandomValues) {
    window.crypto.getRandomValues(bytes);
  } else {
    for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
  }
  let out = 'sq-';
  for (let i = 0; i < bytes.length; i += 1) {
    out += alphabet[bytes[i] % alphabet.length];
  }
  return out;
}

function nextLocalDate() {
  const d = new Date();
  d.setDate(d.getDate() + 1);
  return d.toISOString().slice(0, 10);
}

function makeScheduledIso(date, time) {
  if (!date) return null;
  const value = date + 'T' + (time || '19:30') + ':00';
  const d = new Date(value);
  return Number.isNaN(d.getTime()) ? null : d.toISOString();
}

function formatSquadMeta(squad) {
  const subject = SQ_SUBJECTS.find(s => s.value === squad.subject);
  const parts = [
    subject ? subject.label : squad.subject,
    squad.year_group ? 'Year ' + squad.year_group : '',
    squad.module_label || '',
    (squad.recurring_days || []).length ? (squad.recurring_days.join('/') + (squad.recurring_time_local ? ' ' + squad.recurring_time_local.slice(0, 5) : '')) : 'schedule later'
  ].filter(Boolean);
  return parts.join(' · ');
}

function formatSessionMeta(session, squads) {
  const squad = (squads || []).find(s => s.id === session.squad_id);
  const bits = [
    squad ? squad.name : 'Study squad',
    session.scheduled_for ? new Date(session.scheduled_for).toLocaleString('en-AU', {
      day: 'numeric',
      month: 'short',
      hour: 'numeric',
      minute: '2-digit'
    }) : 'time not set',
    session.question_pack_ref || 'question pack later'
  ];
  return bits.join(' · ');
}

function formatInviteMeta(invite, squads) {
  const squad = (squads || []).find(s => s.id === invite.squad_id);
  const parts = [
    squad ? squad.name : 'Study squad',
    (invite.uses_count || 0) + '/' + invite.max_uses + ' used',
    invite.status
  ];
  return parts.join(' · ');
}

function chooseDefaultPackForSquad(squad) {
  if (!squad) return '';
  const subject = normalisePackSubject(squad.subject);
  const year = String(squad.year_group || '');
  const match = SQ_REAL_PACKS.find(pack => {
    const meta = getPackMeta(pack);
    return meta.subject === subject && meta.year === year;
  }) || SQ_REAL_PACKS.find(pack => getPackMeta(pack).subject === subject);
  return match ? match.ref : '';
}

function getPackGroupsForSquad(squad) {
  const subject = squad ? normalisePackSubject(squad.subject) : '';
  const year = squad ? String(squad.year_group || '') : '';
  const preferred = [];
  const remaining = [];
  SQ_REAL_PACKS.forEach(pack => {
    const meta = getPackMeta(pack);
    if (subject && meta.subject === subject && (!year || meta.year === year)) preferred.push(pack);
    else remaining.push(pack);
  });

  const groups = [];
  if (preferred.length) {
    groups.push({
      label: 'Suggested for this squad',
      priority: true,
      packs: preferred
    });
  }

  const byLabel = {};
  remaining.forEach(pack => {
    const meta = getPackMeta(pack);
    const label = meta.subjectLabel + (meta.year ? ' Y' + meta.year : '');
    if (!byLabel[label]) byLabel[label] = [];
    byLabel[label].push(pack);
  });
  Object.keys(byLabel).sort().forEach(label => {
    groups.push({ label: label, priority: false, packs: byLabel[label] });
  });
  return groups;
}

function getPackMeta(pack) {
  const ref = pack && pack.ref ? pack.ref : '';
  const yearMatch = ref.match(/y(\d{1,2})/);
  const subject = normalisePackSubject(ref);
  return {
    subject: subject,
    subjectLabel: getPackSubjectLabel(subject),
    year: yearMatch ? yearMatch[1] : ''
  };
}

function normalisePackSubject(value) {
  const text = String(value || '').toLowerCase();
  if (text.indexOf('biology') === 0) return 'biology';
  if (text.indexOf('chemistry') === 0) return 'chemistry';
  if (text.indexOf('physics') === 0) return 'physics';
  if (text.indexOf('maths-standard') === 0) return 'maths-standard';
  if (text.indexOf('maths-advanced') === 0) return 'maths-advanced';
  if (text.indexOf('maths-y') === 0 || text === 'maths') return 'maths';
  return text;
}

function getPackSubjectLabel(subject) {
  const map = {
    biology: 'Biology',
    chemistry: 'Chemistry',
    physics: 'Physics',
    'maths-standard': 'Maths Standard',
    'maths-advanced': 'Maths Advanced',
    maths: 'Maths'
  };
  return map[subject] || 'Other';
}

function getPackShortTitle(pack) {
  return pack.title
    .replace(/^Biology /, '')
    .replace(/^Chemistry /, '')
    .replace(/^Physics /, '')
    .replace(/^Maths Standard /, '')
    .replace(/^Maths Advanced /, '')
    .replace(/^Maths /, '');
}

function getAttendanceSummary(rows) {
  const checkedIn = (rows || []).filter(row => row.status === 'checked_in' || row.status === 'completed').length;
  const completed = (rows || []).filter(row => row.status === 'completed').length;
  return checkedIn + ' present · ' + completed + ' complete';
}

function formatAttendanceMeta(row) {
  const bits = [
    row.status === 'completed' ? 'Session completed' : 'Present',
    row.checked_in_at ? new Date(row.checked_in_at).toLocaleTimeString('en-AU', { hour: 'numeric', minute: '2-digit' }) : '',
    row.commitment_text ? 'Next: ' + row.commitment_text : ''
  ].filter(Boolean);
  return bits.join(' · ');
}

function countReviewsForAnswer(reviews, answerId) {
  return (reviews || []).filter(review => review.answer_id === answerId).length;
}

function getQuestionPack(ref) {
  const fallback = SQ_QUESTION_PACKS[ref] || (ref ? {
    title: ref,
    question: 'Use the assigned question pack for this session.',
    model: 'Model answers can be attached to this pack reference when the question bank is connected.',
    rubric: ['Answer the specific question asked', 'Use accurate course language', 'Explain links between cause and effect'],
  } : null);
  return fallback ? Object.assign({
    ref: ref,
    questions: [{
      id: ref + '-manual-question',
      prompt: fallback.question,
      options: []
    }],
    modelAnswers: [{
      id: ref + '-manual-model',
      answer: fallback.model
    }]
  }, fallback) : null;
}

async function resolveQuestionPack(ref) {
  const canonicalRef = SQ_PACK_ALIASES[ref] || ref;
  const realPack = SQ_REAL_PACKS.find(pack => pack.ref === canonicalRef);
  if (!realPack) return getQuestionPack(ref);

  await loadStudySquadScript(realPack.path);
  const data = window.HSCQuestionBankData || {};
  const banks = Object.keys(data)
    .filter(key => key.indexOf(realPack.prefix) === 0)
    .map(key => data[key])
    .filter(bank => bank && (Array.isArray(bank.questions) || Array.isArray(bank)));

  const questions = [];
  banks.forEach(bank => {
    const bankQuestions = Array.isArray(bank) ? bank : bank.questions;
    bankQuestions.forEach(question => {
      if (!question || !(question.prompt || question.stem || question.question)) return;
      questions.push(normalisePackQuestion(question, bank.lessonId || realPack.ref));
    });
  });

  const selected = questions.slice(0, 5);
  if (!selected.length) {
    return getQuestionPack(ref);
  }

  return {
    ref: ref,
    title: realPack.title,
    question: 'Answer the assigned questions individually. Use reveal to compare against model guidance.',
    model: 'Review each question explanation below, then choose one response to repair.',
    questions: selected,
    modelAnswers: selected.map(question => ({
      id: question.id,
      answer: formatModelGuidance(question)
    })),
    rubric: [
      'Selects or states the correct scientific idea',
      'Uses accurate course terminology',
      'Explains why the answer is correct, not just what it is'
    ]
  };
}

function normalisePackQuestion(question, lessonId) {
  const letters = ['A', 'B', 'C', 'D'];
  const correctIndex = typeof question.correctIndex === 'number'
    ? question.correctIndex
    : typeof question.correct === 'number'
      ? question.correct
      : typeof question.correct === 'string'
        ? Math.max(0, letters.indexOf(question.correct.toUpperCase()))
      : 0;
  const options = Array.isArray(question.options)
    ? question.options.map(option => String(option || '').replace(/^[A-D]\.\s*/, ''))
    : [];
  return {
    id: question.id || lessonId + '-q',
    prompt: question.prompt || question.stem || question.question || '',
    options: options,
    correctIndex: correctIndex,
    explanation: question.explanation || '',
    topic: question.topic || (question.topics && question.topics[0]) || ''
  };
}

function formatModelGuidance(question) {
  const correctOption = question.options && question.options.length
    ? question.options[question.correctIndex]
    : '';
  const parts = [
    correctOption ? 'Correct option: ' + correctOption + '.' : '',
    question.explanation || 'Use the class model answer or teacher solution for this question.'
  ].filter(Boolean);
  return parts.join(' ');
}

function loadStudySquadScript(src) {
  return new Promise((resolve, reject) => {
    const existing = document.querySelector('script[src="' + src + '"]');
    if (existing) {
      setTimeout(resolve, 60);
      return;
    }
    const script = document.createElement('script');
    script.src = src;
    script.async = true;
    script.onload = () => resolve();
    script.onerror = () => reject(new Error('Failed to load question pack ' + src));
    document.head.appendChild(script);
  });
}

function getSessionPhaseIndex(phaseKey) {
  return Math.max(0, SQ_SESSION_PHASES.findIndex(phase => phase.key === phaseKey));
}

function getSessionPhase(phaseKey) {
  return SQ_SESSION_PHASES.find(phase => phase.key === phaseKey) || SQ_SESSION_PHASES[0];
}

function getPhaseTasks(phase) {
  const tasks = {
    scheduled: [
      { icon: 'timer', title: 'Get ready', copy: 'Open the assigned pack and confirm the session time.' },
      { icon: 'flag', title: 'Wait for check-in', copy: 'Students should not submit answers until the attempt phase starts.' },
    ],
    check_in: [
      { icon: 'flag', title: 'Mark present', copy: 'Confirm you have joined the session.' },
      { icon: 'target', title: 'Set intent', copy: 'Decide which question skill you want to improve today.' },
    ],
    attempt: [
      { icon: 'pencil', title: 'Answer privately', copy: 'Write your own response before reading anyone else.' },
      { icon: 'clock', title: 'Stay independent', copy: 'Answers stay locked until the owner starts reveal.' },
    ],
    reveal: [
      { icon: 'book', title: 'Compare carefully', copy: 'Read the model guidance and notice missing criteria.' },
      { icon: 'check', title: 'Peer rubric', copy: 'Give constrained rubric feedback, not open commentary.' },
    ],
    repair: [
      { icon: 'pencil', title: 'Rewrite one response', copy: 'Use model guidance and peer rubric notes to improve one answer.' },
      { icon: 'target', title: 'Commit next step', copy: 'Choose one focused task before the next session.' },
    ],
    complete: [
      { icon: 'check', title: 'Close the loop', copy: 'Mark the session complete and record your next commitment.' },
      { icon: 'trophy', title: 'Review progress', copy: 'Use your progress page to see participation and feedback history.' },
    ],
    cancelled: [
      { icon: 'x', title: 'Session cancelled', copy: 'No further action is needed for this session.' },
    ],
  };
  return tasks[phase] || tasks.scheduled;
}

function Idea3() {
  return (
    <IdeaShell
      num="03"
      title="Study"
      titleEm="Squads"
      tagline={<><strong>Recurring question-practice groups.</strong> Small invite-only squads meet at set times, attempt the same question pack, reveal answers after the attempt window, and leave with one repaired response.</>}
      pills={[
        { kind: 'rose',  icon: 'flag', label: 'MVP · create squad' },
        { kind: 'green', icon: 'users', label: 'Invite-only' },
      ]}
      stats={[
        { num: '3-5', label: 'Students per squad', sub: 'Target 4 — small enough for accountability without turning into a public social space.' },
        { num: '0', label: 'Chat surfaces', sub: 'Communication waits until moderation, reporting, blocking, and retention rules are built.', dark: true },
      ]}
      pros={[
        'The product loop is <strong>attempt -> reveal -> compare -> repair -> commit</strong>, not casual messaging.',
        'Answers stay private until the session phase changes to reveal or complete.',
        'Invite-only membership keeps discovery narrow while the safety model is proven.',
        'The schema now supports squads, members, scheduled sessions, and private answer submissions.',
      ]}
    >
      <StudySquads/>
    </IdeaShell>
  );
}

window.Idea3 = Idea3;
window.StudySquads = StudySquads;
