/* EQD2 Calculator — React app (v2) */
/* global React, ReactDOM, EQD2Engine */

const { useState, useEffect, useRef, useCallback, useMemo, useLayoutEffect } = React;
const E = EQD2Engine;

// -----------------------------------------------------------------------------
// Tweak defaults
// -----------------------------------------------------------------------------
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "teal",
  "background": "paper",
  "typePair": "plex",
  "density": "comfortable"
}/*EDITMODE-END*/;

// -----------------------------------------------------------------------------
// Storage
// -----------------------------------------------------------------------------
const STORAGE_KEY = 'eqd2calc/v1';
const ATTESTATION_KEY = 'eqd2_attestation_v1';
const CURRENT_ATTESTATION_VERSION = '2026-05-29';

function loadState() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (!parsed || parsed.version !== 1) {
      if (parsed && parsed.version !== undefined) {
        console.warn('[EQD2] localStorage schema version mismatch — discarding stored state.', parsed.version);
      }
      return null;
    }
    return parsed;
  } catch (e) { return null; }
}
function saveState(state) {
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...state, version: 1 })); } catch (e) {}
}

// -----------------------------------------------------------------------------
// Model
// -----------------------------------------------------------------------------
let _courseIdCounter = 0;
function newCourse() {
  _courseIdCounter += 1;
  return {
    id: `c_${Date.now()}_${_courseIdCounter}`,
    title: '',
    D: null,
    n: null,
    d: null,
    editHistory: [],
    recoveryPct: 0,
  };
}

// -----------------------------------------------------------------------------
// Summary text
// -----------------------------------------------------------------------------
function summarizeCourses(courses, refDose, tissueRecoveryActive, customAb) {
  const valid = courses
    .map((c, i) => ({ c, i }))
    .filter(({ c }) => E.isPositive(c.D) && E.isPositive(c.n));

  if (valid.length === 0) return null;

  const isSingle   = valid.length === 1;
  const showCustom = E.isPositive(customAb);
  const title      = (c, i) => (c.title && c.title.trim()) || `Course ${i + 1}`;
  const regimenStr = c => `${E.fmt1(c.D)} Gy / ${c.n} fx`;
  const eqd        = (c, ab) => E.computeEQD(c.D, c.n, ab, refDose);

  // Cumulative raw values — same computation path as cumulativeRows rows
  const cum3   = valid.reduce((s, { c }) => s + eqd(c, 3),       0);
  const cum10  = valid.reduce((s, { c }) => s + eqd(c, 10),      0);
  const cumCus = showCustom ? valid.reduce((s, { c }) => s + eqd(c, customAb), 0) : null;

  // Course descriptor for the raw block
  const descRaw = isSingle
    ? `${title(valid[0].c, valid[0].i)} (${regimenStr(valid[0].c)}).`
    : `Composite: ${valid.map(({ c, i }) => `${title(c, i)} (${regimenStr(c)})`).join(' + ')}.`;

  // Line-label helpers — α/β=3 first, α/β=10 second, custom third
  const rawLbl = ab => isSingle ? `EQD2 (α/β=${ab}):` : `Cumulative EQD2 (α/β=${ab}):`;
  const block1 = [
    descRaw,
    `${rawLbl(3)} ${E.fmt1(cum3)} Gy`,
    `${rawLbl(10)} ${E.fmt1(cum10)} Gy`,
    ...(showCustom ? [`${rawLbl(customAb)} ${E.fmt1(cumCus)} Gy`] : []),
  ].join('\n');

  // Case A: recovery off — single block
  if (!tissueRecoveryActive) return block1;

  // Case B: recovery on, all courses at 0% — single block
  const anyRecovery = valid.some(({ c }) => (c.recoveryPct || 0) > 0);
  if (!anyRecovery) return block1;

  // Case C: recovery on, at least one course > 0% — two blocks separated by \n\n
  // Recovery-adjusted values — identical formula to cumulativeRows.eqdRecovery
  const applyR  = (c, ab) => E.applyRecoverness(eqd(c, ab), c.recoveryPct || 0);
  const cum3r   = valid.reduce((s, { c }) => s + applyR(c, 3),       0);
  const cum10r  = valid.reduce((s, { c }) => s + applyR(c, 10),      0);
  const cumCusr = showCustom ? valid.reduce((s, { c }) => s + applyR(c, customAb), 0) : null;

  const regimenStrR = c => {
    const pct = c.recoveryPct || 0;
    return pct > 0 ? `${regimenStr(c)} with ${pct}% recovery` : regimenStr(c);
  };
  const descRcv = isSingle
    ? `${title(valid[0].c, valid[0].i)} (${regimenStrR(valid[0].c)}).`
    : `Composite: ${valid.map(({ c, i }) => `${title(c, i)} (${regimenStrR(c)})`).join(' + ')}.`;

  const rcvLbl = ab => isSingle
    ? `EQD2 accounting for recovery (α/β=${ab}):`
    : `Cumulative EQD2 accounting for recovery (α/β=${ab}):`;
  const block2 = [
    descRcv,
    `${rcvLbl(3)} ${E.fmt1(cum3r)} Gy`,
    `${rcvLbl(10)} ${E.fmt1(cum10r)} Gy`,
    ...(showCustom ? [`${rcvLbl(customAb)} ${E.fmt1(cumCusr)} Gy`] : []),
  ].join('\n');

  return `${block1}\n\n${block2}`;
}

// -----------------------------------------------------------------------------
// Cumulative rows (summed across all valid courses)
// -----------------------------------------------------------------------------
function cumulativeRows(courses, customAb, refDose, tissueRecoveryActive) {
  const valid = courses.filter(c => E.isPositive(c.D) && E.isPositive(c.n));
  const sumFor = (ab, ref = refDose) => {
    if (!valid.length) return { bed: null, eqd: null, eqdRecovery: null };
    const bed = valid.reduce((s, c) => s + E.computeBED(c.D, c.n, ab), 0);
    const eqd = valid.reduce((s, c) => s + E.computeEQD(c.D, c.n, ab, ref), 0);
    const eqdRecovery = tissueRecoveryActive
      ? valid.reduce((s, c) => {
          const raw = E.computeEQD(c.D, c.n, ab, ref);
          return s + E.applyRecoverness(raw, c.recoveryPct || 0);
        }, 0)
      : null;
    return { bed, eqd, eqdRecovery };
  };
  return [
    { key: 'late',   ab: 3,        ...sumFor(3) },
    { key: 'tumor',  ab: 10,       ...sumFor(10) },
    { key: 'custom', ab: customAb, editable: true, ...sumFor(customAb) },
  ];
}

