const { useEffect, useRef, useState } = React;

const store = window.V2SessionStore;
const content = window.V2PrototypeContent;
const scenarioModel = window.V2ScenarioModel;
const { MascotPose, TargetBadge, VisualInstruction, PictureChoiceArt, EvidenceCounter, V2Pictograms, KioskIdleExperience } = window;
const LOGO_PATH = "assets/logo.png";

const chapters = [
  { label: "集合", phases: ["prep", "welcome"] },
  { label: "猜想", phases: ["predict"] },
  { label: "安全转向", phases: ["preflight", "slew"] },
  { label: "找证据", phases: ["observe", "compare"] },
  { label: "说发现", phases: ["conclude", "result", "complete"] },
];

const preflightLabels = {
  zoneClear: { title: "周围空", detail: "老师确认周围没有障碍", asset: V2Pictograms.object.dish },
  childrenBehindLine: { title: "线后站", detail: "孩子只观察，不操作设备", asset: V2Pictograms.mascot.safe },
  routeReviewed: { title: "路线清", detail: "老师已经说明模拟路径", asset: null },
  mockModeAcknowledged: { title: "只模拟", detail: "本页不连接真实设备", asset: V2Pictograms.object.signal },
};

function activeScenario(state) {
  return scenarioModel.getScenario(state.runScenarioId || state.config.scenarioId);
}

function childDispatch(state, type, payload) {
  store.dispatch({
    type,
    actor: "child",
    payload: {
      ...(payload || {}),
      expectedScenarioId: state.runScenarioId || state.config.scenarioId,
      expectedPhase: state.phase,
      expectedRunId: state.runId,
    },
  });
}

function useSessionState() {
  const [state, setState] = useState(() => store.getState());

  useEffect(() => store.subscribe(setState), []);
  return state;
}

function getChapterIndex(phase) {
  const index = chapters.findIndex((chapter) => chapter.phases.includes(phase));
  return index < 0 ? 0 : index;
}

function ChapterRibbon({ phase }) {
  const current = getChapterIndex(phase);
  return (
    <nav className="chapter-ribbon" aria-label="观测任务进度">
      {chapters.map((chapter, index) => {
        const stateClass = index < current ? "done" : index === current ? "current" : "";
        return (
          <span
            className={`chapter-node ${stateClass}`}
            aria-current={index === current ? "step" : undefined}
            key={chapter.label}
          >
            {chapter.label}
          </span>
        );
      })}
    </nav>
  );
}

function ChildTopbar({ state }) {
  const ageMode = content.ageModes[state.config.ageBand] || content.ageModes["6-8"];
  const scenario = activeScenario(state);
  const canPause = state.runState === "active" && !["prep", "result", "complete"].includes(state.phase);

  return (
    <header className="child-topbar">
      <div className="child-brand">
        <img src={LOGO_PATH} alt="宇宙来电小海龟" />
        <div className="child-brand-copy">
          <strong>宇宙来电</strong>
          <span>{ageMode.label}</span>
        </div>
      </div>
      <ChapterRibbon phase={state.phase} />
      <div className="child-top-actions">
        <TargetBadge scenario={scenario} compact />
        <span className="prototype-badge" data-tone="violet">教学模拟</span>
        <button
          className="child-pause-request"
          type="button"
          disabled={!canPause}
          onClick={() => childDispatch(state, "REQUEST_SAFE_PAUSE", { reason: "孩子请求先停一下" })}
        >
          需要停一下
        </button>
      </div>
    </header>
  );
}

function StoryCopy({ eyebrow, title, body }) {
  return (
    <div className="story-column">
      <div className="stage-eyebrow">{eyebrow}</div>
      <h1>{title}</h1>
      <p>{body}</p>
    </div>
  );
}

function PrepScene({ scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "prep");
  return (
    <section className="child-scene picture-story-scene" data-screen-label={`儿童屏·${scenario.shortLabel}待命`}>
      <VisualInstruction scenario={scenario} instruction={instruction} ageBand={ageBand} />
      <div className="picture-side-panel">
        <TargetBadge scenario={scenario} />
        <StoryCopy eyebrow="看图就知道下一步" title={phaseCopy.childTitle} body={phaseCopy.childBody} />
      </div>
    </section>
  );
}

