// GENERATED from DNA/index.html (VISION_WORKER_URL, FormField, HandicapField, NavButtons, AdaptiveInterview, ProfileForm, goalSummaryText, GoalTree, InterviewForm, GoalSetting) by superstore-demo/build.mjs — do not edit. Edit DNA/index.html.
const { useState, useCallback, useMemo } = React;

    const VISION_WORKER_URL = (function(){ try { return localStorage.getItem('dna_worker_url') || 'https://golflab-dna-vision.liam-d07.workers.dev'; } catch(e) { return 'https://golflab-dna-vision.liam-d07.workers.dev'; } })();

    function FormField({ label, children, hint }) {
      return (
        <div className="form-group">
          <label>{label}</label>
          {children}
          {hint && <div style={{fontSize: 12, color: 'var(--text-light)', marginTop: 4}}>{hint}</div>}
        </div>
      );
    }

    function HandicapField({ label, value, onChange }) {
      const n = parseFloat(value);
      const known = isFinite(n);
      const isPlus = known && n < 0;
      const mag = known ? Math.abs(n) : '';
      const commit = (magStr, plus) => {
        const m = parseFloat(magStr);
        if (!isFinite(m)) return onChange('');
        onChange(String(plus ? -Math.abs(m) : Math.abs(m)));
      };
      const btn = (active) => ({
        flex: 1, padding: '8px 6px', fontSize: 13, fontWeight: 700, cursor: 'pointer',
        border: '1px solid ' + (active ? 'var(--orange)' : 'var(--border)'),
        background: active ? 'var(--orange)' : 'transparent',
        color: active ? '#fff' : 'var(--text-secondary)', borderRadius: 6
      });
      return (
        <FormField label={label}>
          <input type="number" step="0.1" min="0" value={mag}
                 placeholder="e.g. 6"
                 onChange={e => commit(e.target.value, isPlus)} />
          <div style={{display:'flex', gap:6, marginTop:6}}>
            <button type="button" style={btn(!isPlus)}
                    onClick={() => commit(mag === '' ? '' : mag, false)}>
              over par
            </button>
            <button type="button" style={btn(isPlus)}
                    onClick={() => commit(mag === '' ? '' : mag, true)}>
              under par&nbsp;(+)
            </button>
          </div>
          {known && (
            <div style={{fontSize:12.5, marginTop:5, fontWeight:700,
                         color: isPlus ? 'var(--green)' : 'var(--text-light)'}}>
              Recorded as <b>{isPlus ? '+' : ''}{mag}</b>
              {isPlus ? ' \u2014 ' + mag + ' under par' : (n === 0 ? ' \u2014 scratch' : ' \u2014 ' + mag + ' over par')}
            </div>
          )}
        </FormField>
      );
    }

    function NavButtons({ onPrev, onNext, nextLabel = 'Next Domain', prevLabel = 'Previous' }) {
      return (
        <div className="btn-group">
          <button className="btn btn-secondary" onClick={onPrev}>{prevLabel}</button>
          <button className="btn btn-primary" onClick={onNext}>{nextLabel} &#8250;</button>
        </div>
      );
    }

    function AdaptiveInterview({ role, data, update, contextData, onNext, onPrev }) {
      const [loading, setLoading] = useState(true);
      const [questions, setQuestions] = useState([]);
      // Answers live in data.interview, not in component state. They used to be local, so
      // navigating away from a step — which coaches do constantly, because the session does
      // not run in the app's order — silently discarded everything typed but not "Continue"d.
      // Stored per role alongside the questions that produced them, so returning to a step
      // restores the same questions with the same answers rather than re-asking.
      const answersKey = (role === 'coach' ? 'coachNotes' : role === 'reflection' ? 'reflectionNotes' : 'followupNotes') + 'Answers';
      const questionsKey = (role === 'coach' ? 'coachNotes' : role === 'reflection' ? 'reflectionNotes' : 'followupNotes') + 'Questions';
      const answers = (data && data[answersKey]) || {};
      const setAnswer = (i, v) => update(answersKey, Object.assign({}, answers, { [i]: v }));
      const [error, setError] = useState(false);
      const genericKey = (role === 'coach' ? 'coachNotes' : role === 'reflection' ? 'reflectionNotes' : 'followupNotes') + 'Generic';
      const generic = (data && data[genericKey]) || '';
      const setGeneric = (v) => update(genericKey, v);
      const fetchedRef = React.useRef(false);

      const isCoach = role === 'coach';
      const isReflection = role === 'reflection';
      const title = isCoach ? "Coach's Notes" : isReflection ? 'How Did Today Go?' : 'A Bit More About You';

      React.useEffect(() => {
        if (fetchedRef.current) return;
        fetchedRef.current = true;
        // Already asked on a previous visit to this step? Re-use them, so the answers on
        // screen still belong to the questions above them.
        const stored = (data && data[questionsKey]) || null;
        if (Array.isArray(stored) && stored.length) { setQuestions(stored); setLoading(false); return; }

        // Build a compact context object for the prompt.
        let context = {};
        if (isCoach) {
          const cd = contextData || {};
          const iv = cd.interview || {};
          const d8 = cd.domain8 || {};
          const d7 = cd.domain7 || {};
          const d3 = cd.domain3 || {};
          const d2 = cd.domain2 || {};
          const d4 = cd.domain4 || {};
          // HANDEDNESS, OR THE QUESTIONS COME OUT MIRRORED. Sam Coccari (26 Oct) swings
          // LEFT-handed and throws and writes right; his sports background reads "Hockey LH".
          // With no dexterity in the context the model inferred a RIGHT-handed golfer with a
          // left-handed hockey past — it wrote "the opposite of most right-handed golfers" at
          // him — and then asked whether his RIGHT miss was an open face. For a left-hander an
          // open face sends it LEFT, so the question offered a cause that cannot produce the
          // miss. Every directional question is wrong by mirror without this field.
          const HAND = {
            golfDexterity: iv.golfDexterity || undefined,
            // Cross-dominance is the interesting part, not noise: a left-swinging golfer who
            // throws and writes right has a dominant-side story worth asking about.
            throwHand: iv.throwHand || undefined,
            writeHand: iv.writeHand || undefined,
          };
          // EQUIPMENT, so the generated questions can ask about it. The engine deliberately
          // does NOT judge whether a set is a match — that belongs to a fitter (DOMAIN_QUEUE
          // Q25, closed 2026-10-12). What it can do is put the question in front of the one
          // person who can answer it from the session, with the clubs in their hands.
          const club = (k) => {
            const g = (f) => d7[k + '_' + f] || undefined;
            const o = { brand: g('eqBrand'), model: g('eqModel'), loft: g('eqLoft'),
                        lie: g('eqLie'), length: g('eqLength'), flex: g('eqFlex'),
                        grip: g('eqGrip') };
            return Object.values(o).some(v => v != null) ? o : undefined;
          };
          const equipment = { driver: club('driver'), iron7: club('iron7'), wedge: club('wedge') };
          const hasEquip = Object.values(equipment).some(v => v);
          // The measurements that make an equipment question worth asking at all: where on
          // the face the ball is being struck and how repeatable that is, and how repeatable
          // the face is. A question about lie angle means nothing without the second.
          const nz = (v) => (v != null && v !== '' ? Number(v) : undefined);
          const strikeAndFace = {
            driver_impactOffsetMm: nz(d8.driver_impactOffset),
            driver_impactOffsetSdMm: nz(d8.driver_impactOffsetSd),
            iron7_impactOffsetMm: nz(d8.iron7_impactOffset),
            iron7_impactOffsetSdMm: nz(d8.iron7_impactOffsetSd),
            driver_faceAngleSd: nz(d8.driver_faceAngleSd),
            iron7_faceAngle: nz(d8.iron7_faceAngle),
            iron7_faceAngleSd: nz(d8.iron7_faceAngleSd),
            driver_launchAngle: nz(d8.driver_launchAngle),
            driver_spinRate: nz(d8.driver_spinRate),
            driver_dynamicLoft: nz(d8.driver_dynamicLoft),
            driver_angleOfAttack: nz(d8.driver_angleOfAttack),
          };
          // What TrackMan's optimal window says about this delivery, if it computes — so a
          // question can name the actual gap rather than ask about equipment in the abstract.
          let launchWindow;
          try {
            const ef = (runDiagnosis(cd) || {}).equipFit;
            if (ef && ef.captured) {
              launchWindow = { idealLaunch: ef.target.launch, idealSpin: ef.target.spin,
                               fittingWouldChase: ef.goals, worthYards: ef.gainYds || undefined };
            }
          } catch (e) { launchWindow = undefined; }
          context = {
            ...HAND,
            name: [iv.firstName, iv.lastName].filter(Boolean).join(' ') || undefined,
            handicap: iv.handicap || undefined,
            goalSummary: iv.goalSummary || undefined,
            // self-report (for injury↔mobility↔delivery connections)
            injuryHistory: iv.injuryHistory || undefined,
            sportsParticipation: iv.sportsParticipation || undefined,
            strongestAspect: iv.strongestAspect || undefined,
            weakestAspect: iv.weakestAspect || undefined,
            preventingImprovement: iv.preventingImprovement || undefined,
            // domain 3 — mobility / stability
            deepSquat: d3.deepSquat || undefined,
            balanceL: d3.balanceL || undefined,
            balanceR: d3.balanceR || undefined,
            shoulderRot_L: d3.shoulderRot_basic_L || undefined,
            shoulderRot_R: d3.shoulderRot_basic_R || undefined,
            wristHinge_L: d3.wristHinge_basic_L || undefined,
            wristHinge_R: d3.wristHinge_basic_R || undefined,
            // domain 8 — driver delivery
            driver_clubPath: d8.driver_clubPath != null ? d8.driver_clubPath : undefined,
            driver_faceAngle: d8.driver_faceAngle != null ? d8.driver_faceAngle : undefined,
            driver_clubSpeed: d8.driver_clubSpeed != null ? d8.driver_clubSpeed : undefined,
            // domain 2 — cognition
            motorLearning: d2.motorLearning != null ? d2.motorLearning : undefined,
            motorControl: d2.motorControl != null ? d2.motorControl : undefined,
            // domain 4 — power
            verticalJump: d4.verticalJump != null ? d4.verticalJump : undefined,
            // domain 7 — equipment, and the delivery facts that bear on it
            equipment: hasEquip ? equipment : undefined,
            strikeAndFace,
            launchWindow
          };
        } else if (isReflection) {
          const cd = contextData || {};
          const iv = cd.interview || {};
          const d8 = cd.domain8 || {};
          const measured = {
            driver_clubPath: d8.driver_clubPath != null ? d8.driver_clubPath : undefined,
            driver_faceAngle: d8.driver_faceAngle != null ? d8.driver_faceAngle : undefined,
            driver_clubSpeed: d8.driver_clubSpeed != null ? d8.driver_clubSpeed : undefined,
            driver_carryDistance: d8.driver_carryDistance != null ? d8.driver_carryDistance : undefined
          };
          const hasKpiIssue = (d8.driver_clubPath != null && Math.abs(d8.driver_clubPath) >= 3)
            || (d8.driver_faceAngle != null && Math.abs(d8.driver_faceAngle) >= 3);
          context = {
            golfDexterity: iv.golfDexterity || undefined,
            throwHand: iv.throwHand || undefined,
            writeHand: iv.writeHand || undefined,
            // start-of-session self-assessment
            strongestAspect: iv.strongestAspect || undefined,
            weakestAspect: iv.weakestAspect || undefined,
            preventingImprovement: iv.preventingImprovement || undefined,
            goals: iv.goalSummary || undefined,
            // small measured summary (do not reveal raw numbers to the golfer)
            measured,
            hasKpiIssue
          };
        } else {
          const iv = contextData || {};
          context = {
            golfDexterity: iv.golfDexterity || undefined,
            throwHand: iv.throwHand || undefined,
            writeHand: iv.writeHand || undefined,
            sportsParticipation: iv.sportsParticipation || undefined,
            injuryHistory: iv.injuryHistory || undefined,
            strongestAspect: iv.strongestAspect || undefined,
            weakestAspect: iv.weakestAspect || undefined,
            preventingImprovement: iv.preventingImprovement || undefined,
            reasonForInstruction: iv.reasonForInstruction || undefined,
            goals: iv.goalSummary || undefined,
            handicap: iv.handicap || undefined
          };
        }

        (async () => {
          try {
            const resp = await fetch(VISION_WORKER_URL.replace(/\/$/, '') + '/questions', {
              method: 'POST',
              headers: { 'content-type': 'application/json' },
              body: JSON.stringify({ role, context })
            });
            const j = await resp.json();
            const qs = Array.isArray(j.questions) ? j.questions.filter(q => typeof q === 'string' && q.trim()) : [];
            if (qs.length) { setQuestions(qs); update(questionsKey, qs); } else { setError(true); }
          } catch (e) {
            setError(true);
          } finally {
            setLoading(false);
          }
        })();
      }, []);

      // Did the coach actually write anything? Both "Skip" and an empty "Continue" used to
      // leave coachNotes unset in silence — four consecutive real sessions (Scott Lynn,
      // Colin Kelleher, Arni Sveinsson, Sean Evans) went out with no coach interpretation
      // while the two before them had ~2,600 characters each.
      const hasContent = questions.length
        ? Object.values(answers).some(a => (a || '').trim().length > 0)
        : generic.trim().length > 0;
      const handleContinue = () => {
        const key = isCoach ? 'coachNotes' : isReflection ? 'reflectionNotes' : 'followupNotes';
        let blob;
        if (questions.length) {
          blob = questions.map((q, i) => 'Q: ' + q + '\nA: ' + (answers[i] || '—')).join('\n\n');
        } else {
          blob = generic.trim();
        }
        if (blob) update(key, blob);
        onNext();
      };
      // The coach step carries the two things only the coach can supply: the notes and the
      // KPI. They are ONE gate — assigning a KPI without recording why, or writing notes
      // without naming the KPI, leaves the report half-instructed.
      // A KPI with no measurable target is not an assignment, it is a direction. Grant Lee's
      // report shipped "Attack Angle: -2.6 deg -> more positive", which cannot be scored at
      // retest. The target is now required alongside the KPI itself.
      const coachReady = !isCoach || (hasContent && !!(data && (data.kpiOverride || '').trim())
                                                 && !!(data && (data.kpiTarget || '').trim()));

      return (
        <div>
          <div className="domain-banner">
            <div className="domain-number" style={{ color: 'var(--blue)' }}>{isCoach ? '✎' : isReflection ? '🎯' : '✨'}</div>
            <div className="domain-info">
              <h2>{title}</h2>
              <p>{isReflection ? "A few quick reflections on today's session — there are no wrong answers." : 'A few tailored questions to make your report more personal.'}</p>
            </div>
          </div>

          <div className="card" style={{ maxWidth: 760, margin: '0 auto' }}>
            <div className="card-header">{title}</div>
            <div className="card-subtitle">
              {isCoach
                ? 'Surface the observations only you can see — they get woven into the report.'
                : isReflection
                ? "A few quick reflections on today's session — there are no wrong answers."
                : 'A couple of quick, open questions. Answer as much or as little as you like.'}
            </div>

            {loading && (
              <div style={{ padding: '34px 4px', textAlign: 'center' }}>
                <div className="ai-spinner" />
                <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--blue)' }}>Reading your answers&hellip;</div>
                <div style={{ fontSize: 13, color: 'var(--text-light)', marginTop: 6 }}>Writing a few questions just for {isCoach ? 'this golfer' : 'you'} — this takes a few seconds.</div>
              </div>
            )}

            {!loading && questions.length > 0 && questions.map((q, i) => (
              <FormField key={i} label={q}>
                <textarea value={answers[i] || ''} onChange={e => setAnswer(i, e.target.value)} />
              </FormField>
            ))}

            {!loading && questions.length === 0 && (
              <FormField label="Anything else worth adding?">
                <textarea value={generic} onChange={e => setGeneric(e.target.value)} />
              </FormField>
            )}

            {isCoach && !loading && (() => {
              // The engine's own pick, shown so the assignment is informed rather than blind.
              // Every domain is entered by this step, so runDiagnosis is safe to call here.
              let sug = null;
              try {
                const r = runDiagnosis(contextData);
                sug = r && r.kpi ? r.kpi : null;
              } catch (e) { sug = null; }
              const LBL = { faceAngle:'Face Angle', clubPath:'Club Path', faceToPath:'Face to Path', clubSpeed:'Club Speed',
                            spinRate:'Spin Rate', angleOfAttack:'Attack Angle',
                            carryDistance:'Carry / Speed', impactOffset:'Centre Contact' };
              const chosen = data.kpiOverride || '';
              // Centre Contact is only offerable when the session MEASURED a strike — the
              // KPI's current value is the measured mean impact offset, so without one there
              // is nothing to assign or to score at retest.
              const strikeMeasured = isFinite(parseFloat(
                ((contextData && contextData.domain8) || {}).driver_impactOffset));
              return (
                <div style={{border:'1px solid var(--border)', borderRadius:8, padding:'14px 16px',
                             margin:'4px 0 16px', background:'var(--bg-light)'}}>
                  <div style={{fontWeight:800, fontSize:15, marginBottom:6}}>Assign the primary KPI</div>
                  <div style={{fontSize:13, color:'var(--text-light)', lineHeight:1.5, marginBottom:10}}>
                    You assign it; the report then interprets every domain through that lens and
                    prices strokes gained on it. The engine's own ranking is recorded either way —
                    that is how it earns the job back.
                  </div>
                  {sug && (
                    <div style={{fontSize:13, lineHeight:1.5, marginBottom:10, paddingLeft:10,
                                 borderLeft:'3px solid var(--blue)'}}>
                      <b>Engine suggests: {sug.label}</b>
                      {sug.current != null && <span> ({sug.current}{sug.unit === '°' ? '°' : ' ' + (sug.unit||'')})</span>}
                      <div style={{color:'var(--text-light)', marginTop:3}}>
                        {(sug.rationale || '').replace(/^Engine suggestion, not yet confirmed by a coach\. /, '')}
                      </div>
                    </div>
                  )}
                  <FormField label={chosen ? 'Your assignment' : 'Your assignment — required'}>
                    <select value={chosen} onChange={e => update('kpiOverride', e.target.value)}>
                      <option value="">Select the KPI&hellip;</option>
                      <option value="faceAngle">Face Angle</option>
                      <option value="clubPath">Club Path</option>
                      <option value="faceToPath">Face to Path (shift either side)</option>
                      <option value="clubSpeed">Club Speed</option>
                      <option value="spinRate">Spin Rate</option>
                      <option value="angleOfAttack">Attack Angle</option>
                      <option value="carryDistance">Carry / Speed</option>
                      {/* Centre Contact. Carson asked for it by name after Audrey Martin,
                          whose driver and 7-iron are both struck ~32 mm toward the toe: the
                          strike was the thing to centre the plan on and he had to assign
                          Carry / Speed instead. Priced through smash -> ball speed -> carry;
                          it needs no gear-effect model, so the strike-aware gate is not in
                          its way. Only offered when the session measured a strike. */}
                      {strikeMeasured && <option value="impactOffset">Centre Contact</option>}
                    </select>
                  </FormField>
                  {chosen && (
                    <div style={{marginTop:14, paddingTop:12, borderTop:'1px solid var(--border)'}}>
                      {/* WHOSE CALL. A coach override and a client request are different things
                          and the report should not blur them: one is professional judgement,
                          the other is the player telling you what they want to work on. Asked
                          for after Grant Lee's report (6 Oct) showed "Coach-assigned" with no
                          way to tell which it had been. */}
                      <FormField label="Whose decision was this KPI?">
                        <select value={data.kpiOwner || ''} onChange={e => update('kpiOwner', e.target.value)}>
                          <option value="">Select&hellip;</option>
                          <option value="coach">Mine — coach's judgement</option>
                          <option value="client">The client's — they asked for this</option>
                          <option value="shared">Agreed together</option>
                        </select>
                      </FormField>
                      {/* A TARGET THE PLAYER CAN SCORE THEMSELVES AGAINST. The engine's own
                          defaults are numeric for the four directional parameters and vague
                          for the rest — "more positive", "add speed", "toward optimum". Grant
                          Lee's whole report was built around "Attack Angle: −2.6° → more
                          positive", which cannot be checked at retest: −2.5° technically
                          passes. The coach assigns the range. */}
                      <FormField label={(data.kpiTarget || '').trim()
                                          ? 'Target range or benchmark'
                                          : 'Target range or benchmark — required'}>
                        <input type="text" value={data.kpiTarget || ''}
                               placeholder={(sug && sug.target) || 'e.g. 0 to +3°, or 95-100 mph'}
                               onChange={e => update('kpiTarget', e.target.value)} />
                      </FormField>
                      <div style={{fontSize:12.5, color:'var(--text-light)', lineHeight:1.5, marginTop:-4}}>
                        Give something measurable, not a direction. This is what the player is
                        scored against at retest, and it prints on the cover.
                      </div>
                    </div>
                  )}
                  {chosen && (
                    <div style={{marginTop:16, paddingTop:14, borderTop:'1px solid var(--border)'}}>
                      <div style={{fontWeight:800, fontSize:15, marginBottom:4}}>Timeline &amp; milestones</div>
                      <div style={{fontSize:13, color:'var(--text-light)', lineHeight:1.5, marginBottom:10}}>
                        What should be different, and by when. This builds the goal-setting page
                        and timeline at the end of the report — the player's roadmap, in your words.
                      </div>
                      <FormField label="How often should they be re-checked?">
                        <select value={data.retestInterval || ''}
                                onChange={e => update('retestInterval', e.target.value)}>
                          <option value="">Select&hellip;</option>
                          <option value="Every 2 weeks">Every 2 weeks</option>
                          <option value="Monthly">Monthly</option>
                          <option value="Every 6 weeks">Every 6 weeks</option>
                          <option value="Every 3 months">Every 3 months</option>
                        </select>
                      </FormField>
                      <FormField label="First milestone — what should be different at the next check?"
                                 hint="The nearest observable change. A number, a feel, a position held.">
                        <textarea rows={2} value={data.milestoneNear || ''}
                                  onChange={e => update('milestoneNear', e.target.value)} />
                      </FormField>
                      <FormField label="Mid-term — 3 months">
                        <textarea rows={2} value={data.milestoneMid || ''}
                                  onChange={e => update('milestoneMid', e.target.value)} />
                      </FormField>
                      <FormField label="The season goal — what does success look like?"
                                 hint="What this work is ultimately for.">
                        <textarea rows={2} value={data.milestoneSeason || ''}
                                  onChange={e => update('milestoneSeason', e.target.value)} />
                      </FormField>
                    </div>
                  )}
                  {chosen && sug && (
                    <div style={{fontSize:12.5, marginTop:2,
                                 color: chosen === sug.param ? 'var(--green)' : 'var(--orange)'}}>
                      {chosen === sug.param
                        ? '\u2713 Matches the engine. Recorded as agreement.'
                        : '\u26A0 Differs from the engine (' + (LBL[sug.param] || sug.param) +
                          '). Recorded as a disagreement — this is the signal that improves it.'}
                    </div>
                  )}
                  {!chosen && (
                    <div style={{fontSize:12.5, marginTop:2, color:'var(--orange)'}}>
                      Without an assignment the report prints the engine's suggestion as unconfirmed.
                    </div>
                  )}
                </div>
              );
            })()}
            {isCoach && !loading && !hasContent && (
              <div style={{background:'#FFF7ED', border:'1px solid #FDBA74', borderRadius:8,
                           padding:'10px 14px', marginTop:12, fontSize:13, lineHeight:1.5}}>
                <b>Nothing recorded yet.</b> Your notes are the one input the instruments cannot
                produce — they carry your reading of <i>why</i>, and the report is written around
                them. Leaving this blank publishes a report with the measurements but none of
                your interpretation.
              </div>
            )}
            <div className="btn-group">
              <button className="btn btn-secondary" onClick={onPrev}>Previous</button>
              {!loading && (
                <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
                  {isCoach && !coachReady && (
                    <span style={{fontSize:12.5, color:'var(--orange)', maxWidth:300, textAlign:'right'}}>
                      {!hasContent && !(data && (data.kpiOverride || '').trim())
                        ? 'Add your notes and assign the KPI to continue.'
                        : !hasContent ? 'Add your notes to continue.' : 'Assign the KPI to continue.'}
                    </span>
                  )}
                  {!isCoach && <button className="btn btn-secondary" onClick={onNext}>Skip</button>}
                  <button className="btn btn-primary" onClick={handleContinue} disabled={!coachReady}>
                    Continue &#8250;
                  </button>
                </div>
              )}
            </div>
          </div>
        </div>
      );
    }

    function ProfileForm({ data, update, onNext, onPrev }) {
      const age = (() => {
        if (!data.dateOfBirth) return null;
        const dob = new Date(data.dateOfBirth); if (isNaN(dob.getTime())) return null;
        const now = new Date(); let a = now.getFullYear() - dob.getFullYear();
        const m = now.getMonth() - dob.getMonth();
        if (m < 0 || (m === 0 && now.getDate() < dob.getDate())) a--;
        return (a >= 0 && a < 120) ? a : null;
      })();
      const lead = data.golfDexterity === 'Right' ? 'Left' : data.golfDexterity === 'Left' ? 'Right' : null;
      return (
        <div>
          <div className="domain-banner">
            <div className="domain-number" style={{color: 'var(--blue)'}}>&#128100;</div>
            <div className="domain-info">
              <h2>Player Profile</h2>
              <p>Who we're working with — the front door to every assessment.</p>
            </div>
          </div>
          <div className="card">
            <div className="section-divider">Identity</div>
            <div className="form-row">
              <FormField label="First Name">
                <input type="text" value={data.firstName||''} onChange={e => update('firstName', e.target.value)} />
              </FormField>
              <FormField label="Last Name">
                <input type="text" value={data.lastName||''} onChange={e => update('lastName', e.target.value)} />
              </FormField>
            </div>
            <div className="form-row">
              <FormField label="Email">
                <input type="email" value={data.email||''} onChange={e => update('email', e.target.value)} />
              </FormField>
              <FormField label="Phone">
                <input type="tel" value={data.phone||''} onChange={e => update('phone', e.target.value)} />
              </FormField>
            </div>
            <div className="section-divider">Demographics</div>
            <div className="form-row-3">
              <FormField label="Date of Birth" hint={age != null ? `Age ${age}` : 'Sets age-based context'}>
                <input type="date" value={data.dateOfBirth||''} onChange={e => update('dateOfBirth', e.target.value)} />
              </FormField>
              <FormField label="Gender">
                <select value={data.gender||''} onChange={e => update('gender', e.target.value)}>
                  <option value="">Select...</option>
                  <option>Male</option>
                  <option>Female</option>
                  <option>Prefer not to say</option>
                </select>
              </FormField>
              <FormField label="Golf Dexterity" hint={lead ? `${lead} side leads` : 'Sets lead / trail side'}>
                <select value={data.golfDexterity||''} onChange={e => update('golfDexterity', e.target.value)}>
                  <option value="">Select...</option>
                  <option>Right</option>
                  <option>Left</option>
                </select>
              </FormField>
            </div>
            <div className="form-row">
              <FormField label="Writing Hand">
                <select value={data.writeHand||''} onChange={e => update('writeHand', e.target.value)}>
                  <option value="">Select...</option>
                  <option>Right</option>
                  <option>Left</option>
                </select>
              </FormField>
              <FormField label="Throwing Hand">
                <select value={data.throwHand||''} onChange={e => update('throwHand', e.target.value)}>
                  <option value="">Select...</option>
                  <option>Right</option>
                  <option>Left</option>
                </select>
              </FormField>
            </div>
          </div>
          <NavButtons onPrev={onPrev} onNext={onNext} prevLabel="Back" nextLabel="Begin Interview" />
        </div>
      );
    }

    function goalSummaryText(g) {
      if (g.goalGoal === 'score') return 'Shoot lower scores';
      if (g.goalGoal === 'skill') {
        if (g.goalCategory === 'full') {
          if (g.goalAxis === 'distance') return 'Skill — More distance (' + (g.goalDetail === 'clubSpeed' ? 'club speed' : g.goalDetail === 'clubDelivery' ? 'club delivery' : '…') + ')';
          if (g.goalAxis === 'direction') return 'Skill — Better direction (' + (g.goalDetail === 'startLine' ? 'start line' : g.goalDetail === 'curve' ? 'curve control' : '…') + ')';
          return 'Skill — Full swing…';
        }
        if (g.goalCategory === 'short') {
          if (g.goalShotType === 'putting') return 'Skill — Putting (' + (g.goalPutt === 'startLine' ? 'start line' : g.goalPutt === 'speed' ? 'speed' : '…') + ')';
          if (g.goalShotType) return 'Skill — ' + g.goalShotType.charAt(0).toUpperCase() + g.goalShotType.slice(1);
          return 'Skill — Short game…';
        }
        return 'Skill…';
      }
      return '';
    }

    function GoalTree({ data, update }) {
      const Q = ({ label }) => <div style={{ fontWeight: 600, fontSize: 13, margin: '14px 0 8px', color: 'var(--text)' }}>{label}</div>;
      const Opt = ({ field, value, label, deps }) => (
        <button type="button" onClick={() => update(field, value)}
          className={'rating-btn' + (data[field] === value ? ' selected' : '')}
          style={{ marginRight: 8, marginBottom: 8 }}>{label}</button>
      );
      React.useEffect(() => {
        const s = goalSummaryText(data);
        if (s !== (data.goalSummary || '')) update('goalSummary', s);
      }, [data.goalGoal, data.goalCategory, data.goalAxis, data.goalDetail, data.goalShotType, data.goalPutt]);
      return (
        <div>
          <Q label="Q. Are you looking to shoot lower scores, or develop a specific skill?" />
          <Opt field="goalGoal" value="score" label="Shoot lower scores" />
          <Opt field="goalGoal" value="skill" label="Develop a specific skill" />

          {data.goalGoal === 'skill' && <>
            <Q label="Full swing or short game?" />
            <Opt field="goalCategory" value="full" label="Full Swing" />
            <Opt field="goalCategory" value="short" label="Short Game" />
          </>}

          {data.goalGoal === 'skill' && data.goalCategory === 'full' && <>
            <Q label="Distance or direction?" />
            <Opt field="goalAxis" value="distance" label="Distance" />
            <Opt field="goalAxis" value="direction" label="Direction" />
          </>}

          {data.goalGoal === 'skill' && data.goalCategory === 'full' && data.goalAxis === 'distance' && <>
            <Q label="A club-speed issue, or a club-delivery (efficiency) issue?" />
            <Opt field="goalDetail" value="clubSpeed" label="Club Speed" />
            <Opt field="goalDetail" value="clubDelivery" label="Club Delivery" />
          </>}
          {data.goalGoal === 'skill' && data.goalCategory === 'full' && data.goalAxis === 'direction' && <>
            <Q label="Starting it on line, or curving it off line?" />
            <Opt field="goalDetail" value="startLine" label="Start Line" />
            <Opt field="goalDetail" value="curve" label="Curve" />
          </>}

          {data.goalGoal === 'skill' && data.goalCategory === 'short' && <>
            <Q label="Which shot type?" />
            <Opt field="goalShotType" value="putting" label="Putting" />
            <Opt field="goalShotType" value="chipping" label="Chipping" />
            <Opt field="goalShotType" value="pitching" label="Pitching" />
            <Opt field="goalShotType" value="bunker" label="Bunker" />
          </>}
          {data.goalGoal === 'skill' && data.goalCategory === 'short' && data.goalShotType === 'putting' && <>
            <Q label="Start line or speed?" />
            <Opt field="goalPutt" value="startLine" label="Start Line" />
            <Opt field="goalPutt" value="speed" label="Speed" />
          </>}

          {data.goalSummary && goalSummaryText(data).indexOf('…') < 0 &&
            <div style={{ marginTop: 12, padding: '10px 12px', borderRadius: 8, background: '#EAF2FB', color: '#1D4ED8', border: '1px solid #BBD3F5', fontSize: 13, fontWeight: 600 }}>
              Committed goal: {goalSummaryText(data)}
            </div>}
        </div>
      );
    }

    function InterviewForm({ data, update, onNext, onPrev }) {
      // SLIDERS MUST RECORD WHAT THEY DISPLAY. Both of these render `data.x || <default>`,
      // so the coach sees a number — 30 rounds, 5 hrs — without it ever being stored; the
      // value is only written by onChange, which never fires if the shown value is already
      // right. Carson's session with Grant Lee read "30" on screen and reached the engine as
      // 0 rounds a year, which then set his engagement tier. Seed the defaults on mount so
      // the screen and the record agree. (CLAUDE.md's first question: does this create state
      // that is real but invisible?)
      React.useEffect(() => {
        if (data.roundsPerYear == null || data.roundsPerYear === '') update('roundsPerYear', '30');
        if (data.practiceHours == null || data.practiceHours === '') update('practiceHours', '5');
      }, []);
      return (
        <div>
          <div className="domain-banner">
            <div className="domain-number" style={{color: 'var(--blue)'}}>&#9998;</div>
            <div className="domain-info">
              <h2>Initial Student Interview</h2>
              <p>Background, goals and constraints. This data routes automatically to the relevant domains.</p>
            </div>
          </div>

          <div className="card">
            <div className="section-divider">Section 1 &mdash; Athletic Background</div>
            <FormField label="Sports Participation" hint="Please provide as much detail as possible">
              <textarea value={data.sportsParticipation||''} onChange={e => update('sportsParticipation', e.target.value)} placeholder="List all sports played, levels, and duration..." />
            </FormField>
            <FormField label="Injury History" hint="Please provide as much detail as possible">
              <textarea value={data.injuryHistory||''} onChange={e => update('injuryHistory', e.target.value)} placeholder="Describe any past or current injuries..." />
            </FormField>

            <div className="section-divider">Section 2 &mdash; Golf Profile</div>
            <div className="form-row-3">
              <HandicapField label="Current Handicap" value={data.handicap}
                             onChange={v => update('handicap', v)} />
              <HandicapField label="Lowest Handicap" value={data.lowestHandicap}
                             onChange={v => update('lowestHandicap', v)} />
              <FormField label="Rounds Per Year">
                <div className="slider-container">
                  <input type="range" min="0" max="200" value={data.roundsPerYear||30} onChange={e => update('roundsPerYear', e.target.value)} />
                  <span className="slider-value">{data.roundsPerYear||30}</span>
                </div>
              </FormField>
            </div>
            <div className="form-row">
              <FormField label="Divot Location">
                <select value={data.divotLocation||''} onChange={e => update('divotLocation', e.target.value)}>
                  <option value="">Select...</option>
                  <option>After the Ball</option>
                  <option>Under the Ball</option>
                  <option>Before the Ball</option>
                </select>
              </FormField>
              <FormField label="Greater Difficulty">
                <select value={data.greaterDifficulty||''} onChange={e => update('greaterDifficulty', e.target.value)}>
                  <option value="">Select...</option>
                  <option>Generating Power</option>
                  <option>Controlling Direction</option>
                </select>
              </FormField>
            </div>

            <div className="section-divider">Section 3 &mdash; Self-Assessment</div>
            <FormField label="Strongest Aspect of Your Game" hint="Please provide detail">
              <textarea value={data.strongestAspect||''} onChange={e => update('strongestAspect', e.target.value)} />
            </FormField>
            <FormField label="Weakest Aspect of Your Game" hint="Please provide detail">
              <textarea value={data.weakestAspect||''} onChange={e => update('weakestAspect', e.target.value)} />
            </FormField>
            <FormField label="What is Preventing Improvement?">
              <textarea value={data.preventingImprovement||''} onChange={e => update('preventingImprovement', e.target.value)} />
            </FormField>
            <FormField label="Reason for Seeking Instruction">
              <textarea value={data.reasonForInstruction||''} onChange={e => update('reasonForInstruction', e.target.value)} />
            </FormField>

            <div className="section-divider">Section 4 &mdash; Resources</div>
            <div className="form-row-3">
              <FormField label="Practice Hours / Week">
                <div className="slider-container">
                  <input type="range" min="0" max="30" value={data.practiceHours||5} onChange={e => update('practiceHours', e.target.value)} />
                  <span className="slider-value">{data.practiceHours||5}</span>
                </div>
              </FormField>
              <FormField label="Fitness Training Hours / Week">
                <input type="number" step="0.5" value={data.fitnessHours||''} onChange={e => update('fitnessHours', e.target.value)} />
              </FormField>
              <FormField label="Annual Improvement Budget">
                <input type="text" value={data.annualBudget||''} onChange={e => update('annualBudget', e.target.value)} placeholder="$" />
              </FormField>
            </div>

            <div className="btn-group">
              <div></div>
              <button className="btn btn-primary" onClick={onNext}>Continue to Goal Setting &#8250;</button>
            </div>
          </div>
        </div>
      );
    }

    function GoalSetting({ data, update, onNext, onPrev }) {
      // Ordered, branching question list based on current answers.
      const questions = (g) => {
        const qs = [{
          key: 'goalGoal', title: 'What are you here to do?',
          sub: "This is the single most important answer in the whole assessment — it decides which numbers matter.",
          options: [
            { value: 'score', label: 'Shoot lower scores', desc: 'Let the data find the biggest scoring leak.' },
            { value: 'skill', label: 'Develop a specific skill', desc: "You've got something specific you want to change." },
          ]
        }];
        if (g.goalGoal === 'skill') {
          qs.push({ key: 'goalCategory', title: 'Full swing or short game?', sub: 'Where does this skill live?',
            options: [{ value: 'full', label: 'Full Swing', desc: 'Driver through irons.' }, { value: 'short', label: 'Short Game', desc: 'Inside ~50 yards, and putting.' }] });
          if (g.goalCategory === 'full') {
            qs.push({ key: 'goalAxis', title: 'Distance or direction?', sub: 'What matters more to you right now?',
              options: [{ value: 'distance', label: 'Distance', desc: 'You want it to go further.' }, { value: 'direction', label: 'Direction', desc: 'You want it to go where you aim.' }] });
            if (g.goalAxis === 'distance') qs.push({ key: 'goalDetail', title: 'Speed or delivery?', sub: 'Is it about raw speed, or how efficiently you deliver it?',
              options: [{ value: 'clubSpeed', label: 'Club Speed', desc: 'You need more engine.' }, { value: 'clubDelivery', label: 'Club Delivery', desc: "You've got speed but lose it at impact." }] });
            if (g.goalAxis === 'direction') qs.push({ key: 'goalDetail', title: 'Start line or curve?', sub: 'Where does it go wrong?',
              options: [{ value: 'startLine', label: 'Start Line', desc: 'It starts off your intended line.' }, { value: 'curve', label: 'Curve', desc: 'It starts okay then curves away.' }] });
          } else if (g.goalCategory === 'short') {
            qs.push({ key: 'goalShotType', title: 'Which shot type?', sub: 'Pick the area to focus on.',
              options: [{ value: 'putting', label: 'Putting' }, { value: 'chipping', label: 'Chipping' }, { value: 'pitching', label: 'Pitching' }, { value: 'bunker', label: 'Bunker' }] });
            if (g.goalShotType === 'putting') qs.push({ key: 'goalPutt', title: 'Start line or speed?', sub: 'The two halves of every putt.',
              options: [{ value: 'startLine', label: 'Start Line' }, { value: 'speed', label: 'Speed / Distance Control' }] });
          }
        }
        return qs;
      };
      // panel: 0 = intro, 1..N = questions, N+1 = confirm
      const [panel, setPanel] = useState(0);
      const qs = questions(data);
      const complete = goalSummaryText(data).indexOf('…') < 0 && !!data.goalGoal;

      React.useEffect(() => {
        const s = goalSummaryText(data);
        if (s !== (data.goalSummary || '')) update('goalSummary', s);
      }, [data.goalGoal, data.goalCategory, data.goalAxis, data.goalDetail, data.goalShotType, data.goalPutt]);

      const pick = (key, value) => {
        update(key, value);
        // advance after the choice; recompute next panel from the (soon-to-be) answers
        const next = { ...data, [key]: value };
        const nq = questions(next);
        const idx = nq.findIndex(q => q.key === key);
        setPanel(idx + 1 < nq.length ? idx + 2 : nq.length + 1); // next question, or confirm
      };

      // ---- Intro ----
      if (panel === 0) {
        return (
          <div>
            <div className="domain-banner">
              <div className="domain-number" style={{ color: 'var(--blue)' }}>&#127919;</div>
              <div className="domain-info"><h2>Goal Setting</h2><p>The foundation of everything that follows.</p></div>
            </div>
            <div className="card" style={{ maxWidth: 720, margin: '0 auto' }}>
              <div className="card-header">Let's get clear on what success looks like</div>
              <div style={{ fontSize: 15, lineHeight: 1.7, color: 'var(--text)', margin: '8px 0 4px' }}>
                <p>Before we read a single number, we need to know what you're chasing. The goal you set here is the <strong>lens for your entire assessment</strong> — it decides which of the hundreds of data points we capture actually matter to <em>you</em>.</p>
                <p>A precise goal lets us aim the whole diagnosis at the one or two changes that will move your game the most. A vague goal scatters the effort across things that won't. This is genuinely where our work together succeeds or stalls — so take a moment and be honest about what a win looks like.</p>
                <p style={{ color: 'var(--text-light)' }}>It's only a few quick questions. We'll build the entire plan around your answers.</p>
              </div>
              <div className="btn-group">
                <button className="btn btn-secondary" onClick={onPrev}>Previous</button>
                <button className="btn btn-primary" onClick={() => setPanel(1)}>Let's set my goal &#8250;</button>
              </div>
            </div>
          </div>
        );
      }

      // ---- Confirm ----
      if (panel > qs.length) {
        return (
          <div>
            <div className="domain-banner">
              <div className="domain-number" style={{ color: 'var(--blue)' }}>&#127919;</div>
              <div className="domain-info"><h2>Goal Setting</h2><p>Confirm your committed goal.</p></div>
            </div>
            <div className="card" style={{ maxWidth: 720, margin: '0 auto' }}>
              <div className="card-header">Your committed goal</div>
              <div style={{ textAlign: 'center', padding: '24px 16px' }}>
                <div style={{ fontSize: 13, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--text-light)', marginBottom: 8 }}>We will build the plan around</div>
                <div style={{ fontSize: 26, fontWeight: 700, color: 'var(--blue)' }}>{goalSummaryText(data)}</div>
                <button className="btn btn-secondary" style={{ marginTop: 16 }} onClick={() => setPanel(1)}>Start over</button>
              </div>
              <FormField label="Anything else, in your words? (optional)" hint="Context, a target number, a date — whatever matters to you.">
                <textarea value={data.goals || ''} onChange={e => update('goals', e.target.value)} placeholder="e.g. I want to break 80 by the end of the season…" />
              </FormField>
              <div className="btn-group">
                <button className="btn btn-secondary" onClick={() => setPanel(qs.length)}>Previous</button>
                <button className="btn btn-primary" onClick={onNext}>Lock it in &mdash; Continue to Domain 1 &#8250;</button>
              </div>
            </div>
          </div>
        );
      }

      // ---- A single question ----
      const q = qs[panel - 1] || qs[qs.length - 1];
      return (
        <div>
          <div className="domain-banner">
            <div className="domain-number" style={{ color: 'var(--blue)' }}>&#127919;</div>
            <div className="domain-info"><h2>Goal Setting</h2><p>Question {panel} of {qs.length}</p></div>
          </div>
          <div className="card" style={{ maxWidth: 720, margin: '0 auto' }}>
            <div style={{ fontSize: 22, fontWeight: 700, color: 'var(--dark)', marginBottom: 6 }}>{q.title}</div>
            {q.sub && <div style={{ fontSize: 14, color: 'var(--text-light)', marginBottom: 22 }}>{q.sub}</div>}
            <div style={{ display: 'grid', gap: 12 }}>
              {q.options.map(o => {
                const sel = data[q.key] === o.value;
                return (
                  <button key={o.value} type="button" onClick={() => pick(q.key, o.value)}
                    style={{
                      textAlign: 'left', padding: '16px 18px', borderRadius: 12, cursor: 'pointer',
                      border: '2px solid ' + (sel ? 'var(--blue)' : 'var(--border)'),
                      background: sel ? 'rgba(29,78,216,0.06)' : 'var(--bg-light)', transition: 'all 0.15s'
                    }}>
                    <div style={{ fontSize: 16, fontWeight: 700, color: sel ? 'var(--blue)' : 'var(--text)' }}>{o.label}</div>
                    {o.desc && <div style={{ fontSize: 13, color: 'var(--text-light)', marginTop: 3 }}>{o.desc}</div>}
                  </button>
                );
              })}
            </div>
            <div className="btn-group">
              <button className="btn btn-secondary" onClick={() => setPanel(panel - 1)}>Previous</button>
              {data[q.key] && <button className="btn btn-primary" onClick={() => setPanel(panel + 1 > qs.length ? qs.length + 1 : panel + 1)}>Next &#8250;</button>}
            </div>
          </div>
        </div>
      );
    }