// -----------------------------------------------------------------------------
// Relative time
// -----------------------------------------------------------------------------
function relativeTime(fromISO) {
  if (!fromISO) return null;
  const then = new Date(fromISO).getTime();
  const now = Date.now();
  const mins = Math.round((now - then) / 60000);
  if (mins < 1) return { text: 'just now', hours: 0 };
  if (mins < 60) return { text: `${mins} min ago`, hours: mins / 60 };
  const hours = Math.round(mins / 60);
  if (hours < 24) return { text: `${hours} hour${hours === 1 ? '' : 's'} ago`, hours };
  return {
    text: new Date(fromISO).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),
    hours: hours,
  };
}

// =============================================================================
// Icons
// =============================================================================
const CloseIcon = () => (
  <svg viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true">
    <path d="M5 5l10 10M15 5L5 15" />
  </svg>
);
const CopyIcon = () => (
  <svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <rect x="7" y="7" width="9" height="10" rx="1.5" />
    <path d="M13 7V5a1.5 1.5 0 0 0-1.5-1.5h-6A1.5 1.5 0 0 0 4 5v8.5A1.5 1.5 0 0 0 5.5 15H7" />
  </svg>
);
const CheckIcon = () => (
  <svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path d="M4 10.5l4 4 8-9" />
  </svg>
);
const PlusIcon = () => (
  <svg viewBox="0 0 20 20" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true">
    <path d="M10 4v12M4 10h12" />
  </svg>
);
const PencilIcon = () => (
  <svg viewBox="0 0 20 20" width="11" height="11" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path d="M13 4l3 3-9 9H4v-3z" />
  </svg>
);
const MinusIcon = () => (
  <svg viewBox="0 0 20 20" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true">
    <path d="M4 10h12" />
  </svg>
);

// =============================================================================
// RecoverySlider — native range input with design-token styling
// =============================================================================
function RecoverySlider({ recoveryPct, onChange, onCommit, label }) {
  const startRef = useRef(null);
  const committedRef = useRef(true);

  function tryCommit(currentVal) {
    if (!committedRef.current && startRef.current !== null && currentVal !== startRef.current) {
      onCommit(startRef.current);
    }
    committedRef.current = true;
  }

  function onKeyDown(e) {
    if (e.key === 'PageUp')   { e.preventDefault(); onChange(Math.min(50, recoveryPct + 5)); }
    if (e.key === 'PageDown') { e.preventDefault(); onChange(Math.max(0,  recoveryPct - 5)); }
  }

  return (
    <input
      type="range"
      className="rcv-slider"
      min="0"
      max="50"
      step="1"
      value={recoveryPct}
      aria-label={label}
      onPointerDown={() => {
        startRef.current = recoveryPct;
        committedRef.current = false;
      }}
      onPointerUp={e => tryCommit(Number(e.target.value))}
      onChange={e => {
        const val = Number(e.target.value);
        e.currentTarget.style.setProperty('--rcv-fill', (val * 2) + '%');
        if (committedRef.current) {
          // First native change via keyboard arrows when already focused post-commit.
          startRef.current = recoveryPct;
          committedRef.current = false;
        }
        onChange(val);
      }}
      onFocus={() => {
        if (committedRef.current) {
          startRef.current = recoveryPct;
          committedRef.current = false;
        }
      }}
      onBlur={e => {
        tryCommit(Number(e.target.value));
        startRef.current = null;
      }}
      onKeyDown={onKeyDown}
      style={{ '--rcv-fill': (recoveryPct * 2) + '%' }}
    />
  );
}

// =============================================================================
// RecoveryNumInput — synchronized numeric input for recoveryPct
// =============================================================================
function RecoveryNumInput({ recoveryPct, onChange, onCommit }) {
  const [text, setText] = useState(String(recoveryPct));
  const focused = useRef(false);
  const startRef = useRef(null);

  useEffect(() => {
    if (!focused.current) setText(String(recoveryPct));
  }, [recoveryPct]);

  return (
    <div className="rcv-input">
      <input
        type="text"
        inputMode="decimal"
        className="rcv-input__field"
        value={text}
        onChange={e => {
          const raw = e.target.value.replace(/[^0-9]/g, '');
          setText(raw);
          const n = Math.max(0, Math.min(50, parseInt(raw, 10) || 0));
          onChange(n);
        }}
        onFocus={() => {
          focused.current = true;
          startRef.current = recoveryPct;
        }}
        onBlur={() => {
          focused.current = false;
          const n = Math.max(0, Math.min(50, parseInt(text, 10) || 0));
          setText(String(n));
          if (n !== startRef.current) {
            onCommit(startRef.current);
          }
          startRef.current = null;
        }}
        onKeyDown={e => { if (e.key === 'Enter') e.currentTarget.blur(); }}
        aria-label="Tissue recovery percentage"
      />
      <span className="rcv-input__pct">%</span>
    </div>
  );
}

// =============================================================================
// SelfTestBanner — shown when math self-tests fail at load
// =============================================================================
function SelfTestBanner() {
  return (
    <div className="self-test-banner" role="alert">
      <strong>Math self-tests failed.</strong> Do not trust calculations. See console for details.
    </div>
  );
}