function WelcomeScene({ scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "welcome");
  return (
    <section className="child-scene picture-story-scene" data-screen-label="儿童屏·看图组队">
      <VisualInstruction scenario={scenario} instruction={instruction} ageBand={ageBand} />
      <div className="role-constellation picture-role-grid" aria-label="观测小队角色">
        {scenario.roles.map((role) => (
          <article className="role-star" key={role.name}>
            <img src={role.asset} alt="" aria-hidden="true" />
            <div>
              <strong>{role.name}</strong>
              {ageBand === "9-12" ? <small>{role.detail}</small> : null}
            </div>
          </article>
        ))}
      </div>
    </section>
  );
}

function PredictScene({ scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "predict");
  return (
    <section className="child-scene picture-story-scene" data-screen-label={`儿童屏·${scenario.shortLabel}预测`}>
      <VisualInstruction scenario={scenario} instruction={instruction} ageBand={ageBand} />
      <div className="picture-side-panel prediction-picture-panel">
        <img src={scenario.targetAsset} alt="" aria-hidden="true" />
        <StoryCopy eyebrow="科学家会先猜一猜" title={phaseCopy.childTitle} body={phaseCopy.childBody} />
      </div>
    </section>
  );
}

function PreflightScene({ state, scenario, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "preflight");
  return (
    <section className="child-scene picture-story-scene" data-screen-label="儿童屏·老师安全预检">
      <VisualInstruction scenario={scenario} instruction={instruction} ageBand={ageBand} />
      <div className="preflight-stars" aria-label="老师预检进度，只读">
        {Object.entries(preflightLabels).map(([key, label]) => {
          const ready = Boolean(state.safety.preflight.checks[key]);
          const pictureAsset = key === "routeReviewed" ? scenario.targetAsset : label.asset;
          const displayTitle = key === "routeReviewed" && scenario.preflightRoute
            ? scenario.preflightRoute.childTitle
            : label.title;
          const displayDetail = key === "routeReviewed" && scenario.preflightRoute
            ? scenario.preflightRoute.childDetail
            : label.detail;
          return (
            <article className={`preflight-star ${ready ? "ready" : ""}`} key={key}>
              <img className="preflight-picture" src={pictureAsset} alt="" aria-hidden="true" />
              <div>
                <strong>{displayTitle}</strong>
                <small>{ready ? "老师已确认" : displayDetail}</small>
              </div>
              <span className="star-icon" aria-hidden="true">{ready ? "✓" : "·"}</span>
            </article>
          );
        })}
      </div>
    </section>
  );
}

function RfiSweepScene({ state, scenario, phaseCopy, ageBand }) {
  const progress = Math.round(state.movement.progress || 0);
  const arrived = Boolean(state.movement.arrived);
  const instruction = scenarioModel.instructionFor(scenario.id, "slew");
  return (
    <section className="child-scene picture-story-scene" data-screen-label="儿童屏·展开 L 波段宽频谱">
      <VisualInstruction scenario={scenario} instruction={instruction} ageBand={ageBand} />
      <div className="rfi-sweep-panel" style={{ "--scan-progress": `${progress}%` }} aria-label={`L 波段模拟宽扫进度 ${progress}%`}>
        <div className="rfi-sweep-head">
          <div>
            <span>L-BAND · TEACHING PANORAMA</span>
            <strong>{arrived ? "频谱展开啦" : phaseCopy.childTitle}</strong>
          </div>
          <img src={V2Pictograms.object.dish} alt="" aria-hidden="true" />
        </div>
        <div className="rfi-frequency-window" aria-hidden="true">
          <span className="rfi-noise-floor"></span>
          <span className="rfi-peak short"></span>
          <span className="rfi-peak unknown"></span>
          <span className="rfi-wide-block"></span>
          <span className="rfi-scan-beam"></span>
        </div>
        <div className="rfi-frequency-labels" aria-hidden="true">
          <span>1000</span><span>1250</span><span>1500</span><span>1750</span><span>2000 MHz</span>
        </div>
        <div className="motion-readout rfi-motion-readout">
          <div className="motion-track" aria-hidden="true"><span style={{ "--motion-width": `${progress}%` }}></span></div>
          <strong>{progress}%</strong>
        </div>
      </div>
    </section>
  );
}

