<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="robots" content="noindex, nofollow">
<title>ロボティクス 声の単語帳</title>
<style>
/* 仕事で使っている「台本読み合わせ」ページと同じ見た目：横スワイプのカード＋その下に「押している間だけ聞く」 */
:root{
  --bg:#f2f3f5; --fg:#1d2129; --muted:#6b7280; --panel:#ffffff; --line:#e3e5e9;
  --card:#1a1b1f; --card-fg:#ffffff; --card-sub:#a9adb6; --gold:#f2c14e; --time:#c98a12;
  --miss:#fde2e1; --miss-fg:#b42318; --extra:#dbeafe; --extra-fg:#1e40af; --ok:#15803d;
  --btn:#1a1b1f; --btn-fg:#ffffff; --rec:#b42318;
  --sh:0 8px 16px rgba(0,0,0,.08); --sh-hover:0 16px 32px rgba(0,0,0,.18); --panel-hover:#ffffff;
  --font:-apple-system,"Hiragino Sans","Hiragino Kaku Gothic ProN",sans-serif;
}
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
  --bg:#111215; --fg:#e8eaee; --muted:#9aa0aa; --panel:#1c1d22; --line:#2c2e35;
  --card:#000000; --miss:#4a1714; --miss-fg:#ffb4ab; --extra:#13284d; --extra-fg:#bcd3ff; --ok:#4ade80;
  --btn:#e8eaee; --btn-fg:#111215; --sh:0 8px 16px rgba(0,0,0,.45); --sh-hover:0 16px 32px rgba(0,0,0,.8); --panel-hover:#23242a; color-scheme:dark}}