// =============================================================================
// TriangleField — a numeric input for the Magic Triangle
// The local string is the source of truth while focused; syncs from props
// only when NOT focused. A `resetToken` prop lets parent force-sync (e.g.
// after undo). Uncontrolled pattern for keys ensures no dropped input.
// =============================================================================
function TriangleField({ label, suffix, value, onCommit, ariaLabel, autoFocus, resetToken, skipTab, initialText, onDidConsumeInitial }) {
  const toDisplay = (v) => {
    if (v === null || v === undefined || v === '') return '';
    if (typeof v === 'number') {
      if (!isFinite(v)) return '';
      // 2-decimal rounding for display, drop trailing zeros
      const rounded = Math.round(v * 100) / 100;
      return String(rounded);
    }
    return String(v);
  };

  const [text, setText] = useState(() => initialText != null ? String(initialText) : toDisplay(value));
  const inputRef = useRef(null);
  const focusedRef = useRef(false);
  // Suppress the first value-sync when the field was seeded, so the seed
  // survives until the focus effect commits (otherwise the sync effect wipes it).
  const seededRef = useRef(initialText != null);

  // Sync from parent only when unfocused.
  useEffect(() => {
    if (seededRef.current) { seededRef.current = false; return; }
    if (!focusedRef.current) setText(toDisplay(value));
  }, [value, resetToken]);

  useEffect(() => {
    if (autoFocus && inputRef.current) {
      inputRef.current.focus();
      if (initialText != null) {
        // Preserve seeded digit(s), place caret at end instead of selecting.
        const len = inputRef.current.value.length;
        try { inputRef.current.setSelectionRange(len, len); } catch {}
        if (onDidConsumeInitial) onDidConsumeInitial();
      } else {
        inputRef.current.select();
      }
    }
  }, [autoFocus]);

  const commit = () => {
    const t = text.trim();
    if (t === '') { onCommit(null); return; }
    const n = Number(t);
    if (!isFinite(n) || n < 0) { onCommit(null); return; }
    onCommit(n);
  };

  return (
    <label className="tf">
      <span className="tf__label">{label}</span>
      <span className="tf__shell">
        <input
          ref={inputRef}
          type="text"
          inputMode="decimal"
          value={text}
          aria-label={ariaLabel || label}
          autoComplete="off"
          tabIndex={skipTab ? -1 : 0}
          onChange={e => setText(e.target.value)}
          onFocus={() => { focusedRef.current = true; }}
          onBlur={() => { focusedRef.current = false; commit(); }}
          onKeyDown={e => {
            if (e.key === 'Enter') { e.currentTarget.blur(); }
            if (e.key === 'Escape') { setText(toDisplay(value)); e.currentTarget.blur(); }
          }}
        />
        {suffix && <span className="tf__suffix">{suffix}</span>}
      </span>
    </label>
  );
}

// =============================================================================
// InlineNumber — for custom α/β
// =============================================================================
function InlineNumber({ value, onCommit, ariaLabel, min = 0 }) {
  const [text, setText] = useState(String(value));
  const focusedRef = useRef(false);

  useEffect(() => {
    if (!focusedRef.current) setText(String(value));
  }, [value]);

  return (
    <input
      type="text"
      inputMode="decimal"
      className="inline-num"
      value={text}
      aria-label={ariaLabel}
      onChange={e => setText(e.target.value)}
      onFocus={e => { focusedRef.current = true; e.target.select(); }}
      onBlur={() => {
        focusedRef.current = false;
        const n = Number(text);
        if (isFinite(n) && n > min) onCommit(n);
        else setText(String(value));
      }}
      onKeyDown={e => {
        if (e.key === 'Enter') e.currentTarget.blur();
        if (e.key === 'Escape') { setText(String(value)); e.currentTarget.blur(); }
      }}
    />
  );
}

// =============================================================================
// Course card — just triangle + subtle rename affordance
// =============================================================================
function CourseCard({ course, index, onUpdate, onUpdateImmediate, onCommitRecovery, onRemove, canRemove, autoFocus, initialDoseText, onDidConsumeInitial, showInlineEqd2, tissueRecoveryActive, refDose, resetToken }) {
  const [editingTitle, setEditingTitle] = useState(false);
  const [titleText, setTitleText] = useState('');
  const titleRef = useRef(null);
  const titleEscapeRef = useRef(false);
  const placeholder = `Course ${index + 1}`;

  useEffect(() => {
    if (editingTitle) {
      setTitleText(course.title);
      if (titleRef.current) { titleRef.current.focus(); titleRef.current.select(); }
    }
  }, [editingTitle]);

  function commitField(field, val) {
    const next = { ...course, [field]: val };
    const history = [field, ...(course.editHistory || []).filter(f => f !== field)].slice(0, 3);
    next.editHistory = history;
    const rec = E.reconcileTriangle(next);
    next.D = rec.D; next.n = rec.n; next.d = rec.d;
    onUpdate(next);
  }

  const eqdAb3 = E.computeEQD(course.D, course.n, 3, refDose);
  const recoveryPct = course.recoveryPct || 0;
  const showRhs = showInlineEqd2 || tissueRecoveryActive;
  const recoveryVal = (tissueRecoveryActive && recoveryPct > 0 && eqdAb3 !== null)
    ? E.applyRecoverness(eqdAb3, recoveryPct)
    : null;
  const refLabel = refDose === 2 ? '2' : E.fmt1(refDose);

  return (
    <section className="course" data-course-id={course.id} data-screen-label={`Course ${index + 1}`}>
      <header className="course__head">
        <div className="course__label">
          {editingTitle ? (
            <input
              ref={titleRef}
              className="course__title-input"
              type="text"
              value={titleText}
              placeholder={placeholder}
              autoCorrect="off"
              autoCapitalize="off"
              spellCheck={false}
              onChange={e => setTitleText(e.target.value)}
              onBlur={() => {
                if (!titleEscapeRef.current) onUpdate({ ...course, title: titleText });
                titleEscapeRef.current = false;
                setEditingTitle(false);
              }}
              onKeyDown={e => {
                if (e.key === 'Enter') e.currentTarget.blur();
                if (e.key === 'Escape') { titleEscapeRef.current = true; e.currentTarget.blur(); }
              }}
            />
          ) : (
            <button
              type="button"
              className={`course__title ${course.title ? 'course__title--set' : ''}`}
              onClick={() => setEditingTitle(true)}
              aria-label={`Rename ${course.title || placeholder}`}
              title="Click to rename"
            >
              {course.title || placeholder}
              <span className="course__title-edit" aria-hidden="true"><PencilIcon /></span>
            </button>
          )}
        </div>
        {canRemove && (
          <button
            className="course__remove"
            type="button"
            aria-label={`Remove ${course.title || placeholder}`}
            onClick={() => onRemove(course.id)}
            tabIndex={-1}
          >
            <CloseIcon />
          </button>
        )}
      </header>

      <div className="triangle-row">
        <div className="triangle">
          <TriangleField
            label="Total dose"
            suffix="Gy"
            value={course.D}
            onCommit={v => commitField('D', v)}
            ariaLabel="Total dose in Gy"
            autoFocus={autoFocus}
            initialText={initialDoseText}
            onDidConsumeInitial={onDidConsumeInitial}
            resetToken={resetToken}
          />
          <span className="triangle__op" aria-hidden="true">/</span>
          <TriangleField
            label="Fractions"
            value={course.n}
            onCommit={v => commitField('n', v)}
            ariaLabel="Number of fractions"
            resetToken={resetToken}
          />
          <span className="triangle__op" aria-hidden="true">=</span>
          <TriangleField
            label="Dose / fx"
            suffix="Gy"
            value={course.d}
            onCommit={v => commitField('d', v)}
            ariaLabel="Dose per fraction in Gy"
            skipTab
            resetToken={resetToken}
          />
        </div>
        {showRhs && (
          <div className="course-rhs">
            <div className="course-rhs__eqd2">
              {eqdAb3 !== null ? (
                <>
                  <span className="course-rhs__eqd2-label">EQD<sub>{refLabel}</sub><span className="course-rhs__eqd2-ab"> (α/β&nbsp;=&nbsp;3)</span>:</span>
                  <span className="course-rhs__eqd2-raw">{E.fmt1(eqdAb3)}</span>
                  {recoveryVal !== null && (
                    <>
                      <span className="course-rhs__eqd2-arrow"><span className="eqd2-arrow">→</span></span>
                      <span className="course-rhs__eqd2-rcv">{E.fmt1(recoveryVal)}</span>
                    </>
                  )}
                  <span className="course-rhs__eqd2-unit">Gy</span>
                </>
              ) : '—'}
            </div>
            {tissueRecoveryActive && (
              <div className="course-rhs__slider-row">
                <RecoverySlider
                  recoveryPct={recoveryPct}
                  onChange={pct => onUpdateImmediate({ ...course, recoveryPct: pct })}
                  onCommit={prePct => onCommitRecovery(course.id, prePct)}
                  label={`Tissue recovery for ${course.title || placeholder}`}
                />
                <RecoveryNumInput
                  recoveryPct={recoveryPct}
                  onChange={pct => onUpdateImmediate({ ...course, recoveryPct: pct })}
                  onCommit={prePct => onCommitRecovery(course.id, prePct)}
                />
              </div>
            )}
          </div>
        )}
      </div>
    </section>
  );
}