function SlewScene({ state, scenario, phaseCopy, ageBand }) {
  if (scenario.kind === "rfi_scan") {
    return <RfiSweepScene state={state} scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />;
  }
  const progress = Math.round(state.movement.progress || 0);
  const arrived = Boolean(state.movement.arrived);
  const instruction = scenarioModel.instructionFor(scenario.id, "slew");
  const isSatellite = scenario.kind === "satellite_pass";
  return (
    <section className="child-scene picture-story-scene" data-screen-label={`儿童屏·模拟寻找${scenario.targetName}`}>
      <VisualInstruction scenario={scenario} instruction={instruction} ageBand={ageBand} />
      <div
        className={`sky-path ${isSatellite ? "satellite-path" : ""}`}
        style={{
          "--scope-left": `${18 + progress * 0.42}%`,
          "--scope-bottom": `${12 + progress * 0.25}%`,
          "--satellite-left": `${12 + progress * 0.62}%`,
          "--satellite-bottom": `${10 + progress * 0.38}%`,
        }}
        aria-label={`模拟寻找${scenario.targetName}进度 ${progress}%`}
      >
        {isSatellite ? (
          <React.Fragment>
            <img className="sky-satellite" src={scenario.targetAsset} alt="" aria-hidden="true" />
            <img className="sky-dish" src={V2Pictograms.object.dish} alt="" aria-hidden="true" />
          </React.Fragment>
        ) : <div className="sky-sun" aria-hidden="true" />}
        <div className="scope-marker" aria-hidden="true"></div>
        <div className="sky-phase-label">
          <strong>{arrived ? "到达啦" : phaseCopy.childTitle}</strong>
          <span>{arrived ? `等老师开始看${scenario.targetName}信号` : "只看屏幕，不碰设备"}</span>
        </div>
        <div className="motion-readout">
          <div className="motion-track" aria-hidden="true">
            <span style={{ "--motion-width": `${progress}%` }}></span>
          </div>
          <strong>{progress}%</strong>
        </div>
      </div>
    </section>
  );
}

function setupCanvas(canvas, draw, animate = true) {
  if (!canvas) return () => {};
  let frame = 0;
  let width = 0;
  let height = 0;
  let stopped = false;
  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  const resize = () => {
    const rect = canvas.getBoundingClientRect();
    width = Math.max(1, rect.width);
    height = Math.max(1, rect.height);
    const scale = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.round(width * scale);
    canvas.height = Math.round(height * scale);
    const context = canvas.getContext("2d");
    context.setTransform(scale, 0, 0, scale, 0, 0);
    draw(context, width, height, 0);
  };

  const loop = (time) => {
    if (stopped) return;
    const context = canvas.getContext("2d");
    draw(context, width, height, time);
    if (animate && !reducedMotion) frame = window.requestAnimationFrame(loop);
  };

  const observer = typeof ResizeObserver === "function" ? new ResizeObserver(resize) : null;
  if (observer) observer.observe(canvas);
  window.addEventListener("resize", resize);
  resize();
  if (animate && !reducedMotion) frame = window.requestAnimationFrame(loop);

  return () => {
    stopped = true;
    if (frame) window.cancelAnimationFrame(frame);
    if (observer) observer.disconnect();
    window.removeEventListener("resize", resize);
  };
}

