码桶

发现社区成员的开源项目

index.html53 KB

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="theme-color" content="#fff2f8">
  <title>萝莉气质研究所|隐藏属性测试</title>
  <style>
    :root {
      --paper: #fff2f8;
      --surface: #fffafd;
      --ink: #523c52;
      --muted: #8a7688;
      --line: #efc5db;
      --coral: #ff709f;
      --coral-dark: #c94070;
      --teal: #56a2ae;
      --yellow: #ffd56f;
      --blue: #8794cc;
      --plum: #a278af;
      --shadow: 0 18px 48px rgba(151, 78, 118, 0.16);
      --radius: 8px;
    }

    * { box-sizing: border-box; }

    html { scroll-behavior: smooth; }

    body {
      margin: 0;
      min-width: 320px;
      min-height: 100vh;
      background: var(--paper);
      color: var(--ink);
      font-family: "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", Arial, sans-serif;
      letter-spacing: 0;
    }

    button, input { font: inherit; }

    button { letter-spacing: 0; }

    button:focus-visible,
    summary:focus-visible {
      outline: 3px solid rgba(255, 112, 159, 0.3);
      outline-offset: 3px;
    }

    .app-shell {
      width: min(1180px, calc(100% - 32px));
      min-height: min(760px, calc(100vh - 48px));
      margin: 24px auto;
      display: grid;
      grid-template-columns: minmax(250px, 0.72fr) minmax(0, 1.55fr);
      overflow: hidden;
      border: 1px solid #e6a8c7;
      border-radius: var(--radius);
      background: var(--surface);
      box-shadow: var(--shadow);
    }

    .brand-panel {
      position: relative;
      display: flex;
      min-height: 100%;
      flex-direction: column;
      overflow: hidden;
      padding: 30px;
      border-right: 1px solid #efc5db;
      background: #fff0f7;
      color: var(--ink);
    }

    .brand-panel::after {
      position: absolute;
      right: -38px;
      bottom: -44px;
      width: 156px;
      height: 156px;
      border: 18px solid #f8c8de;
      content: "";
      transform: rotate(45deg);
      opacity: 0.72;
    }

    .brand-lockup {
      position: relative;
      z-index: 1;
    }

    .eyebrow {
      display: flex;
      align-items: center;
      gap: 9px;
      margin: 0 0 14px;
      color: #aa6c90;
      font-size: 12px;
      font-weight: 700;
      text-transform: uppercase;
    }

    .eyebrow::before {
      width: 24px;
      height: 3px;
      background: var(--yellow);
      content: "";
    }

    .brand-title {
      margin: 0;
      font-size: 31px;
      line-height: 1.22;
      font-weight: 900;
    }

    .brand-subtitle {
      max-width: 260px;
      margin: 12px 0 0;
      color: #876f82;
      font-size: 14px;
      line-height: 1.75;
    }

    .signal-board {
      position: relative;
      z-index: 1;
      margin: auto 0;
      padding: 21px 0;
    }

    .signal-meta {
      display: flex;
      justify-content: space-between;
      margin-bottom: 11px;
      color: #a87996;
      font-family: Consolas, monospace;
      font-size: 11px;
    }

    .signal-screen {
      position: relative;
      height: 190px;
      overflow: hidden;
      border: 1px solid #efb8d4;
      background: #fffafd;
    }

    .signal-screen::before,
    .signal-screen::after {
      position: absolute;
      content: "";
      background: rgba(255, 112, 159, 0.58);
    }

    .signal-screen::before {
      top: 50%;
      left: 12px;
      right: 12px;
      height: 1px;
    }

    .signal-screen::after {
      top: 12px;
      bottom: 12px;
      left: 50%;
      width: 1px;
    }

    .avatar-mark {
      position: absolute;
      z-index: 2;
      top: 50%;
      left: 50%;
      width: 112px;
      height: 124px;
      border: 3px solid #f18ab1;
      border-radius: 46% 46% 42% 42%;
      background: #efb078;
      transform: translate(-50%, -50%);
      box-shadow: 7px 7px 0 #c3e8ea;
    }

    .avatar-mark::before {
      position: absolute;
      top: 37px;
      left: 20px;
      width: 68px;
      height: 62px;
      border: 2px solid #c77b93;
      border-radius: 48% 48% 44% 44%;
      background: #fff1df;
      content: "";
    }

    .avatar-mark::after {
      position: absolute;
      top: 61px;
      left: 39px;
      width: 7px;
      height: 7px;
      border-radius: 50%;
      background: #7096cb;
      box-shadow: 27px 0 0 #7096cb;
      content: "";
    }

    .avatar-bow,
    .avatar-bow::before,
    .avatar-bow::after {
      position: absolute;
      z-index: 4;
      width: 22px;
      height: 18px;
      border: 2px solid #cf4d7f;
      border-radius: 10px 4px 10px 4px;
      background: #ff80aa;
      content: "";
    }

    .avatar-bow {
      top: 8px;
      left: 45px;
      width: 20px;
      height: 20px;
      border-radius: 50%;
    }

    .avatar-bow::before { top: -3px; left: -23px; transform: rotate(-22deg); }
    .avatar-bow::after { top: -3px; left: 19px; transform: rotate(22deg); }

    .scan-line {
      position: absolute;
      z-index: 3;
      top: 10px;
      right: 8px;
      left: 8px;
      height: 2px;
      background: var(--coral);
      box-shadow: 0 0 10px rgba(255, 112, 159, 0.45);
      animation: scan 3.2s ease-in-out infinite alternate;
    }

    .signal-label {
      position: absolute;
      z-index: 4;
      right: 9px;
      bottom: 8px;
      padding: 4px 6px;
      background: #dff5f4;
      color: #4b7c82;
      font-family: Consolas, monospace;
      font-size: 10px;
      font-weight: 800;
    }

    .lab-note {
      position: relative;
      z-index: 1;
      display: grid;
      grid-template-columns: 1fr auto;
      gap: 16px;
      align-items: end;
      padding-top: 18px;
      border-top: 1px solid #edb6d1;
      color: #98788c;
      font-size: 12px;
      line-height: 1.55;
    }

    .lab-note strong {
      color: #cc4a78;
      font-family: Consolas, monospace;
      font-size: 21px;
    }

    .main-panel {
      display: flex;
      min-width: 0;
      flex-direction: column;
      padding: 28px 34px 32px;
    }

    .topbar {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      gap: 18px;
      align-items: center;
      padding-bottom: 21px;
      border-bottom: 1px solid var(--line);
    }

    .progress-wrap {
      min-width: 0;
    }

    .progress-meta {
      display: flex;
      justify-content: space-between;
      gap: 12px;
      margin-bottom: 8px;
      color: var(--muted);
      font-size: 12px;
      font-weight: 700;
    }

    .progress-track {
      height: 7px;
      overflow: hidden;
      border: 1px solid #e9b4cf;
      background: #fde5ef;
    }

    .progress-fill {
      width: 8.333%;
      height: 100%;
      background: var(--coral);
      transition: width 360ms ease;
    }

    .icon-button {
      display: inline-grid;
      width: 38px;
      height: 38px;
      place-items: center;
      border: 1px solid #df9cbe;
      border-radius: 4px;
      background: transparent;
      color: var(--ink);
      cursor: pointer;
      font-size: 19px;
      transition: background 160ms ease, color 160ms ease, transform 160ms ease;
    }

    .icon-button:hover { background: var(--coral); border-color: var(--coral); color: #fff; }
    .icon-button:active { transform: translateY(1px); }

    .test-view {
      display: flex;
      min-height: 580px;
      flex: 1;
      flex-direction: column;
      padding-top: 38px;
    }

    .question-kicker {
      display: flex;
      align-items: center;
      gap: 9px;
      margin: 0 0 12px;
      color: #bb4a76;
      font-size: 12px;
      font-weight: 800;
    }

    .question-kicker::before {
      width: 9px;
      height: 9px;
      background: var(--coral);
      content: "";
    }

    .question-title {
      max-width: 720px;
      margin: 0;
      font-size: 25px;
      line-height: 1.48;
      font-weight: 900;
    }

    .question-title:focus,
    .result-title:focus { outline: none; }

    .question-context {
      margin: 10px 0 24px;
      color: var(--muted);
      font-size: 14px;
      line-height: 1.65;
    }

    .options {
      display: grid;
      gap: 10px;
    }

    .option-button {
      position: relative;
      display: grid;
      grid-template-columns: 34px minmax(0, 1fr) 22px;
      gap: 12px;
      align-items: center;
      width: 100%;
      min-height: 57px;
      padding: 10px 14px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: #fff;
      color: var(--ink);
      text-align: left;
      cursor: pointer;
      transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
    }

    .option-button:hover {
      border-color: #d977a5;
      background: #fff2f8;
      transform: translateX(3px);
    }

    .option-button[aria-checked="true"] {
      border-color: var(--coral);
      background: #ffe8f2;
      box-shadow: inset 4px 0 0 var(--coral);
    }

    .option-key {
      display: grid;
      width: 30px;
      height: 30px;
      place-items: center;
      border: 1px solid #e6b8ce;
      border-radius: 4px;
      color: #a76d8d;
      font-family: Consolas, monospace;
      font-size: 12px;
      font-weight: 800;
    }

    .option-button[aria-checked="true"] .option-key {
      border-color: var(--coral);
      background: var(--coral);
      color: #fff;
    }

    .option-text {
      min-width: 0;
      font-size: 15px;
      line-height: 1.45;
    }

    .option-check {
      color: transparent;
      font-size: 19px;
      font-weight: 900;
    }

    .option-button[aria-checked="true"] .option-check { color: var(--coral-dark); }

    .question-footer {
      display: flex;
      justify-content: space-between;
      gap: 16px;
      align-items: center;
      margin-top: auto;
      padding-top: 28px;
    }

    .hint {
      margin: 0;
      color: var(--muted);
      font-size: 12px;
    }

    .actions { display: flex; gap: 9px; }

    .button {
      display: inline-flex;
      min-height: 42px;
      align-items: center;
      justify-content: center;
      gap: 8px;
      padding: 9px 17px;
      border: 1px solid var(--ink);
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
      font-weight: 800;
      transition: background 160ms ease, color 160ms ease, transform 160ms ease;
    }

    .button:hover { transform: translateY(-1px); }
    .button:active { transform: translateY(1px); }

    .button-secondary { background: transparent; color: var(--ink); }
    .button-secondary:hover { background: #ece8df; }

    .button-primary { border-color: var(--coral); background: var(--coral); color: #fff; }
    .button-primary:hover { border-color: var(--teal); background: var(--teal); }

    .button:disabled {
      border-color: #c8c4bc;
      background: #dedbd4;
      color: #8d918f;
      cursor: not-allowed;
      transform: none;
    }

    .result-view { display: none; }

    .result-view.is-visible {
      display: block;
      animation: reveal 420ms ease both;
    }

    .result-header {
      display: grid;
      grid-template-columns: minmax(0, 1.1fr) minmax(230px, 0.9fr);
      gap: 34px;
      align-items: center;
      padding: 30px 0 28px;
      border-bottom: 1px solid var(--line);
    }

    .result-stamp {
      display: inline-flex;
      align-items: center;
      gap: 7px;
      margin-bottom: 13px;
      padding: 5px 8px;
      border: 1px solid #f18ab1;
      border-radius: 4px;
      color: #c94070;
      font-size: 11px;
      font-weight: 900;
    }

    .result-title {
      margin: 0;
      color: #b94173;
      font-size: 35px;
      line-height: 1.22;
      font-weight: 900;
    }

    .result-summary {
      margin: 15px 0 0;
      color: #4e5658;
      font-size: 15px;
      line-height: 1.85;
    }

    .trait-list {
      display: flex;
      flex-wrap: wrap;
      gap: 7px;
      margin: 19px 0 0;
      padding: 0;
      list-style: none;
    }

    .trait-list li {
      padding: 6px 9px;
      border: 1px solid var(--ink);
      border-radius: 4px;
      background: #ffe49a;
      font-size: 12px;
      font-weight: 800;
    }

    .radar-wrap {
      position: relative;
      width: min(100%, 290px);
      margin-inline: auto;
      aspect-ratio: 1;
    }

    #radarCanvas {
      width: 100%;
      height: 100%;
    }

    .result-grid {
      display: grid;
      grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
      gap: 30px;
      padding: 27px 0;
    }

    .section-title {
      margin: 0 0 17px;
      font-size: 16px;
      font-weight: 900;
    }

    .dimension-list { display: grid; gap: 14px; }

    .dimension-row {
      display: grid;
      grid-template-columns: 62px minmax(0, 1fr) 38px;
      gap: 10px;
      align-items: center;
      font-size: 12px;
      font-weight: 700;
    }

    .dimension-bar {
      height: 9px;
      border: 1px solid var(--ink);
      background: #e8e4dc;
    }

    .dimension-fill {
      height: 100%;
      background: var(--coral);
      transform-origin: left;
      animation: grow 700ms 160ms ease both;
    }

    .dimension-value {
      color: var(--muted);
      font-family: Consolas, monospace;
      text-align: right;
    }

    .insight-box {
      padding: 17px 18px;
      border-left: 5px solid var(--coral);
      background: #fff0f6;
    }

    .insight-box strong {
      display: block;
      margin-bottom: 6px;
      font-size: 14px;
    }

    .insight-box p {
      margin: 0;
      color: #555d5f;
      font-size: 13px;
      line-height: 1.75;
    }

    details {
      border-top: 1px solid var(--line);
      border-bottom: 1px solid var(--line);
    }

    summary {
      display: flex;
      min-height: 52px;
      align-items: center;
      justify-content: space-between;
      gap: 20px;
      cursor: pointer;
      font-size: 14px;
      font-weight: 900;
      list-style: none;
    }

    summary::-webkit-details-marker { display: none; }

    summary::after {
      content: "+";
      font-size: 21px;
      font-weight: 500;
    }

    details[open] summary::after { content: "−"; }

    .review-list {
      display: grid;
      gap: 0;
      margin: 0;
      padding: 0 0 16px;
      list-style: none;
    }

    .review-list li {
      display: grid;
      grid-template-columns: 30px minmax(0, 1fr);
      gap: 9px;
      padding: 10px 0;
      border-top: 1px dashed var(--line);
      font-size: 12px;
      line-height: 1.6;
    }

    .review-index {
      color: var(--coral-dark);
      font-family: Consolas, monospace;
      font-weight: 900;
    }

    .result-actions {
      display: flex;
      flex-wrap: wrap;
      justify-content: flex-end;
      gap: 9px;
      padding-top: 22px;
    }

    .toast {
      position: fixed;
      z-index: 20;
      bottom: 22px;
      left: 50%;
      padding: 10px 14px;
      border: 1px solid var(--ink);
      border-radius: 4px;
      background: var(--ink);
      color: #fff;
      font-size: 13px;
      box-shadow: var(--shadow);
      opacity: 0;
      pointer-events: none;
      transform: translate(-50%, 12px);
      transition: opacity 180ms ease, transform 180ms ease;
    }

    .toast.is-visible { opacity: 1; transform: translate(-50%, 0); }

    .sr-only {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    }

    @keyframes scan {
      from { transform: translateY(6px); }
      to { transform: translateY(158px); }
    }

    @keyframes reveal {
      from { opacity: 0; transform: translateY(8px); }
      to { opacity: 1; transform: translateY(0); }
    }

    @keyframes grow {
      from { transform: scaleX(0); }
      to { transform: scaleX(1); }
    }

    @media (max-width: 840px) {
      .app-shell {
        width: min(720px, calc(100% - 24px));
        margin: 12px auto;
        grid-template-columns: 1fr;
      }

      .brand-panel {
        min-height: auto;
        padding: 23px 24px;
        border-right: 0;
        border-bottom: 1px solid var(--ink);
      }

      .brand-panel::after,
      .signal-board,
      .brand-subtitle { display: none; }

      .brand-title { font-size: 24px; }

      .lab-note {
        position: absolute;
        right: 24px;
        bottom: 23px;
        display: block;
        padding: 0;
        border: 0;
        text-align: right;
      }

      .lab-note span { display: none; }

      .main-panel { padding: 23px 24px 26px; }

      .test-view { min-height: 570px; padding-top: 28px; }

      .result-header { grid-template-columns: 1fr; }

      .radar-wrap { width: 250px; }

      .result-grid { grid-template-columns: 1fr; }
    }

    @media (max-width: 560px) {
      .app-shell {
        width: 100%;
        min-height: 100vh;
        margin: 0;
        border: 0;
        border-radius: 0;
      }

      .brand-panel { padding: 18px 19px; }
      .brand-title { font-size: 21px; }
      .eyebrow { margin-bottom: 8px; font-size: 10px; }
      .lab-note { right: 19px; bottom: 19px; }
      .lab-note strong { font-size: 17px; }

      .main-panel { padding: 18px 17px 24px; }
      .topbar { padding-bottom: 16px; }
      .test-view { min-height: calc(100vh - 135px); padding-top: 24px; }
      .question-title { font-size: 21px; }
      .question-context { margin-bottom: 19px; }
      .option-button { grid-template-columns: 31px minmax(0, 1fr) 18px; min-height: 53px; padding: 9px 11px; }
      .option-text { font-size: 14px; }
      .question-footer { align-items: flex-end; }
      .hint { display: none; }
      .actions { width: 100%; }
      .actions .button { flex: 1; }
      .result-title { font-size: 29px; }
      .result-header { gap: 20px; padding-top: 25px; }
      .radar-wrap { width: min(100%, 240px); }
      .dimension-row { grid-template-columns: 53px minmax(0, 1fr) 34px; }
      .result-actions .button { flex: 1 1 140px; }
    }

    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after {
        scroll-behavior: auto !important;
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
      }
    }
  </style>
</head>
<body>
  <main class="app-shell" aria-labelledby="appTitle">
    <aside class="brand-panel">
      <div class="brand-lockup">
        <p class="eyebrow">Loli Profile · 07</p>
        <h1 class="brand-title" id="appTitle">萝莉气质研究所</h1>
        <p class="brand-subtitle">用 12 个生活切片,识别你的发色、瞳色与专属软萌属性。</p>
      </div>

      <div class="signal-board" aria-hidden="true">
        <div class="signal-meta"><span>LOLI PROFILE</span><span>MATCHING</span></div>
        <div class="signal-screen">
          <div class="scan-line"></div>
          <div class="avatar-mark"><span class="avatar-bow"></span></div>
          <span class="signal-label">CUTE MODE</span>
        </div>
      </div>

      <div class="lab-note">
        <span>选你最心动的答案<br>第一反应最接近你</span>
        <strong id="sideCounter">01/12</strong>
      </div>
    </aside>

    <section class="main-panel">
      <header class="topbar">
        <div class="progress-wrap">
          <div class="progress-meta">
            <span id="progressLabel">正在采集日常偏好</span>
            <span id="progressPercent">8%</span>
          </div>
          <div class="progress-track" id="progressTrack" role="progressbar" aria-label="测试进度" aria-valuemin="1" aria-valuemax="12" aria-valuenow="1">
            <div class="progress-fill" id="progressFill"></div>
          </div>
        </div>
        <button class="icon-button" id="resetButton" type="button" title="重新开始" aria-label="重新开始">↻</button>
      </header>

      <section class="test-view" id="testView" aria-live="polite">
        <p class="question-kicker" id="questionKicker">生活切片 · 热身</p>
        <h2 class="question-title" id="questionTitle"></h2>
        <p class="question-context" id="questionContext"></p>
        <div class="options" id="options" role="radiogroup" aria-labelledby="questionTitle"></div>

        <footer class="question-footer">
          <p class="hint">按数字键 1–5 选择,Enter 继续</p>
          <div class="actions">
            <button class="button button-secondary" id="prevButton" type="button">← 上一题</button>
            <button class="button button-primary" id="nextButton" type="button" disabled>下一题 →</button>
          </div>
        </footer>
      </section>

      <section class="result-view" id="resultView" aria-live="polite">
        <div class="result-header">
          <div>
            <span class="result-stamp">萝莉档案完成 · CUTE FOUND</span>
            <h2 class="result-title" id="resultTitle"></h2>
            <p class="result-summary" id="resultSummary"></p>
            <ul class="trait-list" id="traitList"></ul>
          </div>
          <div class="radar-wrap">
            <canvas id="radarCanvas" width="580" height="580" aria-label="五维气质雷达图"></canvas>
          </div>
        </div>

        <div class="result-grid">
          <div>
            <h3 class="section-title">你的五维光谱</h3>
            <div class="dimension-list" id="dimensionList"></div>
          </div>
          <div>
            <h3 class="section-title">研究员手记</h3>
            <div class="insight-box">
              <strong id="insightTitle"></strong>
              <p id="insightText"></p>
            </div>
          </div>
        </div>

        <details>
          <summary>查看我的 12 个选择</summary>
          <ol class="review-list" id="reviewList"></ol>
        </details>

        <div class="result-actions">
          <button class="button button-secondary" id="copyButton" type="button">复制结果</button>
          <button class="button button-secondary" id="saveButton" type="button">保存结果图</button>
          <button class="button button-primary" id="restartButton" type="button">重新测试 ↻</button>
        </div>
      </section>
    </section>
  </main>

  <div class="toast" id="toast" role="status" aria-live="polite"></div>

  <script>
    const DIMENSIONS = {
      spark: { label: "行动力", short: "行动", color: "#e65f51" },
      warmth: { label: "共情力", short: "共情", color: "#f2bd4b" },
      logic: { label: "洞察力", short: "洞察", color: "#3f68a8" },
      dream: { label: "想象力", short: "想象", color: "#765077" },
      ease: { label: "松弛感", short: "自在", color: "#167c80" }
    };

    const questions = [
      {
        kicker: "生活切片 · 热身",
        text: "闹钟响起后的第一个念头,最像下面哪一句?",
        context: "别想太久,抓住刚醒来的那一秒。",
        options: [
          { text: "今天会发生什么有趣的事?", scores: { spark: 3, dream: 1 } },
          { text: "先看看有没有人给我发消息", scores: { warmth: 3, ease: 1 } },
          { text: "再睡五分钟,世界稍后再说", scores: { ease: 3, dream: 1 } },
          { text: "快速过一遍今天的安排", scores: { logic: 3, spark: 1 } },
          { text: "刚才那个梦好像还能续上", scores: { dream: 3, warmth: 1 } }
        ]
      },
      {
        kicker: "生活切片 · 补给",
        text: "走进便利店,只能带走一样东西,你会拿什么?",
        context: "现在是普通的傍晚,没有折扣,也没有人催你。",
        options: [
          { text: "没喝过的新口味饮料", scores: { spark: 3, ease: 1 } },
          { text: "熟悉又稳妥的热饮", scores: { warmth: 3, logic: 1 } },
          { text: "成分表最干净的那一款", scores: { logic: 3, ease: 1 } },
          { text: "包装设计最好看的限定款", scores: { dream: 3, spark: 1 } },
          { text: "随手拿一个,能解渴就行", scores: { ease: 3, warmth: 1 } }
        ]
      },
      {
        kicker: "关系切片 · 初见",
        text: "第一次加入陌生人的聚会,你通常会怎样进入状态?",
        context: "大家看起来都挺友好,只是你一个人都不认识。",
        options: [
          { text: "主动抛出一个轻松的话题", scores: { spark: 3, warmth: 1 } },
          { text: "先和身边的人小范围聊天", scores: { warmth: 3, ease: 1 } },
          { text: "观察大家的关系和氛围", scores: { logic: 3, dream: 1 } },
          { text: "找到有趣的人,聊点不一样的", scores: { dream: 3, spark: 1 } },
          { text: "不刻意表现,舒服就好", scores: { ease: 3, logic: 1 } }
        ]
      },
      {
        kicker: "选择切片 · 周末",
        text: "突然多出一个完全空白的周六,你更想把它交给谁?",
        context: "没有待办、没有工作消息,也没有必须完成的计划。",
        options: [
          { text: "一场说走就走的近郊探索", scores: { spark: 3, dream: 1 } },
          { text: "约上喜欢的人好好吃顿饭", scores: { warmth: 3, spark: 1 } },
          { text: "一本书或一部想看很久的片", scores: { logic: 3, ease: 1 } },
          { text: "做点手工、拍照或写点东西", scores: { dream: 3, warmth: 1 } },
          { text: "宅在家里,想做什么就做什么", scores: { ease: 3, logic: 1 } }
        ]
      },
      {
        kicker: "情绪切片 · 阴天",
        text: "计划被突发状况打乱时,你的真实反应更接近?",
        context: "允许诚实一点,这里不会给“情绪稳定”加分。",
        options: [
          { text: "立刻切换方案,继续往前", scores: { spark: 3, logic: 1 } },
          { text: "先确认有没有影响到别人", scores: { warmth: 3, logic: 1 } },
          { text: "弄清原因,再决定怎么处理", scores: { logic: 3, ease: 1 } },
          { text: "把意外当作剧情的新分支", scores: { dream: 3, spark: 1 } },
          { text: "接受变化,顺着当下走", scores: { ease: 3, warmth: 1 } }
        ]
      },
      {
        kicker: "审美切片 · 房间",
        text: "如果房间里只能保留一件“没什么用但很喜欢”的东西?",
        context: "它不需要证明价值,只需要让你愿意多看一眼。",
        options: [
          { text: "一件有冒险故事的纪念品", scores: { spark: 3, warmth: 1 } },
          { text: "朋友认真挑给我的礼物", scores: { warmth: 3, dream: 1 } },
          { text: "设计精巧、越看越妙的小物", scores: { logic: 3, dream: 1 } },
          { text: "某个虚构世界的角色周边", scores: { dream: 3, warmth: 1 } },
          { text: "触感很好、看着就舒服的抱枕", scores: { ease: 3, spark: 1 } }
        ]
      },
      {
        kicker: "能力切片 · 团队",
        text: "五人小队接到新任务,你自然会站到哪个位置?",
        context: "不是职位选择,而是你最容易进入的状态。",
        options: [
          { text: "开路的人:先做起来再调整", scores: { spark: 3, ease: 1 } },
          { text: "连接的人:让每个人都被听见", scores: { warmth: 3, dream: 1 } },
          { text: "判断的人:拆解信息和风险", scores: { logic: 3, spark: 1 } },
          { text: "点子的人:提供意外的新方向", scores: { dream: 3, logic: 1 } },
          { text: "稳场的人:在混乱里保持从容", scores: { ease: 3, warmth: 1 } }
        ]
      },
      {
        kicker: "直觉切片 · 礼物",
        text: "为很重要的人挑礼物,你最在意哪件事?",
        context: "预算刚刚好,时间也足够。",
        options: [
          { text: "能不能制造一个难忘瞬间", scores: { spark: 3, dream: 1 } },
          { text: "它是不是贴合对方真正的需要", scores: { warmth: 3, logic: 1 } },
          { text: "品质、功能和使用周期", scores: { logic: 3, ease: 1 } },
          { text: "它背后的隐喻与独特故事", scores: { dream: 3, warmth: 1 } },
          { text: "对方收到时会不会毫无压力", scores: { ease: 3, warmth: 1 } }
        ]
      },
      {
        kicker: "社交切片 · 消息",
        text: "朋友深夜发来一句“有空吗”,你会先做什么?",
        context: "没有更多上下文,你并不知道发生了什么。",
        options: [
          { text: "直接拨过去,马上响应", scores: { spark: 3, warmth: 1 } },
          { text: "回复“在,怎么了?”", scores: { warmth: 3, ease: 1 } },
          { text: "回忆他最近是否有异常", scores: { logic: 3, warmth: 1 } },
          { text: "准备好听一个漫长的故事", scores: { dream: 3, warmth: 1 } },
          { text: "先回一个让人安心的表情", scores: { ease: 3, spark: 1 } }
        ]
      },
      {
        kicker: "想象切片 · 世界",
        text: "如果能在另一个世界生活一个月,你会选择哪里?",
        context: "一个月后会平安返回,记忆也会完整保留。",
        options: [
          { text: "地图还没画完的奇幻大陆", scores: { spark: 3, dream: 1 } },
          { text: "邻里彼此照顾的海边小镇", scores: { warmth: 3, ease: 1 } },
          { text: "知识开放的未来研究城", scores: { logic: 3, spark: 1 } },
          { text: "梦境会变成实体的漂浮岛", scores: { dream: 3, ease: 1 } },
          { text: "时间很慢、四季温柔的山谷", scores: { ease: 3, warmth: 1 } }
        ]
      },
      {
        kicker: "价值切片 · 认可",
        text: "听到哪一句评价时,你会在心里偷偷高兴很久?",
        context: "那个人说得很真诚,而且正好看见了你。",
        options: [
          { text: "“和你在一起,总会有新鲜事。”", scores: { spark: 3, dream: 1 } },
          { text: "“你真的很会照顾人的感受。”", scores: { warmth: 3, ease: 1 } },
          { text: "“你总能发现别人忽略的细节。”", scores: { logic: 3, dream: 1 } },
          { text: "“你的想法总有一种独特的美。”", scores: { dream: 3, spark: 1 } },
          { text: "“和你待在一起很放松。”", scores: { ease: 3, warmth: 1 } }
        ]
      },
      {
        kicker: "核心切片 · 最终题",
        text: "故事落幕时,你最希望自己的角色留下什么?",
        context: "这是最后一个切片。选那个让你心里轻轻一动的答案。",
        options: [
          { text: "“她真的勇敢地活过。”", scores: { spark: 3, warmth: 1 } },
          { text: "“因为她,有人被温柔接住。”", scores: { warmth: 3, dream: 1 } },
          { text: "“她看清了真相,也守住了原则。”", scores: { logic: 3, ease: 1 } },
          { text: "“她让平凡世界多了一点想象。”", scores: { dream: 3, spark: 1 } },
          { text: "“她按自己的节奏,过得很自在。”", scores: { ease: 3, logic: 1 } }
        ]
      }
    ];

    const results = {
      spark: {
        title: "金发碧眼、元气满满的小萝莉",
        summary: "你的萝莉属性是明亮又勇敢的元气系。金色的发梢和碧蓝眼眸藏不住好奇心,看到新鲜事总想第一个出发,把普通日子也过成小小冒险。",
        traits: ["金发碧眼", "元气冒险", "笑容发电机"],
        insightTitle: "你是会把周围人一起带动起来的小萝莉",
        insightText: "你最迷人的地方,是愿意先踏出一步的勇气。保持这份闪闪发光的行动力,同时给自己留一点慢下来补充能量的时间。"
      },
      warmth: {
        title: "粉发异瞳、软萌可爱的小萝莉",
        summary: "你的萝莉属性是会治愈人心的软萌系。粉色发丝配着灵动异瞳,让人一眼就想靠近;你总能发现别人没说出口的小情绪,并用温柔把它们接住。",
        traits: ["粉发异瞳", "软萌治愈", "贴心守护"],
        insightTitle: "你的存在自带让人安心的可爱魔法",
        insightText: "你的细腻与善意从不多余,它们让身边的人感到被重视。也别忘了把这份温柔留一部分给自己,软萌的小萝莉也需要被好好照顾。"
      },
      logic: {
        title: "黑发红瞳、安静聪慧的小萝莉",
        summary: "你的萝莉属性是冷静又可靠的观察系。乌黑长发与红色眼眸带着一点神秘感,你习惯默默看清每一个细节,关键时刻总能给出让人安心的判断。",
        traits: ["黑发红瞳", "安静聪慧", "细节捕手"],
        insightTitle: "你是藏着小小智慧库的观察型小萝莉",
        insightText: "你总能在热闹背后找到真正重要的线索。保持这份敏锐,也允许自己偶尔跟着直觉和好奇心走一走,会发现更多有趣的故事。"
      },
      dream: {
        title: "银发紫眸、梦幻甜美的小萝莉",
        summary: "你的萝莉属性是住在想象星云里的梦幻系。银色长发与紫色眼眸像藏着一整片夜空,你会为日常添上闪闪发光的滤镜,也总能从平凡里发现浪漫。",
        traits: ["银发紫眸", "梦幻甜美", "脑洞星球"],
        insightTitle: "你是会把现实点缀成童话的小萝莉",
        insightText: "你的想象力不是飘忽,而是一种珍贵的创造力。把突然冒出的灵感记下来、画下来或做出来,你的小世界会因此越来越闪耀。"
      },
      ease: {
        title: "棕发琥珀瞳、慵懒治愈的小萝莉",
        summary: "你的萝莉属性是轻松又温柔的慵懒系。柔软棕发与琥珀色眼眸像午后阳光,让人想在你身边慢下来;你很懂得按自己的节奏,把日子过得舒服又可爱。",
        traits: ["棕发琥珀瞳", "慵懒治愈", "松弛可爱"],
        insightTitle: "你是让人忍不住放松下来的治愈小萝莉",
        insightText: "你拥有难得的松弛感,也知道如何守住自己的节奏。继续享受小日子里的美好,偶尔主动抓住一个心动机会,会有可爱的惊喜等着你。"
      }
    };

    const state = {
      current: 0,
      answers: Array(questions.length).fill(null)
    };

    let autoAdvanceTimer = null;

    const elements = {
      testView: document.getElementById("testView"),
      resultView: document.getElementById("resultView"),
      questionKicker: document.getElementById("questionKicker"),
      questionTitle: document.getElementById("questionTitle"),
      questionContext: document.getElementById("questionContext"),
      options: document.getElementById("options"),
      prevButton: document.getElementById("prevButton"),
      nextButton: document.getElementById("nextButton"),
      resetButton: document.getElementById("resetButton"),
      progressFill: document.getElementById("progressFill"),
      progressPercent: document.getElementById("progressPercent"),
      progressTrack: document.getElementById("progressTrack"),
      progressLabel: document.getElementById("progressLabel"),
      sideCounter: document.getElementById("sideCounter"),
      resultTitle: document.getElementById("resultTitle"),
      resultSummary: document.getElementById("resultSummary"),
      traitList: document.getElementById("traitList"),
      dimensionList: document.getElementById("dimensionList"),
      insightTitle: document.getElementById("insightTitle"),
      insightText: document.getElementById("insightText"),
      reviewList: document.getElementById("reviewList"),
      radarCanvas: document.getElementById("radarCanvas"),
      copyButton: document.getElementById("copyButton"),
      saveButton: document.getElementById("saveButton"),
      restartButton: document.getElementById("restartButton"),
      toast: document.getElementById("toast")
    };

    function renderQuestion() {
      const question = questions[state.current];
      const selected = state.answers[state.current];
      const progress = Math.round(((state.current + 1) / questions.length) * 100);

      elements.questionKicker.textContent = question.kicker;
      elements.questionTitle.textContent = question.text;
      elements.questionContext.textContent = question.context;
      elements.options.innerHTML = "";

      question.options.forEach((option, index) => {
        const button = document.createElement("button");
        const isSelected = selected === index;
        button.type = "button";
        button.className = "option-button";
        button.setAttribute("role", "radio");
        button.setAttribute("aria-checked", String(isSelected));
        button.dataset.index = index;
        button.innerHTML = `
          <span class="option-key">${index + 1}</span>
          <span class="option-text">${option.text}</span>
          <span class="option-check" aria-hidden="true">✓</span>
        `;
        button.addEventListener("click", () => selectAnswer(index));
        elements.options.appendChild(button);
      });

      elements.progressFill.style.width = `${progress}%`;
      elements.progressPercent.textContent = `${progress}%`;
      elements.progressTrack.setAttribute("aria-valuenow", String(state.current + 1));
      elements.progressLabel.textContent = state.current < 4 ? "正在采集日常偏好" : state.current < 8 ? "正在建立气质坐标" : "正在校准隐藏属性";
      elements.sideCounter.textContent = `${String(state.current + 1).padStart(2, "0")}/${questions.length}`;
      elements.prevButton.disabled = state.current === 0;
      elements.nextButton.disabled = selected === null;
      elements.nextButton.textContent = state.current === questions.length - 1 ? "生成报告 →" : "下一题 →";
      persistState();
    }

    function selectAnswer(index) {
      const questionIndex = state.current;
      state.answers[state.current] = index;
      const buttons = elements.options.querySelectorAll(".option-button");
      buttons.forEach((button, buttonIndex) => {
        button.setAttribute("aria-checked", String(buttonIndex === index));
      });
      elements.nextButton.disabled = false;
      persistState();
      window.clearTimeout(autoAdvanceTimer);
      autoAdvanceTimer = window.setTimeout(() => {
        if (state.current === questionIndex && state.answers[questionIndex] === index && !elements.resultView.classList.contains("is-visible")) {
          nextQuestion();
        }
      }, 420);
    }

    function nextQuestion() {
      window.clearTimeout(autoAdvanceTimer);
      autoAdvanceTimer = null;
      if (state.answers[state.current] === null) return;
      if (state.current === questions.length - 1) {
        showResult();
        return;
      }
      state.current += 1;
      renderQuestion();
      focusQuestion();
    }

    function previousQuestion() {
      window.clearTimeout(autoAdvanceTimer);
      autoAdvanceTimer = null;
      if (state.current === 0) return;
      state.current -= 1;
      renderQuestion();
      focusQuestion();
    }

    function focusQuestion() {
      elements.questionTitle.setAttribute("tabindex", "-1");
      elements.questionTitle.focus({ preventScroll: true });
    }

    function calculateScores() {
      const scores = Object.keys(DIMENSIONS).reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
      state.answers.forEach((answerIndex, questionIndex) => {
        const answer = questions[questionIndex].options[answerIndex];
        Object.entries(answer.scores).forEach(([key, value]) => { scores[key] += value; });
      });
      return scores;
    }

    function getPrimaryDimension(scores) {
      return Object.keys(DIMENSIONS).reduce((best, key) => scores[key] > scores[best] ? key : best, "spark");
    }

    function showResult() {
      const scores = calculateScores();
      const primary = getPrimaryDimension(scores);
      const result = results[primary];
      const highest = Math.max(...Object.values(scores));

      elements.testView.style.display = "none";
      elements.resultView.classList.add("is-visible");
      elements.progressFill.style.width = "100%";
      elements.progressPercent.textContent = "100%";
      elements.progressLabel.textContent = "气质报告已生成";
      elements.sideCounter.textContent = "DONE";
      elements.resultTitle.textContent = result.title;
      elements.resultSummary.textContent = result.summary;
      elements.insightTitle.textContent = result.insightTitle;
      elements.insightText.textContent = result.insightText;

      elements.traitList.innerHTML = result.traits.map(trait => `<li>${trait}</li>`).join("");
      elements.dimensionList.innerHTML = Object.entries(DIMENSIONS).map(([key, dimension]) => {
        const value = Math.round((scores[key] / highest) * 100);
        return `
          <div class="dimension-row">
            <span>${dimension.label}</span>
            <div class="dimension-bar" aria-label="${dimension.label} ${value}%">
              <div class="dimension-fill" style="width:${value}%;background:${dimension.color}"></div>
            </div>
            <span class="dimension-value">${value}</span>
          </div>
        `;
      }).join("");

      elements.reviewList.innerHTML = state.answers.map((answerIndex, index) => `
        <li>
          <span class="review-index">${String(index + 1).padStart(2, "0")}</span>
          <span><strong>${questions[index].text}</strong><br>${questions[index].options[answerIndex].text}</span>
        </li>
      `).join("");

      drawRadar(scores);
      localStorage.removeItem("dimensionLabState");
      window.scrollTo({ top: 0, behavior: "smooth" });
      elements.resultTitle.setAttribute("tabindex", "-1");
      elements.resultTitle.focus({ preventScroll: true });
    }

    function drawRadar(scores) {
      const canvas = elements.radarCanvas;
      const ctx = canvas.getContext("2d");
      const keys = Object.keys(DIMENSIONS);
      const size = canvas.width;
      const center = size / 2;
      const radius = size * 0.31;
      const maxScore = Math.max(...Object.values(scores));
      const angleFor = index => -Math.PI / 2 + (Math.PI * 2 * index / keys.length);

      ctx.clearRect(0, 0, size, size);
      ctx.lineJoin = "round";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.font = "700 24px Microsoft YaHei, sans-serif";

      for (let level = 1; level <= 4; level += 1) {
        ctx.beginPath();
        keys.forEach((key, index) => {
          const angle = angleFor(index);
          const r = radius * level / 4;
          const x = center + Math.cos(angle) * r;
          const y = center + Math.sin(angle) * r;
          index === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
        });
        ctx.closePath();
        ctx.strokeStyle = level === 4 ? "#202628" : "#d7d3ca";
        ctx.lineWidth = level === 4 ? 2.5 : 1.5;
        ctx.stroke();
      }

      keys.forEach((key, index) => {
        const angle = angleFor(index);
        ctx.beginPath();
        ctx.moveTo(center, center);
        ctx.lineTo(center + Math.cos(angle) * radius, center + Math.sin(angle) * radius);
        ctx.strokeStyle = "#d7d3ca";
        ctx.lineWidth = 1.5;
        ctx.stroke();

        const labelRadius = radius + 50;
        ctx.fillStyle = DIMENSIONS[key].color;
        ctx.fillText(DIMENSIONS[key].short, center + Math.cos(angle) * labelRadius, center + Math.sin(angle) * labelRadius);
      });

      ctx.beginPath();
      keys.forEach((key, index) => {
        const angle = angleFor(index);
        const valueRadius = radius * (scores[key] / maxScore);
        const x = center + Math.cos(angle) * valueRadius;
        const y = center + Math.sin(angle) * valueRadius;
        index === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
      });
      ctx.closePath();
      ctx.fillStyle = "rgba(22, 124, 128, 0.24)";
      ctx.strokeStyle = "#167c80";
      ctx.lineWidth = 5;
      ctx.fill();
      ctx.stroke();

      keys.forEach((key, index) => {
        const angle = angleFor(index);
        const valueRadius = radius * (scores[key] / maxScore);
        ctx.beginPath();
        ctx.arc(center + Math.cos(angle) * valueRadius, center + Math.sin(angle) * valueRadius, 8, 0, Math.PI * 2);
        ctx.fillStyle = DIMENSIONS[key].color;
        ctx.fill();
        ctx.strokeStyle = "#fffdf8";
        ctx.lineWidth = 4;
        ctx.stroke();
      });
    }

    function copyResult() {
      const scores = calculateScores();
      const primary = getPrimaryDimension(scores);
      const lines = Object.entries(DIMENSIONS).map(([key, value]) => `${value.label} ${scores[key]}`).join(" · ");
      const text = `我的萝莉气质结果是「${results[primary].title}」!\n${results[primary].traits.join(" / ")}\n${lines}\n——来自萝莉气质研究所`;

      if (navigator.clipboard && window.isSecureContext) {
        navigator.clipboard.writeText(text).then(() => showToast("结果已复制"), () => fallbackCopy(text));
      } else {
        fallbackCopy(text);
      }
    }

    function fallbackCopy(text) {
      const textarea = document.createElement("textarea");
      textarea.value = text;
      textarea.style.position = "fixed";
      textarea.style.opacity = "0";
      document.body.appendChild(textarea);
      textarea.select();
      try {
        document.execCommand("copy");
        showToast("结果已复制");
      } catch (error) {
        showToast("复制失败,请手动截图保存");
      }
      textarea.remove();
    }

    function saveResultImage() {
      const scores = calculateScores();
      const primary = getPrimaryDimension(scores);
      const result = results[primary];
      const canvas = document.createElement("canvas");
      canvas.width = 1200;
      canvas.height = 1500;
      const ctx = canvas.getContext("2d");

      ctx.fillStyle = "#fff2f8";
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.strokeStyle = "#e6a8c7";
      ctx.lineWidth = 4;
      ctx.strokeRect(56, 56, 1088, 1388);

      ctx.fillStyle = "#fff0f7";
      ctx.fillRect(56, 56, 1088, 200);
      ctx.fillStyle = "#ff709f";
      ctx.fillRect(88, 94, 64, 8);
      ctx.fillStyle = "#523c52";
      ctx.font = "900 44px Microsoft YaHei, sans-serif";
      ctx.fillText("萝莉气质研究所", 88, 162);
      ctx.fillStyle = "#a87996";
      ctx.font = "24px Microsoft YaHei, sans-serif";
      ctx.fillText("LOLI PROFILE REPORT · 07", 88, 210);

      ctx.fillStyle = "#c94070";
      ctx.font = "900 25px Microsoft YaHei, sans-serif";
      ctx.fillText("你的专属萝莉属性", 88, 342);
      ctx.fillStyle = "#523c52";
      ctx.font = "900 68px Microsoft YaHei, sans-serif";
      ctx.fillText(result.title, 88, 430);

      let tagX = 88;
      result.traits.forEach(trait => {
        ctx.font = "800 24px Microsoft YaHei, sans-serif";
        const width = ctx.measureText(trait).width + 40;
        ctx.fillStyle = "#ffe49a";
        ctx.fillRect(tagX, 470, width, 48);
        ctx.strokeStyle = "#d777a3";
        ctx.lineWidth = 2;
        ctx.strokeRect(tagX, 470, width, 48);
        ctx.fillStyle = "#523c52";
        ctx.fillText(trait, tagX + 20, 503);
        tagX += width + 14;
      });

      const source = elements.radarCanvas;
      ctx.drawImage(source, 150, 560, 500, 500);

      ctx.fillStyle = "#523c52";
      ctx.font = "900 28px Microsoft YaHei, sans-serif";
      ctx.fillText("五维光谱", 720, 612);
      const highest = Math.max(...Object.values(scores));
      Object.entries(DIMENSIONS).forEach(([key, dimension], index) => {
        const y = 680 + index * 94;
        const value = Math.round(scores[key] / highest * 100);
        ctx.fillStyle = "#523c52";
        ctx.font = "700 23px Microsoft YaHei, sans-serif";
        ctx.fillText(dimension.label, 720, y);
        ctx.fillStyle = "#fde4ef";
        ctx.fillRect(720, y + 22, 330, 16);
        ctx.fillStyle = dimension.color;
        ctx.fillRect(720, y + 22, 330 * value / 100, 16);
        ctx.fillStyle = "#697074";
        ctx.font = "22px Consolas, monospace";
        ctx.fillText(String(value).padStart(3, "0"), 1062, y + 36);
      });

      ctx.fillStyle = "#fff0f6";
      ctx.fillRect(88, 1150, 1024, 174);
      ctx.fillStyle = "#ff709f";
      ctx.fillRect(88, 1150, 10, 174);
      ctx.fillStyle = "#523c52";
      ctx.font = "900 26px Microsoft YaHei, sans-serif";
      ctx.fillText(result.insightTitle, 128, 1205);
      ctx.fillStyle = "#555d5f";
      ctx.font = "22px Microsoft YaHei, sans-serif";
      wrapCanvasText(ctx, result.insightText, 128, 1254, 920, 34, 2);

      ctx.fillStyle = "#697074";
      ctx.font = "20px Microsoft YaHei, sans-serif";
      ctx.fillText("每一种选择,都会指向专属于你的小萝莉属性。", 88, 1390);

      const link = document.createElement("a");
      link.download = `萝莉气质研究所-${result.title}.png`;
      link.href = canvas.toDataURL("image/png");
      link.click();
      showToast("结果图已保存");
    }

    function wrapCanvasText(ctx, text, x, y, maxWidth, lineHeight, maxLines) {
      let line = "";
      let lineCount = 0;
      for (const char of text) {
        const testLine = line + char;
        if (ctx.measureText(testLine).width > maxWidth && line) {
          ctx.fillText(line, x, y + lineCount * lineHeight);
          line = char;
          lineCount += 1;
          if (lineCount >= maxLines) return;
        } else {
          line = testLine;
        }
      }
      if (lineCount < maxLines) ctx.fillText(line, x, y + lineCount * lineHeight);
    }

    function showToast(message) {
      elements.toast.textContent = message;
      elements.toast.classList.add("is-visible");
      window.clearTimeout(showToast.timer);
      showToast.timer = window.setTimeout(() => elements.toast.classList.remove("is-visible"), 1900);
    }

    function persistState() {
      try {
        localStorage.setItem("dimensionLabState", JSON.stringify(state));
      } catch (error) {
        // The test remains fully functional when storage is unavailable.
      }
    }

    function restoreState() {
      try {
        const saved = JSON.parse(localStorage.getItem("dimensionLabState"));
        if (saved && Number.isInteger(saved.current) && Array.isArray(saved.answers) && saved.answers.length === questions.length) {
          state.current = Math.min(Math.max(saved.current, 0), questions.length - 1);
          state.answers = saved.answers.map(value => Number.isInteger(value) && value >= 0 && value < 5 ? value : null);
          if (state.current > 0 || state.answers.some(answer => answer !== null)) {
            window.setTimeout(() => showToast("已恢复上次答题进度"), 350);
          }
        }
      } catch (error) {
        localStorage.removeItem("dimensionLabState");
      }
    }

    function restartTest() {
      window.clearTimeout(autoAdvanceTimer);
      autoAdvanceTimer = null;
      state.current = 0;
      state.answers = Array(questions.length).fill(null);
      localStorage.removeItem("dimensionLabState");
      elements.resultView.classList.remove("is-visible");
      elements.testView.style.display = "flex";
      renderQuestion();
      window.scrollTo({ top: 0, behavior: "smooth" });
      focusQuestion();
    }

    elements.nextButton.addEventListener("click", nextQuestion);
    elements.prevButton.addEventListener("click", previousQuestion);
    elements.resetButton.addEventListener("click", () => {
      if (state.answers.some(answer => answer !== null)) {
        const confirmed = window.confirm("确定清空当前答案并重新开始吗?");
        if (!confirmed) return;
      }
      restartTest();
    });
    elements.restartButton.addEventListener("click", restartTest);
    elements.copyButton.addEventListener("click", copyResult);
    elements.saveButton.addEventListener("click", saveResultImage);

    document.addEventListener("keydown", event => {
      if (!elements.resultView.classList.contains("is-visible")) {
        const numericKey = Number(event.key);
        if (numericKey >= 1 && numericKey <= 5) {
          selectAnswer(numericKey - 1);
          elements.options.children[numericKey - 1].focus();
        }
        if (event.key === "Enter" && state.answers[state.current] !== null) {
          event.preventDefault();
          nextQuestion();
        }
        if (event.key === "ArrowLeft" && state.current > 0) previousQuestion();
      }
    });

    restoreState();
    renderQuestion();
  </script>
</body>
</html>