// =============================================================================
// Cumulative output block — sums across all valid courses
// =============================================================================
function CumulativeOutputs({ courses, customAb, refDose, onUpdateAb, onUpdateRef, hasAny, tissueRecoveryActive }) {
  const rows = useMemo(
    () => cumulativeRows(courses, customAb, refDose, tissueRecoveryActive),
    [courses, customAb, refDose, tissueRecoveryActive]
  );

  const validCount = courses.filter(c => E.isPositive(c.D) && E.isPositive(c.n)).length;

  return (
    <div className={`cum${tissueRecoveryActive ? ' cum--recovery' : ''}`} aria-live="polite">
      <div className="cum__head">
        <span className="cum__title">
          {validCount > 1 ? `Cumulative (${validCount} courses)` : 'Biological dose'}
        </span>
      </div>
      <div className="cum__table">
        <div className="cum__colhead">
          <span className="cum__colhead-cell cum__colhead-cell--spacer" />
          <span className="cum__colhead-cell">BED</span>
          <span className="cum__colhead-cell">EQD<sub>{refDose === 2 ? '2' : E.fmt1(refDose)}</sub></span>
          {tissueRecoveryActive && (
            <span className="cum__colhead-cell cum__colhead-cell--recovery">
              Recovery<br />EQD<sub style={{ fontSize: '0.75em' }}>2</sub>
            </span>
          )}
        </div>
        <div className="cum__rows">
          {rows.map(r => (
            <div key={r.key} className="out-row">
              <div className="out-row__label">
                {r.editable ? (
                  <>α/β = <InlineNumber value={r.ab} onCommit={onUpdateAb} ariaLabel="Custom alpha/beta ratio" /></>
                ) : (
                  <>α/β = {r.ab}</>
                )}
              </div>
              <div className="out-row__val out-row__val--bed">
                <ValueNumber value={hasAny && r.bed !== null ? E.fmt1(r.bed) : null} />
              </div>
              <div className="out-row__val out-row__val--eqd">
                <ValueNumber value={hasAny && r.eqd !== null ? E.fmt1(r.eqd) : null} />
              </div>
              {tissueRecoveryActive && (
                <div className="out-row__val out-row__val--recovery">
                  <ValueNumber value={hasAny && r.eqdRecovery !== null ? E.fmt1(r.eqdRecovery) : null} recovery />
                </div>
              )}
            </div>
          ))}
        </div>
      </div>
      <div className="cum__meta">
        <div className="ref-dose-inline">
          <span className="ref-dose-inline__label">ref dose</span>
          <InlineNumber value={refDose} onCommit={onUpdateRef} ariaLabel="Reference dose per fraction" />
          <span className="ref-dose-inline__unit">Gy</span>
        </div>
      </div>
    </div>
  );
}

function ValueNumber({ value, recovery }) {
  const [display, setDisplay] = useState(value);
  const [op, setOp] = useState(1);
  useEffect(() => {
    if (value === display) return;
    setOp(0.35);
    const t1 = setTimeout(() => { setDisplay(value); setOp(1); }, 80);
    return () => clearTimeout(t1);
  }, [value]);
  if (display === null || display === undefined) {
    return <span className="out-val out-val--empty" style={{ opacity: op, transition: 'opacity 150ms ease' }}>—</span>;
  }
  return (
    <span className={`out-val${recovery ? ' out-val--recovery' : ''}`} style={{ opacity: op, transition: 'opacity 150ms ease' }}>
      <span className="out-val__num">{display}</span>
      <span className="out-val__unit">Gy</span>
    </span>
  );
}