function drawGrid(context, width, height, palette = {}) {
  const xAxis = palette.xAxis && typeof palette.xAxis === "object" ? palette.xAxis : { kind: "time", label: "时间" };
  const frequencyAxis = xAxis.kind === "frequency";
  const pad = { left: 68, right: 28, top: 30, bottom: frequencyAxis ? 58 : 48 };
  const plotWidth = Math.max(10, width - pad.left - pad.right);
  const plotHeight = Math.max(10, height - pad.top - pad.bottom);
  const lineColor = palette.line || "rgba(169, 183, 212, 0.16)";
  const textColor = palette.text || "#a9b7d4";
  const background = palette.background || "rgba(3, 11, 28, 1)";

  context.clearRect(0, 0, width, height);
  context.fillStyle = background;
  context.fillRect(0, 0, width, height);
  context.strokeStyle = lineColor;
  context.lineWidth = 1;
  context.font = "700 12px -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif";
  context.fillStyle = textColor;

  for (let row = 0; row <= 4; row += 1) {
    const y = pad.top + (plotHeight * row) / 4;
    context.beginPath();
    context.moveTo(pad.left, y);
    context.lineTo(width - pad.right, y);
    context.stroke();
    const value = 100 - row * 25;
    context.fillText(String(value), 27, y + 4);
  }
  const frequencyTicks = frequencyAxis && Array.isArray(xAxis.ticks) && Number.isFinite(Number(xAxis.min)) && Number.isFinite(Number(xAxis.max))
    ? xAxis.ticks.map((tick) => ({
      label: String(tick),
      t: (Number(tick) - Number(xAxis.min)) / Math.max(1, Number(xAxis.max) - Number(xAxis.min)),
    })).filter((tick) => tick.t >= 0 && tick.t <= 1)
    : [];
  const columns = frequencyTicks.length
    ? frequencyTicks
    : Array.from({ length: 7 }, (_, index) => ({ label: null, t: index / 6 }));
  columns.forEach((column) => {
    const x = pad.left + plotWidth * column.t;
    context.beginPath();
    context.moveTo(x, pad.top);
    context.lineTo(x, height - pad.bottom);
    context.stroke();
    if (column.label) {
      context.textAlign = "center";
      context.fillText(column.label, x, height - 19);
    }
  });
  context.textAlign = "right";
  if (frequencyAxis) {
    context.fillText(`${xAxis.label || "频率"} / ${xAxis.unit || "MHz"}`, width - pad.right, 20);
  } else {
    context.fillText(`${xAxis.label || "时间"} →`, width - pad.right, height - 17);
  }
  context.textAlign = "left";
  return { ...pad, plotWidth, plotHeight };
}

function drawScenarioSeries(context, plot, series, color, fillColor) {
  const points = series.map((point) => [
    plot.left + point.t * plot.plotWidth,
    plot.top + (1 - point.value / 100) * plot.plotHeight,
  ]);
  if (fillColor) {
    const gradient = context.createLinearGradient(0, plot.top, 0, plot.top + plot.plotHeight);
    gradient.addColorStop(0, fillColor);
    gradient.addColorStop(1, "rgba(41, 216, 242, 0.01)");
    context.beginPath();
    points.forEach(([x, y], index) => index === 0 ? context.moveTo(x, y) : context.lineTo(x, y));
    context.lineTo(plot.left + plot.plotWidth, plot.top + plot.plotHeight);
    context.lineTo(plot.left, plot.top + plot.plotHeight);
    context.closePath();
    context.fillStyle = gradient;
    context.fill();
  }
  context.beginPath();
  points.forEach(([x, y], index) => index === 0 ? context.moveTo(x, y) : context.lineTo(x, y));
  context.strokeStyle = color;
  context.lineWidth = 4;
  context.lineJoin = "round";
  context.lineCap = "round";
  context.stroke();
}

function SignalCanvas({ scenario, active, observation }) {
  const canvasRef = useRef(null);

  useEffect(() => setupCanvas(canvasRef.current, (context, width, height) => {
    const plot = drawGrid(context, width, height, { xAxis: scenario.observe.xAxis });
    const color = scenario.kind === "satellite_pass" ? "#6ee8ef" : scenario.kind === "rfi_scan" ? "#ff9a7c" : "#ffc857";
    const fillColor = scenario.kind === "satellite_pass"
      ? "rgba(41, 216, 242, 0.32)"
      : scenario.kind === "rfi_scan"
        ? "rgba(255, 124, 124, 0.3)"
        : "rgba(255, 200, 87, 0.28)";
    drawScenarioSeries(context, plot, scenario.observe.points, color, fillColor);
    const startedAt = Number(observation && observation.startedAt);
    const durationMs = Number(observation && observation.durationMs) || scenario.observe.durationMs;
    const elapsedRatio = startedAt > 0 ? Math.max(0, (Date.now() - startedAt) / durationMs) : 0;
    const playhead = startedAt > 0
      ? scenario.kind === "rfi_scan" ? elapsedRatio % 1 : Math.max(0, Math.min(1, elapsedRatio))
      : 0;
    const value = scenarioModel.sampleSeries(scenario.observe.points, playhead);
    const x = plot.left + playhead * plot.plotWidth;
    const y = plot.top + (1 - value / 100) * plot.plotHeight;
    context.strokeStyle = "rgba(255,255,255,0.62)";
    context.lineWidth = 2;
    context.beginPath();
    context.moveTo(x, plot.top);
    context.lineTo(x, plot.top + plot.plotHeight);
    context.stroke();
    context.fillStyle = color;
    context.beginPath();
    context.arc(x, y, 7, 0, Math.PI * 2);
    context.fill();
  }, active), [active, scenario.id, observation && observation.startedAt, observation && observation.durationMs]);

  return (
    <canvas
      ref={canvasRef}
      role="img"
      aria-label={`${scenario.observe.signalLabel}，纵轴固定为 0 到 100。${scenario.observe.disclaimer}`}
    />
  );
}