:root[data-theme="dark"]{
  --bg:#111215; --fg:#e8eaee; --muted:#9aa0aa; --panel:#1c1d22; --line:#2c2e35;
  --card:#000000; --miss:#4a1714; --miss-fg:#ffb4ab; --extra:#13284d; --extra-fg:#bcd3ff; --ok:#4ade80;
  --btn:#e8eaee; --btn-fg:#111215; --sh:0 8px 16px rgba(0,0,0,.45); --sh-hover:0 16px 32px rgba(0,0,0,.8); --panel-hover:#23242a; color-scheme:dark}
*{box-sizing:border-box}
[hidden]{display:none!important}
html{-webkit-text-size-adjust:100%}
body{margin:0;background:var(--bg);color:var(--fg);font:18px/1.7 var(--font);padding:0 0 calc(24px + env(safe-area-inset-bottom))}
header{background:#1a1b1f;color:#fff;padding:calc(12px + env(safe-area-inset-top)) 16px 12px}
header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
header .hl{min-width:0}
header h1{margin:0;font-size:21px}
.crumb{display:flex;align-items:center;gap:8px;font-size:14px;margin:0 0 2px}
.crumb a{color:rgba(255,255,255,.75);text-decoration:underline;text-underline-offset:3px;text-decoration-color:rgba(255,255,255,.35)}
.crumb a:hover{color:#fff;text-decoration-color:#fff}
.crumb span{color:rgba(255,255,255,.5)}
.logo{flex:0 0 auto;display:block;margin-top:6px;opacity:.9}
.logo:hover{opacity:1}
.logo .ptm{display:block;height:17px;width:auto}
@media (max-width:420px){.logo .ptm{height:13px}}
header p{margin:2px 0 0;font-size:14px;color:rgba(255,255,255,.75)}
main{padding:12px 16px 0;max-width:720px;margin:0 auto}
.bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 10px}
.selwrap{position:relative;display:block;width:100%}
select{-webkit-appearance:none;appearance:none;display:block;width:100%;font:inherit;font-size:16px;font-weight:600;background:var(--btn);color:var(--btn-fg);border:0;border-radius:10px;padding:10px 44px 10px 14px;margin:0}
.selwrap::after{content:"";position:absolute;right:19px;top:50%;width:9px;height:9px;border-right:2.75px solid var(--btn-fg);border-bottom:2.75px solid var(--btn-fg);border-radius:1px;transform:translateY(-70%) rotate(45deg);pointer-events:none}
.seg{display:inline-flex;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--panel)}
.seg button{border:0;background:transparent;color:var(--fg);font:inherit;font-size:15px;padding:8px 12px}
.seg button[aria-pressed="true"]{background:var(--btn);color:var(--btn-fg)}
.seg.step{display:flex;margin:0 0 8px}.seg.step button{flex:1;font-size:16px;font-weight:600;padding:10px 6px}
.card .w{font-size:27px;line-height:1.5;font-weight:700;margin:8px 0 2px}
.card .w.small{font-size:20px;margin-bottom:0}
.info{font-size:14px;color:var(--muted);margin:0 2px 8px}
.track{display:flex;align-items:flex-start;overflow-x:auto;overflow-y:hidden;transition:height .15s;scroll-snap-type:x mandatory;gap:10px;padding:4px 2px 10px;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.track::-webkit-scrollbar{display:none}
.card{flex:0 0 88%;scroll-snap-align:center;background:var(--card);color:var(--card-fg);border-radius:14px;padding:16px 16px 12px;min-height:250px;display:flex;flex-direction:column}
.card .sec{font-size:13.5px;color:var(--card-sub)}
.card .lb{font-size:13.5px;font-weight:700;color:var(--gold);margin-top:2px}
.card .q{font-size:15px;color:var(--card-sub);margin-top:6px}
.card .t{font-size:25px;line-height:1.75;font-weight:600;flex:1;margin:8px 0}
.card .t.long{font-size:20px}
.card .t.mid{font-size:22px}
.card .t.hide{color:var(--card-sub)}
.card .b{font-size:14.5px;line-height:1.7;color:var(--card-sub);margin:0 0 8px}
.card .m{font-size:13.5px;color:var(--time);display:flex;justify-content:space-between;gap:8px}
.card .m .done{color:#9be3b2}.card .m .done.low{color:#ffb4ab}.card .m .done small{color:#a9adb6;font-size:12.5px;margin-left:4px}
.pos{font-size:14px;color:var(--muted);text-align:center;margin:0 0 8px;font-variant-numeric:tabular-nums}
.mic{display:block;width:100%;border:0;border-radius:16px;padding:18px 12px;font:inherit;font-size:20px;font-weight:700;background:var(--btn);color:var(--btn-fg);-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;touch-action:manipulation}
.mic.on{background:var(--rec);color:#fff}
.mic:disabled{opacity:.45}
.mic small{display:block;font-size:13.5px;font-weight:400;opacity:.8}
.res{background:var(--panel);border-radius:14px;padding:12px 14px;margin:12px 0 0;border:1px solid var(--line)}
.res h2{font-size:15px;margin:0 0 6px;color:var(--muted);font-weight:600}
.res .said{font-size:19px;line-height:1.8;min-height:1.8em;word-break:break-all}
.res .cmp{font-size:19px;line-height:1.8;word-break:break-all}
.res .miss{background:var(--miss);color:var(--miss-fg);border-radius:3px}
.res .kw{display:inline-block;border-radius:6px;padding:0 8px;margin:2px 6px 2px 0;background:var(--extra);color:var(--extra-fg)}
.res .kw.miss{background:var(--miss);color:var(--miss-fg)}
.res .lesson{margin-top:6px}
.res .extra{background:var(--extra);color:var(--extra-fg);border-radius:3px}
.stats{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:15px;margin-top:8px;font-variant-numeric:tabular-nums}
.stats b{font-size:20px}
.stats .good{color:var(--ok)}.stats .bad{color:var(--miss-fg)}
.row{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
.row #save{background:var(--btn);color:var(--btn-fg);border-color:var(--btn);font-weight:700}
.row button{border:1px solid var(--line);background:var(--panel);color:var(--fg);font:inherit;font-size:15px;border-radius:10px;padding:8px 12px}
.log{margin:18px 0 0}
.log h2{font-size:18px;margin:0 0 8px;padding-left:10px;border-left:4px solid var(--fg)}
.log table{width:100%;border-collapse:collapse;font-size:15px;background:var(--panel);border-radius:12px;overflow:hidden;font-variant-numeric:tabular-nums}
.log th,.log td{padding:7px 6px;border-bottom:1px solid var(--line);text-align:right}
.log th:first-child,.log td:first-child{text-align:left}
.log th{font-size:13px;color:var(--muted)}
.note{font-size:13px;color:var(--muted);margin:14px 2px 0}
.warn{background:var(--miss);color:var(--miss-fg);border-radius:10px;padding:10px 12px;font-size:15px;margin:0 0 10px}
button:focus-visible,select:focus-visible{outline:3px solid var(--time);outline-offset:2px}
/* カテゴリの一覧（最初の画面） */
.cats{display:flex;flex-direction:column;gap:10px;margin:4px 0 0}
.cat{display:block;width:100%;text-align:left;border:0;background:var(--panel);color:var(--fg);font:inherit;border-radius:8px;box-shadow:var(--sh);padding:14px 44px 14px 16px;position:relative}
.cat::after{content:"";position:absolute;right:20px;top:50%;width:9px;height:9px;border-right:2.75px solid var(--fg);border-top:2.75px solid var(--fg);border-radius:1px;transform:translateY(-50%) rotate(45deg)}
button,select{cursor:pointer}button:disabled{cursor:default}
@media (hover:hover){.cat{transition:box-shadow .18s,transform .18s,background-color .18s}.cat::after{transition:right .18s}.cat:hover{box-shadow:var(--sh-hover);transform:translateY(-4px);background:var(--panel-hover)}.cat:hover::after{right:14px}.cat:active{transform:translateY(-1px)}}
@media (prefers-reduced-motion:reduce){.cat,.cat::after{transition:none}}
.cat .nm{display:block;font-size:18px;font-weight:700;line-height:1.5}
.cat .ct{display:block;font-size:14px;color:var(--muted);margin-top:2px;font-variant-numeric:tabular-nums}
.cat .ct b{color:var(--fg);font-size:16px}
.cat .ct .weak{color:var(--miss-fg)}
.cat .meter{display:block;height:6px;border-radius:3px;background:var(--line);margin-top:10px;overflow:hidden}
.cat .meter i{display:block;height:100%;background:var(--fg);border-radius:3px}
.cat.dark{background:var(--btn);color:var(--btn-fg);border-color:var(--btn)}
.cat.dark::after{border-color:var(--btn-fg)}
.cat.dark .ct{color:inherit;opacity:.75}.cat.dark .ct b{color:inherit}
.homeh{font-size:15px;color:var(--muted);font-weight:600;margin:18px 2px 8px}
@media (min-width:560px){.cats{display:grid;grid-template-columns:1fr 1fr;gap:20px}.homeh{grid-column:1/-1;margin:12px 2px -6px}.cat{display:flex;flex-direction:column}.cat .meter{margin-top:auto}.cat .ct+.ct{margin-bottom:10px}main:has(#home:not([hidden])){max-width:960px}}
.ctitle{font-size:22px;font-weight:700;margin:0 0 10px;line-height:1.4}
</style>
</head>
<body>
<header>
  <div class="hl">
    <nav class="crumb" id="crumb" aria-label="パンくず" hidden><a href="./" id="back">ロボティクス 声の単語帳</a><span aria-hidden="true">›</span></nav>
    <h1 id="h1">ロボティクス 声の単語帳</h1>
    <p id="sub"></p>
  </div>
  <a class="logo" href="https://pixelstomotors.com/" target="_blank" rel="noopener" aria-label="Pixels to Motors（pixelstomotors.com を開く）"><svg class="ptm" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2146 227" role="img" aria-label="Pixels to Motors"><path fill="#ffffff" fill-rule="evenodd" d="M112.78 209.75L112.5 196.5L99.22 196.22L85.94 195.94L86.22 183.22L86.5 170.5L99.78 170.22L113.05 169.94L112.78 155.22L112.5 140.5L99.5 140.5L86.5 140.5L86.5 113L86.5 85.5L99.5 85L112.5 84.5L112.78 71.28L113.06 58.06L125.78 57.78L138.5 57.5L138.5 44.5L138.5 31.5L125.75 31.22L113 30.94L113 18.08C113 7.74 113.29 5.1 114.46 4.66C116.77 3.77 138.58 5.84 145.34 7.58C153.76 9.76 167.03 15.35 175.48 20.28C183.61 25.02 201.04 41.49 207.44 50.48C219.85 67.9 227.92 93.14 227.98 114.76C228.03 131.31 221.93 153.84 213.74 167.34C208.59 175.84 199.91 187.14 194.59 192.27C189.66 197.02 177 206.42 170.88 209.88C157.95 217.17 135.84 223 121.12 223L113.06 223L112.78 209.75ZM60 210L60 198L71 198L82 198L82 210L82 222L71 222L60 222L60 210ZM672 210.35C648.29 207.18 630.41 194.35 622.75 175C618.26 163.66 617.62 160.03 617.56 145.5C617.49 128.97 618.93 122.39 625.73 108.08C634.59 89.44 650.56 77.75 671.87 74.3C711.12 67.95 735.25 91.06 735.45 135.16C735.5 146.17 735.36 146.98 733.08 149.41L730.65 152L689.4 152C642.65 152 644.38 151.71 646.27 159.17C647.94 165.76 653.14 174.12 657.74 177.63C664.51 182.79 672.14 185.2 683.5 185.74C694.69 186.27 701.55 184.93 713.44 179.88C717.25 178.26 720.89 177.13 721.51 177.37C723.28 178.04 731.25 194.45 730.85 196.59C730.34 199.36 717.26 205.53 706.5 208.06C697.69 210.14 679.47 211.35 672 210.35ZM857.83 209.98C847.56 208.24 832.58 202.74 828.78 199.3C827.8 198.42 827 196.69 827 195.46C827 192.99 834.28 177.84 836.25 176.21C837.15 175.47 840.21 176.51 847.18 179.94C858.28 185.39 864.99 187.01 876.42 186.98C888.96 186.94 896.6 182.4 897.68 174.35C898.68 166.86 893.85 163.22 870.19 153.64C852.9 146.63 845.54 142.49 839.52 136.36C828.3 124.95 826.87 105.4 836.24 91.38C840.39 85.17 849.94 78.71 859.34 75.75C869.07 72.7 893.7 72.5 904.5 75.39C913.38 77.76 920.68 81.41 921.46 83.86C922.58 87.39 913.84 108.61 911.67 107.62C896 100.52 883.16 97.21 876.19 98.46C868.83 99.79 865.02 101.35 862.04 104.26C859.59 106.66 859 108.07 859 111.52C859 115.16 859.52 116.25 862.53 118.89C866.99 122.81 870.65 124.65 885.41 130.41C908.5 139.42 917.74 145.94 922.83 156.81C925.17 161.79 925.5 163.74 925.49 172.5C925.48 181.41 925.17 183.12 922.68 188.21C917.66 198.48 906.01 206.41 891.41 209.5C884.08 211.05 865.76 211.31 857.83 209.98ZM1156.16 209.56C1137.52 206.01 1120.64 193.17 1113.11 176.84C1108.09 165.95 1106.54 158.05 1106.66 144C1106.99 103.16 1130.66 76.51 1169.31 73.44C1194.76 71.42 1217.33 81.81 1229.19 101C1244.66 126.01 1243.13 160.84 1225.49 185.57C1220.66 192.33 1212.31 199.28 1203.84 203.58C1191.74 209.72 1170.77 212.35 1156.16 209.56ZM1600.2 209.5C1592.4 208.08 1580.34 202.87 1574.29 198.3C1555.95 184.44 1547.48 161.03 1550.93 133.68C1554.72 103.55 1571.65 83.11 1598.86 75.81C1609.18 73.04 1628.71 72.82 1637.91 75.37C1659.65 81.38 1674.11 95.75 1680.61 117.83C1682.74 125.08 1683.17 128.55 1683.18 139C1683.21 165.01 1674.8 184.47 1658.11 197.02C1642.72 208.59 1621.01 213.27 1600.2 209.5ZM1849.5 209.52C1819.86 204.03 1801.05 181.34 1799.32 149C1797.38 112.66 1816.12 84.33 1847.7 75.85C1858.11 73.05 1877.54 72.83 1887 75.4C1912.45 82.32 1928.16 101.62 1932.05 130.75C1934.17 146.62 1930.26 166.31 1922.28 179.96C1908.72 203.13 1879.36 215.06 1849.5 209.52ZM2070 209.05C2054.77 205.8 2043 200.04 2043 195.83C2043 193.43 2050.57 177.6 2052.47 176.02C2053.47 175.2 2055.37 175.79 2060.21 178.45C2070.47 184.09 2077.39 186.1 2088.73 186.72C2100.85 187.39 2106.14 185.89 2110.95 180.4C2116.32 174.28 2114.52 167.88 2105.9 162.44C2103.78 161.1 2096.3 157.72 2089.28 154.92C2059.7 143.13 2049.02 134.18 2046.5 119.04C2043.04 98.33 2054.16 82.15 2076.5 75.39C2085.19 72.76 2110.68 72.76 2120.5 75.39C2128.86 77.62 2136.86 81.47 2137.7 83.66C2138.86 86.67 2129.73 108.55 2127.7 107.63C2118.17 103.33 2108.27 99.89 2102.74 98.96C2088.17 96.5 2075 102.21 2075 110.98C2075 118.25 2080.03 122 2100.56 130.06C2124.9 139.62 2133.64 145.72 2138.82 156.78C2141.17 161.79 2141.5 163.74 2141.5 172.5C2141.5 181.43 2141.2 183.11 2138.69 188.21C2133.65 198.49 2122.01 206.41 2107.41 209.5C2097.97 211.5 2080.52 211.29 2070 209.05ZM283.62 208.71C281.86 207.42 281.79 203.64 281.94 116.43C282.07 38.35 282.3 25.29 283.55 24.03C284.79 22.79 290.71 22.61 323.25 22.86C362.91 23.16 368.98 23.69 379.64 27.8C394.35 33.46 403 43.37 407.75 60C410.2 68.59 409.91 88.54 407.19 97.89C400.23 121.89 384.68 135.68 359 140.62C354.88 141.41 343.02 142.35 332.66 142.69C318.44 143.17 313.47 143.67 312.41 144.73C311.26 145.89 311 151.79 311 177.1C311 201 310.71 208.15 309.75 208.45C309.06 208.66 303.32 209.11 296.98 209.45C288.14 209.91 285.03 209.74 283.62 208.71ZM444.53 209.21C438.91 209.02 435.28 208.47 434.8 207.71C433.66 205.92 433.84 77.81 434.98 76C435.72 74.83 438.38 74.47 447.18 74.36C453.37 74.28 459.46 74.6 460.72 75.08L463 75.95L463 141.35C463 192.48 462.73 206.97 461.75 207.74C460 209.12 454.96 209.55 444.53 209.21ZM491.25 209.26C486.48 209.06 483 208.49 483 207.89C483 206.64 485.69 202.66 501.84 180C508.71 170.38 516.88 158.9 520.02 154.5C527.25 144.35 529.17 140.6 528.29 138.31C527.91 137.32 521.74 128.18 514.59 118C507.44 107.83 498.31 94.83 494.29 89.13C490.28 83.42 487 77.94 487 76.95C487 74.56 494.67 73.57 508.19 74.22C520.26 74.8 518.71 73.49 533.86 96C546.64 114.98 547.39 116 548.66 116C550.67 116 556.13 108.87 566.42 92.83C572.31 83.67 577.88 75.87 578.81 75.51C579.74 75.15 586.12 74.61 593 74.32C603.02 73.9 605.88 74.09 607.42 75.26C609.28 76.68 609.25 76.84 606.36 81.11C602.55 86.73 597.47 93.98 588.57 106.5C580.32 118.1 579.95 118.63 572 130.35C568.7 135.22 566 140.07 566 141.12C566 143.72 568.35 147.32 590.05 178.02C600.37 192.61 608.96 205.43 609.15 206.52C609.47 208.35 608.71 208.56 599 209.26C593.23 209.68 586.2 209.77 583.38 209.46C577.72 208.83 575.34 206.9 570.46 199C565.46 190.9 551.75 171.57 548.64 168.25C546.97 166.46 545.08 165 544.43 165C543.32 165 539.2 170.85 523.67 194.5C520.06 200 516.43 205.34 515.6 206.38C514.01 208.35 507.54 210.02 502.5 209.75C500.85 209.66 495.79 209.44 491.25 209.26ZM785.38 208.63C775.57 206.17 768.62 200.76 765.33 193C761.24 183.35 761 178.06 761 96.11C761 36.22 761.29 17.11 762.2 16.2C763.91 14.49 787.58 14.53 788.69 16.25C789.14 16.94 789.5 52.6 789.5 95.5C789.5 172.12 789.54 173.56 791.53 177C794.31 181.78 799.1 184 806.67 184C816.25 184 816 183.69 816 195.42C816 209.54 816.09 209.45 801.83 209.71C795.22 209.83 788.37 209.38 785.38 208.63ZM1051 208.41C1043.62 206.46 1035.24 200.58 1031.73 194.89C1026.38 186.22 1025.87 181.93 1025.3 140.5C1024.93 113.71 1024.44 102.28 1023.64 101.77C1023.01 101.36 1019.35 101.02 1015.5 101C1003.4 100.95 1004 101.63 1004 87.99C1004 74.43 1003.56 75.01 1013.83 74.99C1025.88 74.96 1025 76.4 1025 56.58C1025 44.55 1025.36 39.04 1026.2 38.2C1027.46 36.94 1048.4 36.45 1051.42 37.61C1052.78 38.13 1053 40.66 1053 55.55C1053 68.32 1053.33 73.09 1054.25 73.68C1054.94 74.12 1062.7 74.63 1071.5 74.82C1084.77 75.1 1087.71 75.43 1088.75 76.77C1090.65 79.23 1090.42 95.95 1088.44 98.78C1086.95 100.91 1086.25 101 1071.69 101.02C1063.34 101.02 1055.83 101.46 1055 101.98C1053.71 102.79 1053.5 107.88 1053.5 137.71C1053.5 170.84 1053.6 172.67 1055.53 176C1058.9 181.83 1063.65 183.5 1076.79 183.5C1085.73 183.5 1088.27 183.81 1089.02 185C1089.54 185.82 1089.98 190.34 1089.98 195.03C1090 204.96 1088.82 207.4 1083.22 208.96C1077.78 210.47 1057.47 210.13 1051 208.41ZM1744.23 208.45C1729.79 204.56 1722.49 196.28 1719.43 180.31C1718.36 174.69 1718 164.01 1718 137.52C1718 97.26 1719.03 101 1707.98 101C1704.21 101 1699.98 100.56 1698.57 100.02C1696.06 99.07 1696 98.82 1696 89.59C1696 84.39 1696.43 78.99 1696.96 77.6C1697.89 75.16 1698.28 75.06 1707.71 74.79L1717.5 74.5L1717.77 57.07C1717.94 46.29 1718.47 39.14 1719.15 38.32C1720.62 36.55 1743.04 36.44 1744.8 38.2C1745.64 39.04 1746 44.47 1746 56.22L1746 73.05L1748.57 74.02C1749.98 74.56 1757.82 75 1766.01 75C1774.19 75 1781.14 75.42 1781.46 75.94C1781.78 76.45 1782.14 81.66 1782.27 87.51C1782.57 101.45 1783.19 101 1763.88 101C1751.59 101 1748.87 101.27 1747.51 102.63C1746.06 104.09 1745.92 107.97 1746.19 139.06C1746.49 172.36 1746.59 173.98 1748.56 176.61C1752.58 181.99 1757.26 183.5 1769.93 183.5C1783.07 183.5 1782.49 183.01 1782.85 194.5C1783.29 208.12 1780.71 210.03 1762 209.93C1754.56 209.89 1747.37 209.29 1744.23 208.45ZM1333.35 207.48C1333.03 206.64 1333.26 190.11 1333.86 170.73C1334.47 151.35 1335.24 110.98 1335.59 81C1336.13 33.86 1336.43 26.26 1337.76 24.75C1339.77 22.48 1354.64 22.24 1358.83 24.41C1361.72 25.9 1362.61 26.97 1382.74 53.01C1399.5 74.69 1418.82 98.95 1424.33 105.25C1425.65 106.76 1427.42 108 1428.27 108C1429.11 108 1432.02 105.41 1434.73 102.25C1439.86 96.28 1470.04 58.01 1485.42 38C1490.28 31.68 1495.21 25.72 1496.38 24.77C1499.11 22.55 1517.08 22.19 1518.85 24.32C1519.59 25.21 1520.13 39.38 1520.5 67.57C1520.8 90.63 1521.54 131.22 1522.14 157.76C1523 195.91 1522.97 206.33 1521.99 207.51C1520.35 209.49 1495.8 209.7 1494.26 207.75C1493.65 206.98 1493.03 183.37 1492.64 146.34C1492.29 113.25 1491.73 85.46 1491.4 84.59C1491.06 83.72 1490 83 1489.04 83C1486.98 83 1482.67 87.88 1461.21 114.5C1427.53 156.29 1429.4 154.35 1425.08 152.04C1423.39 151.14 1410.95 136.35 1400.73 123.1C1399.2 121.12 1397.4 118.83 1396.72 118C1396.04 117.17 1390.09 109.75 1383.51 101.5C1368.67 82.9 1367.16 81.41 1365.17 83.4C1363.93 84.64 1363.62 94.21 1363.1 145.97C1362.58 198.75 1362.3 207.2 1361 208.02C1360.17 208.54 1353.75 208.98 1346.72 208.98C1336.19 209 1333.83 208.73 1333.35 207.48ZM1961.26 207.75C1960.63 206.96 1960.27 182.83 1960.28 141.75C1960.29 86.55 1960.5 76.83 1961.73 75.81C1963.57 74.28 1982.57 74.17 1984.07 75.67C1984.66 76.26 1985.47 79.17 1985.87 82.14C1986.64 87.82 1987.52 90 1989.02 90C1989.52 90 1993.47 87.58 1997.81 84.63C2006.81 78.5 2014.06 75.51 2023.58 73.99C2033.05 72.47 2033 72.39 2033 89.15L2033 103.05L2030.43 104.02C2029.02 104.56 2025.18 105 2021.9 105C2011.76 105 1998.02 110.8 1992.06 117.59L1989 121.07L1989 163.83C1989 195.76 1988.7 206.9 1987.8 207.8C1986.09 209.51 1962.61 209.46 1961.26 207.75ZM33.67 191.33C33.3 190.97 33 186.69 33 181.83L33 173L42.5 173L52 173L52 182.5L52 192L43.17 192C38.31 192 34.03 191.7 33.67 191.33ZM1186.07 183.07C1198.11 178.56 1206.39 167.49 1209.67 151.5C1216.03 120.5 1192.45 95.16 1164 102.41C1151.8 105.52 1143.19 113.54 1138.68 126C1130.87 147.58 1137.9 172.79 1154.03 181.01C1164.37 186.29 1175.56 187.01 1186.07 183.07ZM1629 183.52C1639.77 179.73 1648.38 170.06 1652.12 157.55C1653.82 151.88 1654.14 148.57 1653.85 139.53C1653.53 129.37 1653.23 127.9 1650.16 121.68C1646.09 113.4 1640.3 107.49 1633.01 104.17C1628.43 102.08 1625.9 101.65 1618 101.58C1607.75 101.49 1604.68 102.32 1596.68 107.3C1584.22 115.05 1577.65 132.77 1580.07 152.08C1582.28 169.73 1591.72 180.87 1607.98 185.03C1612.72 186.24 1623.48 185.47 1629 183.52ZM1878.18 183.32C1888.78 179.16 1896.04 171.44 1900.59 159.5C1902.11 155.51 1902.5 151.87 1902.5 141.5C1902.5 128.91 1902.4 128.29 1899.31 122C1895.32 113.87 1889.12 107.48 1882 104.16C1877.28 101.96 1875.08 101.59 1866.48 101.55C1857.12 101.5 1856.02 101.72 1849.77 104.79C1835.99 111.57 1828.59 125.46 1828.59 144.5C1828.59 161.97 1835.29 175.45 1846.99 181.5C1856.04 186.18 1868.99 186.93 1878.18 183.32ZM60 155.12L60 143.18L68.25 142.5C72.79 142.13 78.08 141.97 80 142.16L83.5 142.5L83.5 154.5L83.5 166.5L71.75 166.78L60 167.06L60 155.12ZM4.95 164.62C4.7 164.37 4.64 159.86 4.82 154.59L5.14 145L14.57 145L24 145L24 154.5L24 164L16.08 164C11.73 164 7.54 164.24 6.78 164.53C6.02 164.82 5.19 164.86 4.95 164.62ZM32 127.5L32 115.93L42.72 116.22L53.45 116.5L54.2 120.92C54.61 123.35 54.63 128.3 54.23 131.92L53.5 138.5L42.75 138.78L32 139.07L32 127.5ZM139 126.97L139 114.94L125.75 115.22L112.5 115.5L112.22 127.25L111.94 139L125.47 139L139 139L139 126.97ZM706.89 127.09C710.02 125.11 706.15 112.13 700.66 106.15C692.84 97.64 674.17 96.82 662.09 104.44C655.1 108.85 647.52 119.68 648.19 124.29C648.38 125.56 649.5 126.69 651 127.11C654.71 128.13 705.27 128.11 706.89 127.09ZM358.45 114.02C365.14 111.42 373.22 104.49 375.94 99.02C380.02 90.83 380.87 78.6 378.03 68.88C375.53 60.29 368.56 54.09 358.54 51.52C350.91 49.57 314.09 49.31 312.2 51.2C311.32 52.08 311 60.68 311 83.68L311 114.96L313.75 115.57C315.26 115.9 316.95 116.35 317.5 116.57C318.05 116.78 326.15 116.81 335.5 116.64C350.42 116.37 353.23 116.04 358.45 114.02ZM5 100L5 90L14.5 90L24 90L24 100L24 110L14.5 110L5 110L5 100ZM61 100L61 90L71 90L81 90L81 100L81 110L71 110L61 110L61 100ZM59.43 82.69C59.16 81.96 59.06 76.45 59.21 70.44L59.5 59.5L71.25 59.22L83 58.94L83 71.47L83 84L71.47 84C62.87 84 59.81 83.67 59.43 82.69ZM33.21 72.25L33.5 62.5L42.5 62.5L51.5 62.5L51.79 72.25L52.07 82L42.5 82L32.93 82L33.21 72.25ZM97.22 57.26L85.94 56.93L86.22 44.22L86.5 31.5L99.28 31.41L112.06 31.33L111.78 44.4C111.55 55.2 111.24 57.49 110 57.53C109.17 57.56 103.42 57.44 97.22 57.26ZM441.78 56.85C436.56 55.4 431.32 49.33 430.49 43.76C428.65 31.52 437.66 21.08 450.1 21.02C455.48 21 460.25 23.76 463.73 28.92C467.2 34.08 466.94 43.13 463.13 49.13C458.79 55.98 450.05 59.14 441.78 56.85ZM13.16 53.21C5.22 52.97 3.9 52.69 4.41 51.38C4.73 50.53 5 46.27 5 41.92L5 34L14.5 34L24 34L24 43.44C24 48.63 23.66 53.02 23.25 53.19C22.84 53.36 18.3 53.37 13.16 53.21ZM60 17L60 5L71.5 5L83 5L83 17L83 29L71.5 29L60 29L60 17Z"/></svg></a>
</header>
<main>
  <div id="nojs" class="warn" style="font-size:17px;line-height:1.8">このファイルは「ファイル」アプリのプレビューでは動きません（声の聞き取りとカードはブラウザで動きます）。<br><a href="https://pixelstomotors.github.io/robotics-review/" style="color:inherit;font-weight:700">Safari で開く → pixelstomotors.github.io/robotics-review</a></div>
  <section id="home">
    <div class="cats" id="cats"></div>
  </section>
  <section id="study" hidden>
  <div class="seg step" role="group" aria-label="段階">
    <button id="s1" type="button" aria-pressed="true">1 単語</button>
    <button id="s2" type="button" aria-pressed="false">2 説明</button>
    <button id="s3" type="button" aria-pressed="false">3 判断の型</button>
  </div>
  <div class="bar">
    <div class="seg" role="group" aria-label="言い方">
      <button id="mSee" type="button" aria-pressed="true">見て言う</button>
      <button id="mHide" type="button" aria-pressed="false">見ないで言う</button>
    </div>
    <div class="seg" role="group" aria-label="出す範囲">
      <button id="fAll" type="button" aria-pressed="true">全部</button>
      <button id="fWeak" type="button" aria-pressed="false">苦手だけ</button>
    </div>
  </div>
  <div id="warn" class="warn" hidden></div>
  <p class="info" id="info"></p>
  <div class="track" id="track" aria-live="off"></div>
  <p class="pos" id="pos"></p>
  <button id="mic" class="mic" type="button">押している間だけ聞く<small>カードを見て話して、指を離す</small></button>
  <section class="res" id="res" aria-live="polite">
    <h2>話した言葉</h2>
    <div class="said" id="said">ここに出ます</div>
    <h2 style="margin-top:10px" id="cmpH">違い（赤＝言わなかった・青＝余分）</h2>
    <div class="cmp" id="cmp">—</div>
    <div class="stats" id="stats"></div>
  </section>
  </section>
  <section class="log" id="logSec" hidden>
    <h2>今回の練習</h2>
    <table><thead><tr><th>文</th><th>一致</th><th>前回</th></tr></thead><tbody id="log"><tr><td colspan="3">まだありません</td></tr></tbody></table>
    <div class="row"><button id="save" type="button">結果を保存</button><button id="copy" type="button">結果をコピー</button><button id="clear" type="button">今回の結果を消す</button></div>
    <div class="row" id="dirRow" hidden><button id="redir" type="button">保存先を選び直す</button><span id="dirName" class="note" style="margin:0;align-self:center"></span></div>
    <p class="note" id="saveNote" style="margin-top:6px"></p>
  </section>
  <p class="note" id="ver"></p>
</main>
<script>
(() => {
  document.getElementById('nojs').remove(); // ブラウザで開けている（プレビューではない）
  const DATA = /*DATA*/null;
  const LABEL = Object.fromEntries(DATA.categories.map(c => [c.key, c.label]));
  const GOOD = 80; // 一致がこれ未満なら「苦手」

  const $ = id => document.getElementById(id);
  const track = $('track'), micBtn = $('mic');
  let step = 1, cat = 'all', mode = 'see', filter = 'all', cards = [], cur = 0, results = {}, tries = {};
  const rk = x => `${x.id}:${step === 3 ? 3 : step}`; // 結果は段階ごとに分けて覚える

  // 前回の一致（この端末のブラウザの中だけに覚える）
  const STORE = 'robotics-review-v2';
  let saved = {};
  try { saved = JSON.parse(localStorage.getItem(STORE) || '{}') || {}; } catch (_) { saved = {}; }
  const persist = () => { try { localStorage.setItem(STORE, JSON.stringify(saved)); } catch (_) {} };
  // 保存した練習記録の集計（iCloud の 練習記録 → _tools/practice_review.py → 書き出し時に埋め込む）。端末の記録より新しければこちらを使う
  const PROGRESS = /*PROGRESS*/{};
  Object.entries(PROGRESS).forEach(([k, v]) => { if (!saved[k] || !saved[k].at || saved[k].at < v.at) saved[k] = v; });
  let attempts = []; // 今回の練習の全部の回（記録ファイルに書く）

  // 声に出す文：要点はその文、用語・問いは答え
  const sayText = x => x.type === 'word' ? (step === 2 ? x.detail : x.back) : x.type === 'point' ? x.front : x.back;
  const termForms = w => { const m = w.match(/^(.*?)（(.*)）$/); if (!m) return [w]; const sub = m[2].split(/・|、/).map(t => t.trim()).filter(Boolean); return [m[1], ...sub, m[1] + m[2]]; };
  const targets = x => x.type === 'word' && step === 1 ? termForms(x.front).flatMap(t => [`${t}は、${x.back}`, `${t}、${x.back}`]) : [sayText(x)];
  const pool = () => DATA.items.filter(x => step === 3 ? x.type === 'lesson' : x.type === 'word');
  const esc = s => String(s).replace(/[&<>"]/g, c => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'}[c]));

  // ===== カテゴリの一覧（最初の画面）。カテゴリを選ぶと、その中で 1 単語 → 2 説明 → 3 判断の型 を練習する =====
  const inCat = (x, k) => k === 'all' ? true : k === 'key' ? x.key : x.category === k;
  const catName = k => k === 'all' ? '全部' : k === 'key' ? 'まず覚える・核心' : LABEL[k];
  function progress(k) { // 言えた＝最新の一致が80%以上。単語は1と2の2回、判断の型は3で数える
    const its = DATA.items.filter(x => inCat(x, k));
    const keys = its.flatMap(x => x.type === 'word' ? [`${x.id}:1`, `${x.id}:2`] : [`${x.id}:3`]);
    const ok = keys.filter(q => saved[q] && saved[q].s >= GOOD).length, weak = keys.filter(q => saved[q] && saved[q].s < GOOD).length;
    return {w: its.filter(x => x.type === 'word').length, l: its.filter(x => x.type === 'lesson').length, ok, weak, total: keys.length};
  }
  function catRow(k, dark) {
    const p = progress(k);
    return `<button class="cat${dark ? ' dark' : ''}" type="button" data-k="${k}"><span class="nm">${esc(catName(k))}</span>`
      + `<span class="ct">単語 ${p.w}・判断の型 ${p.l}</span><span class="ct">言えた <b>${p.ok}</b> / ${p.total}${p.weak ? `<span class="weak">・苦手 ${p.weak}</span>` : ''}</span>`
      + `<span class="meter"><i style="width:${p.total ? Math.round(p.ok / p.total * 100) : 0}%"></i></span></button>`;
  }
  function renderHome() {
    $('cats').innerHTML = DATA.categories.map(c => catRow(c.key)).join('')
      + `<div class="homeh">まとめて</div>` + catRow('key') + catRow('all');
  }
  $('cats').addEventListener('click', e => { const b = e.target.closest('.cat'); if (b) openCat(b.dataset.k, true); });
  function showView(k) {
    const home = !k;
    $('home').hidden = !home; $('study').hidden = home;
    if (home) { if (listening) stop(); $('h1').textContent = 'ロボティクス 声の単語帳'; document.title = 'ロボティクス 声の単語帳'; $('sub').hidden = false; $('crumb').hidden = true; renderHome(); $('sub').textContent = `${DATA.categories.length}のカテゴリ・単語 ${DATA.items.filter(x => x.type === 'word').length}・判断の型 ${DATA.items.filter(x => x.type === 'lesson').length}`; window.scrollTo(0, 0); return; }
    cat = k; $('h1').textContent = catName(k); document.title = catName(k) + '｜ロボティクス 声の単語帳'; $('sub').hidden = true; $('crumb').hidden = false;
    setStep(step); window.scrollTo(0, 0);
  }
  function openCat(k, push) { if (push) try { history.pushState({k}, '', '#' + k); } catch (_) {} showView(k); }
  $('back').addEventListener('click', e => { e.preventDefault(); if (history.state && history.state.k) history.back(); else { try { history.replaceState(null, '', location.pathname); } catch (_) {} showView(null); } });
  window.addEventListener('popstate', () => { const k = (location.hash || '').slice(1); showView(validCat(k) ? k : null); });
  const validCat = k => k === 'all' || k === 'key' || !!LABEL[k];
  $('ver').textContent = `データ：${DATA.updated} 更新${Object.keys(PROGRESS).length ? `・記録の集計 ${Object.keys(PROGRESS).length}件を反映` : ''}`;

  function render() { // 出すカードを選び直して描く
    cards = pool().filter(x => cat === 'key' ? x.key : cat === 'all' ? true : x.category === cat)
      .filter(x => filter === 'all' || (saved[rk(x)] && saved[rk(x)].s < GOOD));
    draw();
    track.scrollLeft = 0; cur = 0; updatePos(); showResult();
  }
  function draw() { // 今のカードのまま描き直す（言い終えたあと）
    track.innerHTML = cards.map(x => {
      const say = sayText(x);
      const r = results[rk(x)], p = saved[rk(x)];
      const done = r ? `<span class="done${r.score < GOOD ? ' low' : ''}">${r.n > 1 ? `${r.n}回目 ` : ''}一致 ${r.score}%</span>`
        : p ? `<span class="done${p.s < GOOD ? ' low' : ''}">前回 ${p.s}%</span>` : '';
      const lb = x.key ? `<div class="lb">${x.type === 'word' ? 'まず覚える' : '核心'}</div>` : '';
      const sec = x.type === 'word' || x.type === 'lesson' ? (cat === 'all' || cat === 'key' ? LABEL[x.category] : '') : `${cat === 'key' || cat === 'all' ? LABEL[x.category] + '｜' : ''}${x.sub}`;
      let q = '', t, cls = say.length > 60 ? ' long' : '', b = '', w = '';
      if (x.type === 'word') {
        const hidden = mode === 'hide' && !r;
        if (step === 1) { w = `<div class="w">${esc(x.front)}</div>`; t = hidden ? 'どういう意味？' : say; cls = hidden ? ' mid hide' : ' mid'; }
        else { w = `<div class="w small">${esc(x.front)}</div><div class="q">${esc(x.back)}</div>`; t = hidden ? 'もう少し詳しく言うと？' : say; cls = hidden ? ' mid hide' : ' long'; }
      } else if (x.type === 'lesson') { // 場面を見て、教訓を自分の言葉で言う。実例は読むだけ
        const hidden = mode === 'hide' && !r;
        if (hidden) { t = x.front; cls = ' mid'; }
        else { q = `<div class="q">${esc(x.front)}</div>`; t = x.back; cls = ''; b = `<div class="b">${esc(x.detail)}</div>`; }
      } else if (x.type === 'point') {
        if (mode === 'hide' && !r) { t = say.slice(0, 10) + '…'; cls = ' hide'; } else t = say;
        if (mode === 'see' && x.back) b = `<div class="b">${esc(x.back)}</div>`;
      } else {
        const ask = x.type === 'term' ? `「${x.front}」とは？` : x.front;
        if (mode === 'hide' && !r) { q = ''; t = ask; cls = x.front.length > 40 ? ' long' : ''; }
        else { q = `<div class="q">${esc(ask)}</div>`; t = say; }
      }
      const kind = x.type === 'word' ? (x.src && step === 2 ? x.src : '') : x.type === 'lesson' ? x.src : `${x.type === 'point' ? '要点' : x.type === 'term' ? '用語' : '問い'}・${x.src}`;
      return `<div class="card">${sec ? `<div class="sec">${esc(sec)}</div>` : ''}${lb}${w}${q}<div class="t${cls}">${esc(t)}</div>${b}<div class="m"><span>${esc(kind)}</span>${done}</div></div>`;
    }).join('') || '<div class="card"><div class="t">ここに出す項目がありません</div></div>';
    const keys = cards.filter(x => x.key).length;
    $('info').textContent = `${cards.length}枚${keys && cat !== 'key' ? `（${step === 3 ? '核心' : 'まず覚える'} ${keys}）` : ''}${step === 1 ? '・用語から続けて言う' : step === 3 ? '・場面を見て、教訓を自分の言葉で' : ''}`;
  }

  function updatePos() {
    const el = track.children[0];
    if (!el || !cards.length) { $('pos').textContent = ''; micBtn.disabled = true; return; }
    const w = el.getBoundingClientRect().width + 10;
    cur = Math.min(cards.length - 1, Math.max(0, Math.round(track.scrollLeft / w)));
    $('pos').textContent = `${cur + 1} / ${cards.length}`;
    const cel = track.children[cur]; if (cel) track.style.height = (cel.offsetHeight + 14) + 'px'; // 今のカードの高さに合わせる（長いカードの分の空白を作らない）
    micBtn.disabled = false;
  }
  let posTimer;
  track.addEventListener('scroll', () => { clearTimeout(posTimer); posTimer = setTimeout(() => { const before = cur; updatePos(); if (before !== cur) showResult(); }, 80); }, {passive: true});

  // 文字の比較（台本読み合わせページと同じ。句読点と空白は無視し、記号は読み方にそろえる）
  const SYM = [['°', '度'], ['mm', 'ミリ'], ['cm', 'センチ'], ['px', 'ピクセル'], ['%', 'パーセント'], ['％', 'パーセント'], ['±', 'プラスマイナス'], ['×', 'かける'], ['＝', 'は'], ['=', 'は'], ['→', ''], ['＋', 'プラス'], ['+', 'プラス'], ['&', 'アンド'], ['＆', 'アンド']];
  const sym = s => SYM.reduce((t, [a, b]) => t.split(a).join(b), s);
  const PUNCT = /[\s、。，．,.!！?？「」『』（）()・…〜~：:；;"'“”‘’\-―｜|／/①②③④⑤⑥]/;
  const kata2hira = s => s.replace(/[\u30A1-\u30F6]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0x60));
  const half = ch => ch.replace(/[Ａ-Ｚａ-ｚ０-９]/g, c => String.fromCharCode(c.charCodeAt(0) - 0xFEE0));
  // 1文字ずつ「表示する字」と「比べる字」を持つ（カタカナ・ひらがな、全角・半角、大文字・小文字は同じとみなす）
  const norm = s => prep(s).map(c => c.k).join('');
  const prep = s => [...sym(s)].filter(ch => !PUNCT.test(ch)).map(ch => ({o: ch, k: kata2hira(half(ch).toLowerCase())}));
  // 音声認識の書き分け・聞き違い（同じ読み）は、カードの書き方にそろえてから比べる。一覧はデータの homophones（練習記録を見て増やす）
  const VARIANTS = DATA.homophones || [];
  const align = (script, said) => {
    script = script.toLowerCase(); said = said.replace(/\s+/g, '').toLowerCase();
    said = VARIANTS.reduce((t, g) => { const k = g.find(v => script.includes(v)); return k ? g.filter(v => v !== k).reduce((u, v) => u.split(v).join(k), t) : t; }, said);
    return said.replace(/([ァ-ヶ]+)ー/g, (m, w) => script.includes(w + 'ー') || !script.includes(w) ? m : w); // シミュレーター→シミュレータ（カードに長音がないとき）
  };
  function diff(a, b) { // a=カード, b=話した（prep の配列）
    const n = a.length, m = b.length, dp = Array.from({length: n + 1}, () => new Uint16Array(m + 1));
    for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) dp[i][j] = a[i].k === b[j].k ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
    const ops = []; let i = 0, j = 0;
    while (i < n && j < m) { if (a[i].k === b[j].k) { ops.push(['=', a[i].o]); i++; j++; } else if (dp[i + 1][j] >= dp[i][j + 1]) { ops.push(['-', a[i].o]); i++; } else { ops.push(['+', b[j].o]); j++; } }
    while (i < n) ops.push(['-', a[i++].o]); while (j < m) ops.push(['+', b[j++].o]);
    return {ops, same: dp[0][0]};
  }
  function opsHtml(ops) {
    let h = '', run = '', type = null;
    const flush = () => { if (!run) return; const t = esc(run); h += type === '=' ? t : `<span class="${type === '-' ? 'miss' : 'extra'}">${t}</span>`; run = ''; };
    ops.forEach(([o, ch]) => { if (o !== type) { flush(); type = o; } run += ch; });
    flush(); return h;
  }

  function showResult() {
    const x = cards[cur];
    const r = x ? results[rk(x)] : null;
    if (!r) { $('cmpH').textContent = x && x.type === 'lesson' ? 'キーワード（赤＝言えなかった）' : '違い（赤＝言わなかった・青＝余分）'; $('said').textContent = 'ここに出ます'; $('cmp').textContent = '—'; $('stats').innerHTML = ''; return; }
    $('said').textContent = r.raw || '（聞き取れませんでした）';
    $('cmpH').textContent = r.kws ? 'キーワード（赤＝言えなかった）' : '違い（赤＝言わなかった・青＝余分）';
    $('cmp').innerHTML = r.kws ? r.kws.map(k => `<span class="kw${k.hit ? '' : ' miss'}">${esc(k.w)}</span>`).join('') + `<div class="lesson">${esc(x.back)}</div>` : opsHtml(r.ops);
    const p = saved[rk(x)] && saved[rk(x)].prev;
    $('stats').innerHTML = `<span>${r.n}回目</span><span>一致 <b class="${r.score >= GOOD ? 'good' : 'bad'}">${r.score}%</b>${p != null ? `（前回 ${p}%）` : ''}</span>`;
  }

  function renderLog() {
    $('logSec').hidden = !Object.keys(results).length && !attempts.length;
    const done = Object.entries(results);
    const STEP = {1: '単語', 2: '説明', 3: '型'};
    $('log').innerHTML = done.length ? done.map(([k, r]) => { const p = saved[k] && saved[k].prev; const s = r.x.type === 'lesson' ? r.x.back : r.x.front;
      const sv = r.ai < savedIdx ? ' <small style="color:var(--ok)">保存済</small>' : '';
      return `<tr><td>${STEP[r.st]}｜${esc(s.slice(0, 14))}${s.length > 14 ? '…' : ''}${sv}</td><td>${r.score}%${r.n > 1 ? `<br><small>${r.n}回目</small>` : ''}</td><td>${p != null ? p + '%' : '—'}</td></tr>`; }).join('')
      : '<tr><td colspan="3">まだありません</td></tr>';
  }


  // 音声認識（押している間だけ）
  const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
  const IOS = /iPhone|iPad|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
  let rec = null, listening = false, finalText = '', interim = '', t0 = 0, target = null, restarts = 0;
  if (!SR) { micBtn.disabled = true; showWarn('このブラウザでは声を文字にできません。iPhoneはSafari、PCはChromeかSafariで開いてください。'); }
  function makeRec() {
    const r = new SR(); r.lang = 'ja-JP'; r.continuous = true; r.interimResults = true;
    r.onresult = e => { if (r !== rec) return; interim = ''; for (let i = e.resultIndex; i < e.results.length; i++) { const tx = e.results[i][0].transcript; if (e.results[i].isFinal) finalText += tx; else interim += tx; } $('said').textContent = (finalText + interim) || '…'; };
    r.onerror = e => { if (e.error === 'not-allowed' || e.error === 'service-not-allowed') showWarn('マイクが許可されていません。ブラウザの設定でマイクを許可してください。'); };
    r.onend = () => {
      if (r !== rec) return;
      if (listening && restarts < 3) { restarts++; finalText += interim; interim = ''; rec = makeRec(); try { rec.start(); } catch (_) {} }
      else finish();
    };
    return r;
  }
  // PC：iPhoneと同じ「始まり」「終わり」の音を鳴らす
  let actx = null;
  function beep(up) {
    if (IOS) return;
    try {
      actx = actx || new (window.AudioContext || window.webkitAudioContext)();
      if (actx.state === 'suspended') actx.resume();
      const t = actx.currentTime + 0.01;
      (up ? [660, 990] : [990, 660]).forEach((f, k) => {
        const o = actx.createOscillator(), g = actx.createGain();
        o.type = 'sine'; o.frequency.value = f;
        g.gain.setValueAtTime(0.0001, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.25, t + k * 0.09 + 0.012); g.gain.exponentialRampToValueAtTime(0.0001, t + k * 0.09 + 0.08);
        o.connect(g).connect(actx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.09);
      });
    } catch (_) {}
  }
  const PRE = IOS ? 0 : 200;
  function start(ev) {
    if (micBtn.disabled || listening || !SR || $('study').hidden) return;
    ev.preventDefault();
    updatePos(); // スワイプ・←→の直後でも、今見えているカードを対象にする
    target = cards[cur]; if (!target) return;
    finalText = ''; interim = ''; listening = true; t0 = performance.now(); restarts = 0;
    micBtn.classList.add('on'); micBtn.firstChild.textContent = '聞いています…';
    $('said').textContent = '…'; $('cmp').textContent = '—'; $('stats').innerHTML = '';
    beep(true);
    try { if (rec) rec.abort(); } catch (_) {}
    rec = makeRec();
    const go = () => { if (!listening) return; try { rec.start(); } catch (_) {} };
    PRE ? setTimeout(go, PRE) : go();
  }
  function stop(ev) {
    if (!listening) return;
    if (ev) ev.preventDefault();
    listening = false; target.sec = (performance.now() - t0) / 1000;
    micBtn.classList.remove('on'); micBtn.firstChild.textContent = '押している間だけ聞く';
    setTimeout(() => { try { rec.stop(); } catch (_) { finish(); } beep(false); }, 250); // 言い終わりを取りこぼさない
  }
  function finish() {
    if (!target) return;
    const x = target; target = null;
    const raw = (finalText + interim).trim();
    if (!raw || x.sec < 0.6) { $('said').textContent = !raw ? '聞き取れませんでした。押したまま話して、言い終わってから離してください' : '押す時間が短すぎました'; return; }
    // 1 単語は「用語＋意味」を一続きで言う。用語はカッコの前後どちらで言ってもよく、「は」は無くてもよい（いちばん合う言い方で比べる）
    let best = null;
    if (x.type === 'lesson') { // 教訓はキーワードが言えたかで見る（言い方は自由）
      const s = norm(raw);
      const kws = x.kw.map(g => ({w: g[0], hit: g.some(v => s.includes(norm(v)))}));
      best = {say: x.back, kws, ops: null, score: Math.round(kws.filter(k => k.hit).length / kws.length * 100)};
    } else targets(x).forEach(t => { const a = prep(t), b = prep(align(t, raw)); const d = diff(a, b); const sc = a.length ? Math.round(d.same / Math.max(a.length, b.length) * 100) : 0; if (!best || sc > best.score) best = {say: t, ops: d.ops, score: sc}; });
    const {say, ops, score, kws} = best;
    const key = rk(x);
    (tries[key] = tries[key] || []).push(score);
    const at = stamp();
    results[key] = {raw, ops, kws, score, n: tries[key].length, x, st: step, ai: attempts.length}; // ai：この回が attempts の何番目か（保存済みの表示用）
    if (tries[key].length === 1) saved[key] = {s: score, prev: saved[key] ? saved[key].s : null, at};
    else saved[key] = {s: score, prev: saved[key] ? saved[key].prev : null, at};
    persist();
    attempts.push({id: x.id, step, word: x.type === 'lesson' ? x.back : x.front, card: say, said: raw, score, sec: Math.round(x.sec * 10) / 10, at, ...(kws ? {miss: kws.filter(k => !k.hit).map(k => k.w)} : {})});
    unsaved++; saveLabel();
    const keep = track.scrollLeft; draw(); track.scrollLeft = keep; updatePos(); showResult(); renderLog();
  }
  micBtn.addEventListener('touchstart', start, {passive: false});
  micBtn.addEventListener('touchend', stop, {passive: false});
  micBtn.addEventListener('touchcancel', stop, {passive: false});
  micBtn.addEventListener('mousedown', start);
  window.addEventListener('mouseup', () => stop());
  micBtn.addEventListener('contextmenu', e => e.preventDefault());
  // PC：スペースを押している間だけ聞く／←→でカードを移動
  const typing = e => /INPUT|TEXTAREA|SELECT/.test((e.target || {}).tagName || '');
  window.addEventListener('keydown', e => {
    if (typing(e)) return;
    if (e.code === 'Space') { e.preventDefault(); if (!e.repeat) start(e); }
    else if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
      e.preventDefault(); const el = track.children[0]; if (!el || listening) return;
      const w = el.getBoundingClientRect().width + 10;
      track.scrollTo({left: Math.round(track.scrollLeft / w + (e.key === 'ArrowRight' ? 1 : -1)) * w, behavior: 'smooth'});
    }
  });
  window.addEventListener('keyup', e => { if (e.code === 'Space' && !typing(e)) { e.preventDefault(); stop(e); if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); } });
  window.addEventListener('blur', () => stop());
  if (!IOS) micBtn.querySelector('small').textContent = 'スペースキーでも同じ。←→でカードを移動';

  function showWarn(t) { const w = $('warn'); w.textContent = t; w.hidden = false; }

  const press = (on, off, set) => { $(on).setAttribute('aria-pressed', true); $(off).setAttribute('aria-pressed', false); set(); render(); };
  $('mSee').addEventListener('click', () => { press('mSee', 'mHide', () => mode = 'see'); keepView(); });
  $('mHide').addEventListener('click', () => { press('mHide', 'mSee', () => mode = 'hide'); keepView(); });
  $('fAll').addEventListener('click', () => press('fAll', 'fWeak', () => filter = 'all'));
  $('fWeak').addEventListener('click', () => press('fWeak', 'fAll', () => filter = 'weak'));
  const PREF = 'robotics-review-view';
  const keepView = () => { try { localStorage.setItem(PREF, JSON.stringify({step, cat, mode})); } catch (_) {} };
  const setStep = n => { step = n; [1, 2, 3].forEach(k => $('s' + k).setAttribute('aria-pressed', k === n)); render(); keepView(); };
  [1, 2, 3].forEach(k => $('s' + k).addEventListener('click', () => setStep(k)));
  $('clear').addEventListener('click', () => { results = {}; tries = {}; attempts = []; unsaved = 0; savedIdx = 0; saveLabel(); draw(); showResult(); renderLog(); });

  // ===== 記録の保存（台本読み合わせページと同じ流れ。保存先は iCloud の ロボティクス復習/練習記録） =====
  function stamp() { const d = new Date(), z = n => String(n).padStart(2, '0'); const o = -d.getTimezoneOffset(); return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}T${z(d.getHours())}:${z(d.getMinutes())}:${z(d.getSeconds())}${o >= 0 ? '+' : '-'}${z(Math.floor(Math.abs(o) / 60))}:${z(Math.abs(o) % 60)}`; }
  let unsaved = 0, savedIdx = 0; // savedIdx：保存済みの回の数（次の保存はそれより後の回だけ送る）
  const saveLabel = () => { $('save').textContent = unsaved ? `結果を保存（未保存 ${unsaved}回）` : attempts.length ? `保存済み（${attempts.length}回）` : '結果を保存'; };
  const STEPNAME = {1: '1 単語', 2: '2 説明', 3: '3 判断の型'};
  function report(all) {
    const list = all ? attempts : attempts.slice(savedIdx);
    const d = new Date(); const ts = `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
    let s = `# ロボティクス見直しの記録（${ts}）\n\n${list.length}回・データ ${DATA.updated}\n\n`;
    list.forEach(a => { s += `## ${STEPNAME[a.step]}｜${a.word}\n- カード：${a.card}\n- 話した：${a.said}\n- 一致 ${a.score}%／${a.sec}秒\n\n`; });
    s += '<!-- 集計用（_tools/practice_review.py が読む） -->\n```json\n' + JSON.stringify({page: 'robotics-review', data: DATA.updated, at: stamp(), tries: list}) + '\n```\n';
    return s;
  }
  const fileName = () => { const d = new Date(), z = n => String(n).padStart(2, '0'); return `ロボ見直し_${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}_${z(d.getHours())}${z(d.getMinutes())}${z(d.getSeconds())}.md`; };
  const doneSaving = msg => { unsaved = 0; savedIdx = attempts.length; renderLog(); $('save').textContent = msg; setTimeout(saveLabel, 2500); };
  function download() {
    const b = new Blob([report()], {type: 'application/octet-stream'}); const a = document.createElement('a');
    a.href = URL.createObjectURL(b); a.download = fileName(); document.body.appendChild(a); a.click();
    setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1500);
    doneSaving('保存しました（ダウンロード）');
  }
  // PC（Chrome・Edge）：一度選んだ「練習記録」フォルダへ直接書き込む（選んだフォルダはこのブラウザの中だけに覚える）
  const CAN_DIR = !IOS && 'showDirectoryPicker' in window;
  const idb = (mode, fn) => new Promise((ok, ng) => { try { const q = indexedDB.open('robotics-review', 1); q.onupgradeneeded = () => q.result.createObjectStore('kv'); q.onerror = () => ng(q.error);
    q.onsuccess = () => { try { const t = q.result.transaction('kv', mode); const r = fn(t.objectStore('kv')); t.oncomplete = () => ok(r && r.result); t.onerror = () => ng(t.error); } catch (e) { ng(e); } }; } catch (e) { ng(e); } });
  const getDir = () => idb('readonly', st => st.get('dir')).catch(() => null);
  const putDir = h => idb('readwrite', st => st.put(h, 'dir')).catch(() => null);
  const showDirName = h => { $('dirName').textContent = h ? `保存先：${h.name}` : '保存先：まだ選んでいません'; };
  async function pickDir() {
    const h = await window.showDirectoryPicker({id: 'robo-renshu', mode: 'readwrite'});
    if (h.name !== '練習記録') showWarn(`選んだのは「${h.name}」です。iCloud Drive → PixelstoMotors → ロボティクス復習 → 練習記録 を選んでください。`); else $('warn').hidden = true;
    await putDir(h); showDirName(h); return h;
  }
  // iPhone：共有の画面から「"ファイル"に保存」→ ロボティクス復習 → 練習記録（2回目からは同じ場所が出る）
  const SHORTCUT = 'ロボ見直しを保存';
  async function shareSave() {
    const f = new File([report()], fileName(), {type: 'text/markdown'});
    if (navigator.canShare && navigator.canShare({files: [f]})) { await navigator.share({files: [f]}); doneSaving('保存しました'); return true; }
    return false;
  }
  $('save').addEventListener('click', async () => {
    if (attempts.length <= savedIdx) { $('save').textContent = attempts.length ? '保存済みです' : 'まだ記録がありません'; setTimeout(saveLabel, 1800); return; }
    if (IOS) { // iPhone：記録をコピーして、ショートカット「ロボ見直しを保存」に渡す（練習記録フォルダに直接入る）
      try { await navigator.clipboard.writeText(report()); doneSaving('ショートカットで保存中'); location.href = 'shortcuts://run-shortcut?name=' + encodeURIComponent(SHORTCUT) + '&input=clipboard'; return; } catch (_) {}
      try { if (await shareSave()) return; } catch (e) { if (e && e.name === 'AbortError') return; } return download();
    }
    if (!CAN_DIR) return download();
    try {
      let h = await getDir();
      if (h && (await h.queryPermission({mode: 'readwrite'})) !== 'granted' && (await h.requestPermission({mode: 'readwrite'})) !== 'granted') h = null;
      if (!h) { showWarn('保存先のフォルダを選んでください：iCloud Drive → PixelstoMotors → ロボティクス復習 → 練習記録（最初の1回だけ）'); h = await pickDir(); }
      const w = await (await h.getFileHandle(fileName(), {create: true})).createWritable(); await w.write(report()); await w.close();
      doneSaving(`${h.name}に保存しました`);
    } catch (e) {
      if (e && e.name === 'AbortError') { $('warn').hidden = true; return; }
      showWarn('練習記録に書き込めなかったので、ダウンロードに保存しました。'); download();
    }
  });
  $('copy').addEventListener('click', async () => { try { await navigator.clipboard.writeText(report(true)); $('copy').textContent = 'コピーしました'; } catch (_) { $('copy').textContent = 'コピーできませんでした'; } setTimeout(() => $('copy').textContent = '結果をコピー', 1800); });
  if (CAN_DIR) { $('dirRow').hidden = false; $('redir').addEventListener('click', () => pickDir().catch(() => {})); getDir().then(showDirName); }
  if (IOS) { const b = document.createElement('button'); b.type = 'button'; b.id = 'share'; b.textContent = '共有から保存';
    b.addEventListener('click', async () => { if (attempts.length <= savedIdx) return; try { if (!(await shareSave())) download(); } catch (e) { if (!(e && e.name === 'AbortError')) download(); } });
    $('clear').before(b); }
  $('saveNote').textContent = IOS ? '' : CAN_DIR ? '' : '保存はダウンロードに入ります。練習記録に直接入れたいときはChromeで開いてください';
  window.addEventListener('beforeunload', e => { if (unsaved) { e.preventDefault(); e.returnValue = ''; } });
  // 前に開いていた段階・カテゴリ・言い方から始める（この端末の中だけ）
  // 最初はカテゴリの一覧。URL に #カテゴリ があればそこから（読み込み直しても同じ場所に戻る）
  try { const v = JSON.parse(localStorage.getItem(PREF) || 'null'); if (v) { step = v.step || 1; if (v.mode === 'hide') { mode = 'hide'; $('mHide').setAttribute('aria-pressed', true); $('mSee').setAttribute('aria-pressed', false); } } } catch (_) {}
  const h0 = (location.hash || '').slice(1);
  showView(validCat(h0) ? h0 : null);
})();
</script>
</body>
</html>