// =============================================================================
// Summary bar
// =============================================================================
function SummaryBar({ text }) {
  const [copied, setCopied] = useState(false);
  if (!text) return null;

  const blocks = text.split('\n\n');

  async function copy() {
    const clipboardText = `${text}\n\nCalculated on eqd2calculator.com`;
    try { await navigator.clipboard.writeText(clipboardText); } catch {}
    setCopied(true);
    setTimeout(() => setCopied(false), 1000);
  }

  return (
    <div className="summary summary--inline">
      <div className="summary__inner">
        <div className="summary__text">
          {blocks.map((block, bi) => (
            <div key={bi} className="summary__block">
              {block.split('\n').map((line, li) => (
                <p key={li} className="summary__line">{line}</p>
              ))}
            </div>
          ))}
        </div>
        <button
          className={`summary__copy ${copied ? 'summary__copy--ok' : ''}`}
          type="button"
          onClick={copy}
          aria-label="Copy summary to clipboard"
          title="Copy summary"
        >
          {copied ? <CheckIcon /> : <CopyIcon />}
        </button>
      </div>
    </div>
  );
}

// =============================================================================
// Session banner
// =============================================================================
function SessionBanner({ lastEditedAt, onNewSession }) {
  if (!lastEditedAt) return null;
  const rt = relativeTime(lastEditedAt);
  if (!rt) return null;
  const stale = rt.hours > 4;
  return (
    <div className={`session-banner ${stale ? 'session-banner--stale' : ''}`}>
      <span>
        {stale
          ? <>Session from {rt.text}. Still the same patient? </>
          : <>Session resumed {rt.text === 'just now' ? '' : `(${rt.text})`} · </>
        }
      </span>
      <button className="session-banner__action" type="button" onClick={onNewSession}>New session</button>
    </div>
  );
}

// =============================================================================
// Tweaks panel
// =============================================================================
function TweaksPanel({ tweaks, setTweak, visible, onClose }) {
  if (!visible) return null;
  const Field = ({ label, keyName, options }) => (
    <div className="tweak-row">
      <div className="tweak-row__label">{label}</div>
      <div className="tweak-row__options">
        {options.map(opt => (
          <button
            key={opt.value}
            type="button"
            className={`tweak-opt ${tweaks[keyName] === opt.value ? 'tweak-opt--on' : ''}`}
            onClick={() => setTweak(keyName, opt.value)}
          >
            {opt.swatch && <span className="tweak-opt__swatch" style={{ background: opt.swatch }} />}
            {opt.label}
          </button>
        ))}
      </div>
    </div>
  );
  return (
    <div className="tweaks">
      <div className="tweaks__head">
        <span className="tweaks__title">Tweaks</span>
        <button className="tweaks__close" onClick={onClose} aria-label="Close tweaks"><CloseIcon /></button>
      </div>
      <Field label="Accent" keyName="accent" options={[
        { value: 'teal', label: 'Teal', swatch: 'oklch(0.55 0.08 200)' },
        { value: 'ink',  label: 'Ink',  swatch: 'oklch(0.45 0.08 260)' },
        { value: 'moss', label: 'Moss', swatch: 'oklch(0.50 0.06 145)' },
        { value: 'none', label: 'Gray', swatch: '#8A8A86' },
      ]} />
      <Field label="Background" keyName="background" options={[
        { value: 'paper', label: 'Warm paper', swatch: '#FAFAF7' },
        { value: 'cool',  label: 'Cool gray',  swatch: '#F6F7F8' },
        { value: 'white', label: 'Pure white', swatch: '#FFFFFF' },
      ]} />
      <Field label="Type" keyName="typePair" options={[
        { value: 'plex',   label: 'IBM Plex' },
        { value: 'inter',  label: 'Inter + JetBrains' },
        { value: 'system', label: 'System' },
      ]} />
      <Field label="Density" keyName="density" options={[
        { value: 'comfortable', label: 'Comfortable' },
        { value: 'compact',     label: 'Compact' },
      ]} />
    </div>
  );
}

// =============================================================================
// App
// =============================================================================
// =============================================================================
// AttestationModal — blocking first-use gate (§1.8) and read-only review
// =============================================================================
function AttestationModal({ readOnly, onAgree, onClose }) {
  const modalRef  = useRef(null);
  const headingRef = useRef(null);

  useEffect(() => {
    headingRef.current?.focus();

    function onKeyDown(e) {
      if (e.key === 'Escape') {
        if (readOnly) onClose?.();
        return;
      }
      if (e.key !== 'Tab') return;
      const el = modalRef.current;
      if (!el) return;
      const focusable = Array.from(
        el.querySelectorAll('a[href], button:not([disabled])')
      );
      if (!focusable.length) return;
      const first = focusable[0];
      const last  = focusable[focusable.length - 1];
      if (e.shiftKey) {
        if (document.activeElement === first) { e.preventDefault(); last.focus(); }
      } else {
        if (document.activeElement === last)  { e.preventDefault(); first.focus(); }
      }
    }

    document.addEventListener('keydown', onKeyDown);
    return () => document.removeEventListener('keydown', onKeyDown);
  }, [readOnly, onClose]);

  function handleAgree() {
    try {
      localStorage.setItem(ATTESTATION_KEY, JSON.stringify({
        version: CURRENT_ATTESTATION_VERSION,
        agreedAt: new Date().toISOString(),
      }));
    } catch {}
    onAgree();
  }

  return (
    <div className="attest-overlay" onClick={readOnly ? onClose : undefined}>
      <div
        role="dialog"
        aria-modal="true"
        aria-labelledby="attest-heading"
        aria-describedby="attest-intro"
        className="attest-modal"
        ref={modalRef}
        onClick={e => e.stopPropagation()}
      >
        <h2 id="attest-heading" ref={headingRef} tabIndex={-1} className="attest-heading">
          Before using EQD2 Calculator
        </h2>
        <p id="attest-intro" className="attest-intro">
          EQD2 Calculator is an informational tool. It is not a medical device and is not a
          validated clinical decision support system. By clicking "I Agree and Proceed" below,
          you confirm ALL of the following:
        </p>
        <ol className="attest-list">
          <li>
            I am a licensed healthcare professional (radiation oncologist, medical physicist,
            radiation therapist, or equivalent) or an authorized healthcare researcher operating
            under institutional oversight, and I am at least 18 years old.{' '}
            <a href="/terms.html#s-1-3" target="_blank" rel="noopener">(see §1.3)</a>
          </li>
          <li>
            I will not enter patient-identifiable information into any field of this tool,
            including course title fields.{' '}
            <a href="/terms.html#s-b-3" target="_blank" rel="noopener">(see Privacy Policy §3)</a>
          </li>
          <li>
            I understand that this tool is not a validated clinical treatment planning system
            and is not intended for clinical application.{' '}
            <a href="/terms.html#s-1-2" target="_blank" rel="noopener">(see §1.2)</a>
          </li>
          <li>
            I will independently verify every output — by hand-calculation, a validated
            treatment planning system, secondary verification software, or peer review —
            before relying on it for any clinical, treatment-planning, dosimetric, or
            patient-care purpose.{' '}
            <a href="/terms.html#s-1-5" target="_blank" rel="noopener">(see §1.5)</a>
          </li>
        </ol>
        <p className="attest-secondary">
          By proceeding, you accept the full{' '}
          <a href="/terms.html" target="_blank" rel="noopener">Terms of Service and Privacy Policy</a>.
          {' '}Your saved courses and preferences are stored only in your browser and are not
          transmitted to us.
        </p>
        <div className="attest-actions">
          {readOnly ? (
            <button type="button" className="attest-btn attest-btn--close" onClick={onClose}>
              Close
            </button>
          ) : (
            <button type="button" className="attest-btn attest-btn--agree" onClick={handleAgree}>
              I Agree and Proceed
            </button>
          )}
        </div>
        <p className="attest-modal-footer">
          <a href="/terms.html" target="_blank" rel="noopener">Read the full Terms &amp; Privacy →</a>
        </p>
      </div>
    </div>
  );
}