function ObserveScene({ state, scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "observe");
  const isRfi = scenario.kind === "rfi_scan";
  return (
    <section className="child-scene data-scene" data-screen-label={`儿童屏·${scenario.shortLabel}捕获证据`}>
      <div className="data-header">
        <div className="visual-data-cue">
          <MascotPose pose={instruction.pose} />
          <div>
            <div className="stage-eyebrow">{scenario.shortLabel} · 固定模拟 · 0–100</div>
            <strong>{instruction.verb}</strong>
            <small>{ageBand === "9-12" ? phaseCopy.childBody : scenario.observe.disclaimer}</small>
          </div>
        </div>
        <div>
          <TargetBadge scenario={scenario} />
          <EvidenceCounter count={state.responses.markers.length} />
        </div>
      </div>
      {isRfi ? (
        <div className="rfi-clue-deck">
          <div className="rfi-pattern-strip" aria-label="L 波段图形线索">
            {scenario.observe.patternCards.map((card) => (
              <article key={card.label}>
                <PictureChoiceArt kind={card.art} />
                <span><strong>{card.label}</strong><small>{card.detail}</small></span>
              </article>
            ))}
            <div className="rfi-boundary-stamps" aria-label="观察边界">
              <span>只看强弱</span><span>不听内容</span><span>来源待查</span>
            </div>
          </div>
          <div className="rfi-reference-strip" aria-label="候选频率位置参考">
            {scenario.observe.referenceBands.map((band) => (
              <span key={band.label}><strong>{band.range}</strong><small>{band.label}</small></span>
            ))}
          </div>
        </div>
      ) : null}
      <div className="signal-stage" data-scenario={scenario.kind}>
        <div className="signal-axis-label">{scenario.observe.axisLabel}</div>
        <SignalCanvas scenario={scenario} active={state.runState === "active"} observation={state.observation} />
        <div className="signal-markers" aria-label={`已经留下 ${state.responses.markers.length} 个证据标记`}>
          {state.responses.markers.map((marker) => {
            const sampleT = Number.isFinite(marker.sampleT) ? marker.sampleT : 0.5;
            const value = scenarioModel.sampleSeries(scenario.observe.points, sampleT);
            return (
              <span
                className="evidence-star"
                key={marker.id}
                aria-label={marker.label}
                style={{ "--marker-x": `${sampleT * 100}%`, "--marker-y": `${100 - value}%` }}
              >
                {isRfi && Number.isFinite(marker.sampleX) ? <b>{Math.round(marker.sampleX)} MHz</b> : null}
              </span>
            );
          })}
        </div>
      </div>
    </section>
  );
}