// =============================================================================
// SiteFooter — static site footer with attestation management links
// =============================================================================
function SiteFooter({ onReview }) {
  function resetAttestation() {
    if (window.confirm('Reset attestation and require re-confirmation on next load?')) {
      try { localStorage.removeItem(ATTESTATION_KEY); } catch {}
      window.location.reload();
    }
  }

  return (
    <footer className="site-footer">
      <p>EQD2 Calculator — informational and educational use only.</p>
      <p>Not a medical device. Not a treatment planning system. All outputs must be independently verified before any clinical reliance.</p>
      <div className="site-footer__links">
        <span>© 2026 eqd2calculator.com</span>
        <span className="site-footer__sep" aria-hidden="true">·</span>
        <a href="/terms.html">Terms &amp; Privacy</a>
        <span className="site-footer__sep" aria-hidden="true">·</span>
        <a href="/about">About</a>
        <span className="site-footer__sep" aria-hidden="true">·</span>
        <a href="/faq">FAQ</a>
        <span className="site-footer__sep" aria-hidden="true">·</span>
        <a href="mailto:feedback@eqd2calculator.com">Contact (&amp; Feedback!)</a>
      </div>
      <div className="site-footer__links">
        <button type="button" className="site-footer__attest-btn" onClick={onReview}>
          Review attestation
        </button>
        <span className="site-footer__sep" aria-hidden="true">·</span>
        <button type="button" className="site-footer__attest-btn" onClick={resetAttestation}>
          Reset attestation
        </button>
      </div>
    </footer>
  );
}