function StaticChart({ chart, scenario }) {
  const canvasRef = useRef(null);

  useEffect(() => setupCanvas(canvasRef.current, (context, width, height) => {
    const plot = drawGrid(context, width, height, {
      background: "#f6f2e8",
      line: "rgba(11, 20, 48, 0.11)",
      text: "#667186",
      xAxis: scenario.observe.xAxis,
    });
    const isTarget = chart.id === scenario.compare.target.id;
    const targetColor = scenario.kind === "rfi_scan" ? "#df725f" : "#e69328";
    drawScenarioSeries(context, plot, chart.points, isTarget ? targetColor : "#2b8193");
    const average = chart.points.reduce((sum, point) => sum + point.value, 0) / chart.points.length;
    const averageY = plot.top + (1 - average / 100) * plot.plotHeight;
    context.setLineDash([8, 8]);
    context.strokeStyle = isTarget
      ? scenario.kind === "rfi_scan" ? "rgba(223, 114, 95, 0.62)" : "rgba(230, 147, 40, 0.62)"
      : "rgba(43, 129, 147, 0.62)";
    context.lineWidth = 2;
    context.beginPath();
    context.moveTo(plot.left, averageY);
    context.lineTo(plot.left + plot.plotWidth, averageY);
    context.stroke();
    context.setLineDash([]);
  }, false), [scenario.id, chart.id]);

  return (
    <article className="comparison-chart">
      <header>
        <strong>{chart.label}</strong>
        <span>{chart.subtitle}</span>
      </header>
      <canvas
        ref={canvasRef}
        role="img"
        aria-label={`${chart.label}教学模拟曲线，和另一张图使用相同的 0 到 100 纵轴`}
      />
    </article>
  );
}

function CompareScene({ scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "compare");
  return (
    <section className="child-scene data-scene" data-screen-label={`儿童屏·比较${scenario.shortLabel}数据`}>
      <div className="data-header">
        <div className="visual-data-cue">
          <MascotPose pose={instruction.pose} />
          <div>
            <div className="stage-eyebrow">同一刻度 · 看图比较</div>
            <strong>{instruction.verb}</strong>
            <small>{ageBand === "9-12" ? phaseCopy.childBody : "先看图，再选答案"}</small>
          </div>
        </div>
        <TargetBadge scenario={scenario} />
      </div>
      <div className="comparison-grid">
        <StaticChart chart={scenario.compare.background} scenario={scenario} />
        <StaticChart chart={scenario.compare.target} scenario={scenario} />
      </div>
    </section>
  );
}

function ConcludeScene({ scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "conclude");
  return (
    <section className="child-scene picture-story-scene" data-screen-label="儿童屏·形成证据结论">
      <VisualInstruction scenario={scenario} instruction={instruction} ageBand={ageBand} />
      <div className="picture-side-panel">
        <img className="large-evidence-icon" src={V2Pictograms.object.evidence} alt="" aria-hidden="true" />
        <StoryCopy eyebrow="发现 + 证据" title={phaseCopy.childTitle} body={phaseCopy.childBody} />
      </div>
    </section>
  );
}

function labelFor(scenario, kind, ageBand, value) {
  if (!value) return "等待小队选择";
  return scenarioModel.getAnswerLabel(scenario.id, kind, ageBand, value) || value;
}

function ResultScene({ state, scenario, phaseCopy, ageBand }) {
  const isRfi = scenario.kind === "rfi_scan";
  return (
    <section className="child-scene result-scene" data-screen-label="儿童屏·团队发现卡">
      <div className="result-copy">
        <MascotPose pose="present" />
        <div className="stage-eyebrow">猜想 · 证据 · 发现</div>
        <h1>{phaseCopy.childTitle}</h1>
        {ageBand === "9-12" ? <p>{phaseCopy.childBody}</p> : null}
      </div>
      <article className="result-card" aria-label={`${scenario.result.cardTitle}发现卡`}>
        <header className="result-card-head">
          <div>
            <span>宇宙来电 · 团队发现记录</span>
            <strong>{scenario.result.cardTitle}</strong>
            <small>{scenario.result.sourceLabel}</small>
          </div>
          <img src={scenario.targetAsset} alt={`${scenario.targetName}任务图标`} />
        </header>
        {isRfi ? (
          <div className="result-evidence rfi-report-evidence">
            <div>
              <img src={V2Pictograms.object.signal} alt="" aria-hidden="true" />
              <span>固定模拟包含</span>
              <strong>局部频段增强和间歇窄峰</strong>
            </div>
            <div>
              <img src={V2Pictograms.object.evidence} alt="" aria-hidden="true" />
              <span>小队的结论</span>
              <strong>{labelFor(scenario, "conclusion", ageBand, state.responses.conclusion)} · {state.responses.markers.length} 个频率证据点</strong>
            </div>
            <div>
              <img src={V2Pictograms.mascot.present} alt="" aria-hidden="true" />
              <span>还不能说</span>
              <strong>一定来自某台手机，或知道通信内容</strong>
            </div>
          </div>
        ) : (
          <div className="result-evidence">
            <div>
              <img src={V2Pictograms.mascot.think} alt="" aria-hidden="true" />
              <span>先猜</span>
              <strong>{labelFor(scenario, "prediction", ageBand, state.responses.prediction)}</strong>
            </div>
            <div>
              <img src={V2Pictograms.object.evidence} alt="" aria-hidden="true" />
              <span>找证据</span>
              <strong>{state.responses.markers.length} 颗证据星 · {scenario.result.evidenceText}</strong>
            </div>
            <div>
              <img src={V2Pictograms.mascot.present} alt="" aria-hidden="true" />
              <span>说发现</span>
              <strong>{labelFor(scenario, "conclusion", ageBand, state.responses.conclusion)}</strong>
            </div>
          </div>
        )}
        <div className="role-seals" aria-label="小队完成的科学工作">
          <span>提出预测</span>
          <span>观察变化</span>
          <span>保存证据</span>
          <span>比较数据</span>
          <span>说明发现</span>
        </div>
      </article>
    </section>
  );
}

function CompleteScene({ scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "complete");
  return (
    <section className="child-scene picture-story-scene" data-screen-label="儿童屏·任务完成">
      <VisualInstruction scenario={scenario} instruction={instruction} ageBand={ageBand} />
      <div className="picture-side-panel">
        <TargetBadge scenario={scenario} />
        <StoryCopy eyebrow="小队安全结束" title={phaseCopy.childTitle} body={phaseCopy.childBody} />
      </div>
    </section>
  );
}

function ChildScene({ state }) {
  const scenario = activeScenario(state);
  const phaseCopy = scenarioModel.getPhaseCopy(scenario.id, state.phase);
  const ageBand = state.config.ageBand === "9-12" ? "9-12" : "6-8";
  const scenes = {
    prep: <PrepScene scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />,
    welcome: <WelcomeScene scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />,
    predict: <PredictScene scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />,
    preflight: <PreflightScene state={state} scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />,
    slew: <SlewScene state={state} scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />,
    observe: <ObserveScene state={state} scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />,
    compare: <CompareScene scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />,
    conclude: <ConcludeScene scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />,
    result: <ResultScene state={state} scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />,
    complete: <CompleteScene scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />,
  };
  return <React.Fragment key={state.phase}>{scenes[state.phase] || scenes.prep}</React.Fragment>;
}