// =============================================================================
function App() {
  // Tweaks
  const [tweaks, setTweaksState] = useState(TWEAK_DEFAULTS);
  const [tweaksOpen, setTweaksOpen] = useState(false);
  const [tweaksEnabled, setTweaksEnabled] = useState(false);

  const setTweak = (k, v) => {
    const next = { ...tweaks, [k]: v };
    setTweaksState(next);
    try { window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [k]: v } }, '*'); } catch {}
  };

  useEffect(() => {
    const onMsg = (e) => {
      // Only accept messages from the direct parent frame (edit-mode host).
      if (e.source !== window.parent || window.parent === window) return;
      const d = e && e.data;
      if (!d || typeof d !== 'object') return;
      if (d.type === '__activate_edit_mode') { setTweaksEnabled(true); setTweaksOpen(true); }
      if (d.type === '__deactivate_edit_mode') { setTweaksEnabled(false); setTweaksOpen(false); }
    };
    window.addEventListener('message', onMsg);
    try { window.parent.postMessage({ type: '__edit_mode_available' }, '*'); } catch {}
    return () => window.removeEventListener('message', onMsg);
  }, []);

  useEffect(() => {
    const r = document.documentElement;
    r.setAttribute('data-accent', tweaks.accent);
    r.setAttribute('data-bg', tweaks.background);
    r.setAttribute('data-typepair', tweaks.typePair);
    r.setAttribute('data-density', tweaks.density);
  }, [tweaks]);

  // Attestation gate
  const shellRef = useRef(null);
  const [attestationDone, setAttestationDone] = useState(() => {
    try {
      const raw = localStorage.getItem(ATTESTATION_KEY);
      if (!raw) return false;
      const p = JSON.parse(raw);
      return !!(p && p.version === CURRENT_ATTESTATION_VERSION);
    } catch { return false; }
  });
  const [reviewMode, setReviewMode] = useState(false);

  useLayoutEffect(() => {
    const el = shellRef.current;
    if (!el) return;
    if (!attestationDone) {
      el.setAttribute('inert', '');
      el.setAttribute('aria-hidden', 'true');
    } else {
      el.removeAttribute('inert');
      el.removeAttribute('aria-hidden');
    }
  }, [attestationDone]);

  // Main state
  const [courses, setCourses] = useState(() => [newCourse()]);
  const [customAb, setCustomAb] = useState(2);
  const [refDose, setRefDose] = useState(2);
  const [tissueRecoveryActive, setTissueRecoveryActive] = useState(false);
  const [resumedAt, setResumedAt] = useState(null);
  const [bannerDismissed, setBannerDismissed] = useState(false);
  const [hydrated, setHydrated] = useState(false);
  const [autoFocusCourseId, setAutoFocusCourseId] = useState(() => courses[0]?.id ?? null);
  const [resetToken, setResetToken] = useState(0); // bump to force input resync (e.g. undo)

  // Undo/redo
  const undoStack = useRef([]);
  const redoStack = useRef([]);
  const STACK_MAX = 50;

  // Stable refs for keyboard handler — updated synchronously each render.
  const keyStateRef = useRef({ courses, customAb, refDose, tissueRecoveryActive });
  const keyHandlersRef = useRef({});

  // Hydrate once.
  useEffect(() => {
    const s = loadState();
    if (s && Array.isArray(s.courses) && s.courses.length > 0) {
      // Drop fully-empty courses (no data + no title) on restore.
      const filtered = s.courses.filter(c =>
        E.isPositive(c.D) || E.isPositive(c.n) || E.isPositive(c.d) || (c.title && c.title.trim())
      );
      const normalized = filtered.map(c => ({
        ...c,
        recoveryPct: typeof c.recoveryPct === 'number' ? Math.max(0, Math.min(50, Math.round(c.recoveryPct))) : 0,
      }));
      setCourses(normalized.length ? normalized : [newCourse()]);
      setResumedAt(s.lastEditedAt || null);
      // customAbs (array) is the pre-v1 shape; customAb (scalar) is current.
      if (Array.isArray(s.customAbs) && s.customAbs.length) setCustomAb(s.customAbs[0]);
      else if (typeof s.customAb === 'number') setCustomAb(s.customAb);
      if (typeof s.refDose === 'number') setRefDose(s.refDose);
      if (typeof s.tissueRecoveryActive === 'boolean') setTissueRecoveryActive(s.tissueRecoveryActive);
    }
    setHydrated(true);
  }, []);

  // Debounced persistence
  useEffect(() => {
    if (!hydrated) return;
    const t = setTimeout(() => {
      saveState({ courses, customAb, refDose, tissueRecoveryActive, lastEditedAt: new Date().toISOString() });
    }, 300);
    return () => clearTimeout(t);
  }, [courses, customAb, refDose, tissueRecoveryActive, hydrated]);

  // Commit with undo snapshot.
  const commit = useCallback((producer) => {
    setCourses(prev => {
      const next = typeof producer === 'function' ? producer(prev) : producer;
      if (JSON.stringify(next) === JSON.stringify(prev)) return prev;
      undoStack.current.push({ courses: prev, customAb, refDose, tissueRecoveryActive });
      if (undoStack.current.length > STACK_MAX) undoStack.current.shift();
      redoStack.current = [];
      return next;
    });
    setBannerDismissed(true);
  }, [customAb, refDose, tissueRecoveryActive]);

  function updateCourse(updated) {
    commit(prev => prev.map(c => (c.id === updated.id ? updated : c)));
  }

  // Live update without undo — used during slider drags and numeric typing.
  function updateCourseImmediate(updated) {
    setCourses(prev => prev.map(c => (c.id === updated.id ? updated : c)));
  }

  // Push ONE undo entry for a completed recovery-pct gesture (drag or typed value).
  // preDragPct is the value the course had BEFORE the gesture started.
  const commitRecoveryPct = useCallback((courseId, preDragPct) => {
    undoStack.current.push({
      courses: courses.map(c => c.id === courseId ? { ...c, recoveryPct: preDragPct } : c),
      customAb,
      refDose,
      tissueRecoveryActive,
    });
    redoStack.current = [];
  }, [courses, customAb, refDose, tissueRecoveryActive]);

  function addCourse(seedText) {
    const nc = newCourse();
    commit(prev => [...prev, nc]);
    setAutoFocusCourseId(nc.id);
    setInitialDoseText(seedText != null ? String(seedText) : null);
  }

  function removeCourse(id) {
    commit(prev => (prev.length <= 1 ? prev : prev.filter(c => c.id !== id)));
  }

  // Scroll new course into view only when it would otherwise land off-screen.
  const isInitialFocus = useRef(true);
  useEffect(() => {
    if (isInitialFocus.current) { isInitialFocus.current = false; return; }
    if (!autoFocusCourseId) return;
    const timer = setTimeout(() => {
      const el = document.querySelector(`[data-course-id="${autoFocusCourseId}"]`);
      if (!el) return;
      const rect = el.getBoundingClientRect();
      const vh = window.innerHeight;
      // Only scroll if the new course's bottom is below 70% of the viewport.
      if (rect.bottom > vh * 0.7) {
        // Position the new course's top at ~25% from the top, leaving ~75% to
        // show its content and a peek of the cumulative table below.
        const targetY = window.scrollY + rect.top - vh * 0.25;
        window.scrollTo({ top: Math.max(0, targetY), behavior: 'smooth' });
      }
    }, 60);
    return () => clearTimeout(timer);
  }, [autoFocusCourseId]);

  function clearAll() {
    const nc = newCourse();
    undoStack.current.push({ courses, customAb, refDose, tissueRecoveryActive });
    setCourses([nc]);
    setCustomAb(2);
    setRefDose(2);
    setTissueRecoveryActive(false);
    setResumedAt(null);
    setAutoFocusCourseId(nc.id);
    setResetToken(t => t + 1);
    try { localStorage.removeItem(STORAGE_KEY); } catch {}
  }

  function newSession() {
    const nc = newCourse();
    undoStack.current.push({ courses, customAb, refDose, tissueRecoveryActive });
    setCourses([nc]);
    setTissueRecoveryActive(false);
    setResumedAt(null);
    setAutoFocusCourseId(nc.id);
    setResetToken(t => t + 1);
    try { localStorage.removeItem(STORAGE_KEY); } catch {}
  }

  function activateRecovery() {
    undoStack.current.push({ courses, customAb, refDose, tissueRecoveryActive });
    redoStack.current = [];
    setCourses(prev => prev.map(c => ({ ...c, recoveryPct: 0 })));
    setTissueRecoveryActive(true);
  }

  function deactivateRecovery() {
    undoStack.current.push({ courses, customAb, refDose, tissueRecoveryActive });
    redoStack.current = [];
    setCourses(prev => prev.map(c => ({ ...c, recoveryPct: 0 })));
    setTissueRecoveryActive(false);
  }

  function toggleRecovery() {
    if (tissueRecoveryActive) deactivateRecovery();
    else activateRecovery();
  }

  // Keep key-handler refs current on every render so the listener (registered
  // once below) always reads fresh state without re-registering.
  keyStateRef.current = { courses, customAb, refDose, tissueRecoveryActive };
  keyHandlersRef.current.newSession = newSession;
  keyHandlersRef.current.toggleRecovery = toggleRecovery;

  // Cmd/Ctrl+Z / Shift+Z — registered once, reads state via keyStateRef.
  useEffect(() => {
    function onKey(e) {
      const mod = e.metaKey || e.ctrlKey;
      if (!mod) return;
      const { courses, customAb, refDose, tissueRecoveryActive } = keyStateRef.current;
      if (e.shiftKey && e.key.toLowerCase() === 'n') {
        e.preventDefault();
        keyHandlersRef.current.newSession();
        return;
      }
      if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'n') {
        e.preventDefault();
        keyHandlersRef.current.newSession();
        return;
      }
      if (e.shiftKey && e.key.toLowerCase() === 'f') {
        e.preventDefault();
        keyHandlersRef.current.toggleRecovery();
        return;
      }
      if (e.key.toLowerCase() !== 'z') return;
      if (e.shiftKey) {
        if (redoStack.current.length) {
          e.preventDefault();
          const next = redoStack.current.pop();
          undoStack.current.push({ courses, customAb, refDose, tissueRecoveryActive });
          setCourses(next.courses);
          setCustomAb(typeof next.customAb === 'number' ? next.customAb : 2);
          setRefDose(next.refDose ?? 2);
          setTissueRecoveryActive(typeof next.tissueRecoveryActive === 'boolean' ? next.tissueRecoveryActive : false);
          setResetToken(t => t + 1);
        }
      } else {
        if (undoStack.current.length) {
          e.preventDefault();
          const prev = undoStack.current.pop();
          redoStack.current.push({ courses, customAb, refDose, tissueRecoveryActive });
          setCourses(prev.courses);
          setCustomAb(typeof prev.customAb === 'number' ? prev.customAb : 2);
          setRefDose(prev.refDose ?? 2);
          setTissueRecoveryActive(typeof prev.tissueRecoveryActive === 'boolean' ? prev.tissueRecoveryActive : false);
          setResetToken(t => t + 1);
        }
      }
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  const summaryText = summarizeCourses(courses, refDose, tissueRecoveryActive, customAb);
  const [initialDoseText, setInitialDoseText] = useState(null);
  const hasAny = courses.some(c => E.isPositive(c.D) && E.isPositive(c.n));
  const showBanner = !bannerDismissed && resumedAt != null && hydrated;
  return (
    <>
    <div ref={shellRef}>
    {!_selfTestsPassed && <SelfTestBanner />}
    <div className="app">
      <header className="app__head">
        <div className="app__head-left">
          <span className="app__name">EQD2</span>
          <span className="app__sep">·</span>
          <span className="app__tag">radiation dose calculator</span>
        </div>
        <button type="button" className="clear-all" onClick={clearAll}>Clear all</button>
      </header>

      {showBanner && (
        <SessionBanner lastEditedAt={resumedAt} onNewSession={newSession} />
      )}

      <main className="courses">
        {courses.map((c, i) => (
          <CourseCard
            key={c.id}
            course={c}
            index={i}
            onUpdate={updateCourse}
            onUpdateImmediate={updateCourseImmediate}
            onCommitRecovery={commitRecoveryPct}
            onRemove={removeCourse}
            canRemove={courses.length > 1}
            autoFocus={c.id === autoFocusCourseId}
            initialDoseText={c.id === autoFocusCourseId ? initialDoseText : null}
            onDidConsumeInitial={() => setInitialDoseText(null)}
            showInlineEqd2={courses.length > 1}
            tissueRecoveryActive={tissueRecoveryActive}
            refDose={refDose}
            resetToken={resetToken}
          />
        ))}
      </main>

      <div className="course-actions">
      <button
        className="add-course"
        type="button"
        onClick={() => addCourse()}
        onKeyDown={e => {
          if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            addCourse();
            return;
          }
          const isDigit = /^[0-9]$/.test(e.key);
          if (isDigit && !e.metaKey && !e.ctrlKey && !e.altKey) {
            e.preventDefault();
            addCourse(e.key);
          }
        }}
      >
        <PlusIcon /> Add course
      </button>
      <button
        type="button"
        className="add-recovery"
        onClick={toggleRecovery}
      >
        {tissueRecoveryActive ? <MinusIcon /> : <PlusIcon />}
        {tissueRecoveryActive ? 'Remove Tissue Recovery' : 'Add Tissue Recovery'}
      </button>
      </div>

      <CumulativeOutputs
        courses={courses}
        customAb={customAb}
        refDose={refDose}
        tissueRecoveryActive={tissueRecoveryActive}
        onUpdateAb={v => {
          undoStack.current.push({ courses, customAb, refDose, tissueRecoveryActive });
          redoStack.current = [];
          setCustomAb(v);
        }}
        onUpdateRef={v => { undoStack.current.push({ courses, customAb, refDose, tissueRecoveryActive }); redoStack.current = []; setRefDose(v); }}
        hasAny={hasAny}
      />

      <SummaryBar text={summaryText} />

      {tweaksEnabled && (
        <TweaksPanel
          tweaks={tweaks}
          setTweak={setTweak}
          visible={tweaksOpen}
          onClose={() => setTweaksOpen(false)}
        />
      )}
      {tweaksEnabled && !tweaksOpen && (
        <button className="tweaks-fab" onClick={() => setTweaksOpen(true)}>Tweaks</button>
      )}
    </div>
    <SiteFooter onReview={() => setReviewMode(true)} />
    </div>
    {(!attestationDone || reviewMode) && (
      <AttestationModal
        readOnly={!!reviewMode}
        onAgree={() => setAttestationDone(true)}
        onClose={() => setReviewMode(false)}
      />
    )}
    </>
  );
}

const _selfTestsPassed = (function() { try { return E.runSelfTests(); } catch(e) { return false; } })();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