function ChoiceDock({ state, options, selected, actionType }) {
  return (
    <div className="child-action-dock">
      <div className="choice-grid" style={{ "--choice-count": options.length }} role="group" aria-label="小队看图选择">
        {options.map((option) => (
          <button
            className={`child-choice picture-choice ${selected === option.value ? "selected" : ""}`}
            type="button"
            aria-pressed={selected === option.value}
            aria-label={`${option.label}。${option.help || ""}`}
            key={option.value}
            onClick={() => childDispatch(state, actionType, { value: option.value })}
          >
            <PictureChoiceArt kind={option.art} />
            <span className="choice-copy">
              <strong>{option.label}</strong>
              <small>{option.help}</small>
            </span>
            <span className="choice-check" aria-hidden="true">{selected === option.value ? "✓" : "·"}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

function WaitingDock({ title, detail }) {
  return (
    <div className="child-action-dock waiting" aria-live="polite">
      <div>
        <strong>{title}</strong>
        <span>{detail}</span>
      </div>
    </div>
  );
}

function ActionDock({ state }) {
  const ageBand = state.config.ageBand === "9-12" ? "9-12" : "6-8";
  const scenario = activeScenario(state);
  if (state.phase === "welcome") {
    return (
      <div className="child-action-dock">
        <button
          className={`child-primary ${state.responses.teamReady ? "done" : ""}`}
          type="button"
          disabled={state.responses.teamReady}
          onClick={() => childDispatch(state, "TEAM_READY")}
        >
          <img className="primary-action-icon" src={V2Pictograms.mascot.ready} alt="" aria-hidden="true" />
          <span>{state.responses.teamReady ? "准备好啦" : "我们准备好了"}</span>
        </button>
      </div>
    );
  }
  if (state.phase === "predict") {
    return <ChoiceDock state={state} options={scenarioModel.getAnswers(scenario.id, "prediction", ageBand)} selected={state.responses.prediction} actionType="SUBMIT_PREDICTION" />;
  }
  if (state.phase === "observe") {
    const count = state.responses.markers.length;
    const requiredMarkers = Math.max(1, Number(scenario.observe.minMarkers) || 1);
    return (
      <div className="child-action-dock">
        <button
          className={`child-primary ${count > 0 ? "done" : ""}`}
          type="button"
          disabled={count >= 3}
          onClick={() => {
            const elapsed = state.observation.startedAt ? Date.now() - state.observation.startedAt : 0;
            const duration = Number(state.observation.durationMs) || scenario.observe.durationMs;
            const rawSampleT = duration > 0 ? elapsed / duration : 0;
            childDispatch(state, "MARK_SIGNAL", {
              label: scenario.observe.markerLabel,
              sampleT: scenario.kind === "rfi_scan" ? rawSampleT % 1 : Math.max(0, Math.min(1, rawSampleT)),
            });
          }}
        >
          <img className="primary-action-icon" src={V2Pictograms.object.evidence} alt="" aria-hidden="true" />
          <span>
            {scenario.kind === "rfi_scan"
              ? count === 0 ? "标一处频率" : count < requiredMarkers ? "再找一种" : count < 3 ? "还可再标" : "证据收好啦"
              : count === 0 ? "发现变化" : count < 3 ? "再找一个" : "证据收好啦"}
          </span>
          <EvidenceCounter count={count} />
        </button>
      </div>
    );
  }
  if (state.phase === "compare") {
    return <ChoiceDock state={state} options={scenarioModel.getAnswers(scenario.id, "comparison", ageBand)} selected={state.responses.comparison} actionType="SUBMIT_COMPARISON" />;
  }
  if (state.phase === "conclude") {
    return <ChoiceDock state={state} options={scenarioModel.getAnswers(scenario.id, "conclusion", ageBand)} selected={state.responses.conclusion} actionType="SUBMIT_CONCLUSION" />;
  }

  const instruction = scenarioModel.instructionFor(scenario.id, state.phase);
  const waitingCopy = {
    prep: [instruction.verb, "老师准备好后带大家开始"],
    preflight: ["线后等", "这一步只由老师完成"],
    slew: [state.movement.arrived ? scenario.kind === "rfi_scan" ? "频谱展开啦" : "到达啦" : instruction.verb, "只看屏幕，不碰设备"],
    result: ["讲给同伴听", "老师审核后安全结束"],
    complete: ["完成啦", "谢谢每一位小小射电员"],
  };
  const copy = waitingCopy[state.phase] || ["等待老师继续", "小队可以先交换刚才看到的证据"];
  return <WaitingDock title={copy[0]} detail={copy[1]} />;
}

function SafetyOverlay({ state }) {
  if (!state.safety.estop.latched && state.runState !== "paused") return null;
  const estop = state.safety.estop.latched;
  return (
    <div className="paused-overlay" role="alert" aria-live="assertive">
      <article className="paused-card" data-estop={estop ? "true" : "false"}>
        <img src={V2Pictograms.mascot.safe} alt="小海龟站在安全线后" />
        <h2>{estop ? "安全停" : "先停一下"}</h2>
        <p>
          {estop
            ? "请大家留在安全线后，不靠近设备。老师检查完现场后会重新开始预检。"
            : "保持原来的位置，回想刚才看到了什么。只有老师完成安全检查后，任务才会继续。"}
        </p>
      </article>
    </div>
  );
}

function ChildApp() {
  const state = useSessionState();
  const scenario = activeScenario(state);
  const screenLabel = `儿童共享舞台 · ${scenario.shortLabel} · ${content.phases[state.phase].short}`;

  return (
    <KioskIdleExperience state={state}>
      <main
        className="child-shell picture-first"
        data-phase={state.phase}
        data-run-state={state.runState}
        data-screen-label={screenLabel}
        aria-label={screenLabel}
      >
        <ChildTopbar state={state} />
        <div className="child-stage">
          <ChildScene state={state} />
        </div>
        <ActionDock state={state} />
        <SafetyOverlay state={state} />
      </main>
    </KioskIdleExperience>
  );
}

ReactDOM.createRoot(document.getElementById("childRoot")).render(<ChildApp />);
