{"id":511,"date":"2026-01-07T04:48:22","date_gmt":"2026-01-07T04:48:22","guid":{"rendered":"https:\/\/i-cte.org\/robot\/?p=511"},"modified":"2026-01-09T11:45:25","modified_gmt":"2026-01-09T11:45:25","slug":"ielts-speaking-2","status":"publish","type":"post","link":"https:\/\/i-cte.org\/robot\/ielts-speaking-2\/","title":{"rendered":"IELTs &#8211; Speaking &#8211; Test 3"},"content":{"rendered":"\n<!-- \u2705 IELTS SPEAKING CHATBOT (Fixed: valid HTML + no JS syntax errors + Part 2 wait + balanced fonts) -->\n<div id=\"icte-speaking-test\">\n\n <!-- \u2705 TOP NAV MENU (GREEN, STICKY, SCROLLABLE) -->\n  <div class=\"icte-menu\" role=\"navigation\" aria-label=\"IELTS practice navigation\">\n       <a href=\"https:\/\/i-cte.org\/robot\/ielts-speaking\/\">Test 1<\/a>\n    <a href=\"https:\/\/i-cte.org\/robot\/ielts-speaking-1\/\">Test 2<\/a>\n    <a href=\"https:\/\/i-cte.org\/robot\/ielts-speaking-2\/\">Test 3<\/a>\n     <a href=\"https:\/\/i-cte.org\/robot\/ielts-speaking-test-4\/\">Test 4<\/a>\n    <a href=\"https:\/\/i-cte.org\/robot\/ielts-listening-overview\/\" class=\"is-current\">Listening<\/a>\n    <a href=\"https:\/\/i-cte.org\/robot\/ielts-reading-overview\/\">Reading<\/a>\n    <a href=\"https:\/\/i-cte.org\/robot\/ielts-writing-overview\/\">Writing<\/a>\n  <\/div>\n\n  <section class=\"icte-ielts\" aria-label=\"IELTS Speaking Test\">\n\n    <!-- \u2705 Robot hero header -->\n    <section class=\"icte-hero\" aria-label=\"Robot examiner header\">\n      <div class=\"icte-hero__left\">\n        <div class=\"icte-badges\">\n          <span class=\"icte-badge\">IELTS Speaking<\/span>\n          <span class=\"icte-badge icte-badge--blue\">Robot Examiner<\/span>\n        <\/div>\n\n        <h2 class=\"icte-hero__title\">Practice with a Robot Examiner<\/h2>\n        <p class=\"icte-hero__sub\">\n          Choose <strong>Part 1<\/strong>, <strong>Part 2<\/strong>, or <strong>Part 3<\/strong>. The examiner reads questions aloud.\n          Recording stays ON until you click Stop or say <strong>\u201cbye \/ goodbye\u201d<\/strong>.\n        <\/p>\n\n        <div class=\"icte-hero__note\">\n          Tip: Answer + explain + give an example to sound more natural and score higher.\n        <\/div>\n      <\/div>\n\n      <div class=\"icte-hero__right\">\n        <div class=\"icte-robotCard\" aria-label=\"Robot examiner image\">\n          <img decoding=\"async\"\n            src=\"https:\/\/i-cte.org\/robot\/wp-content\/uploads\/2025\/12\/3.webp\"\n            alt=\"A friendly robot examiner interviewing a student\"\n            onerror=\"this.onerror=null;this.src='https:\/\/i-cte.org\/robot\/wp-content\/uploads\/2025\/12\/3.webp';\"\n          \/>\n          <div class=\"icte-robotOverlay\">\n            <div class=\"t1\">Robot Examiner<\/div>\n            <div class=\"t2\">IELTS Speaking Practice<\/div>\n          <\/div>\n        <\/div>\n\n        <div class=\"icte-heroHint\" role=\"note\">\n          <strong>Audio note:<\/strong> On mobile, tap <em>Begin<\/em> once to allow voice playback.\n        <\/div>\n      <\/div>\n    <\/section>\n\n    <!-- \u2705 How to use (moved OUTSIDE hero to avoid layout break) -->\n    <section class=\"icte-ielts__panel\" aria-label=\"How to use this page\">\n      <div class=\"icte-ielts__panelHead\">\n        <h3 class=\"icte-ielts__h3\">How to Use the Buttons<\/h3>\n      <\/div>\n\n      <div class=\"icte-ielts__reading\">\n        <div class=\"icte-quickGuide\">\n          <div class=\"icte-quickGuide__grid\">\n            <div class=\"icte-step\">\n              <div class=\"icte-step__title\"><span class=\"icte-step__num\">1<\/span> Select a Part<\/div>\n              <div class=\"icte-step__text\">\n                Click <strong>Full test<\/strong>, <strong>Part 1<\/strong>, <strong>Part 2<\/strong>, or <strong>Part 3<\/strong>.\n                The active part turns <strong>green<\/strong>.\n              <\/div>\n            <\/div>\n\n            <div class=\"icte-step\">\n              <div class=\"icte-step__title\"><span class=\"icte-step__num\">2<\/span> Begin the Test<\/div>\n              <div class=\"icte-step__text\">\n                Click <strong>Begin the Test<\/strong>. The robot shows the first question and (if voice is ON) reads it aloud.\n              <\/div>\n            <\/div>\n\n            <div class=\"icte-step\">\n              <div class=\"icte-step__title\"><span class=\"icte-step__num\">3<\/span> Start \/ Stop Recording<\/div>\n              <div class=\"icte-step__text\">\n                Click <strong>Start Recording<\/strong> and speak. Click <strong>Stop Recording<\/strong> anytime.\n                If <strong>Auto-advance<\/strong> is ON, the robot moves on after the silence time.\n              <\/div>\n            <\/div>\n\n            <div class=\"icte-step\">\n              <div class=\"icte-step__title\"><span class=\"icte-step__num\">4<\/span> Next Question<\/div>\n              <div class=\"icte-step__text\">\n                When you finish, click <strong>Next Question<\/strong>. Your answer is saved into <strong>Full test transcript<\/strong>.\n              <\/div>\n            <\/div>\n\n            <div class=\"icte-step\">\n              <div class=\"icte-step__title\"><span class=\"icte-step__num\">5<\/span> Sample Answer<\/div>\n              <div class=\"icte-step__text\">\n                Click <strong>Sample Answer<\/strong> to view an example response for the current question.\n              <\/div>\n            <\/div>\n\n            <div class=\"icte-step\">\n              <div class=\"icte-step__title\"><span class=\"icte-step__num\">6<\/span> Band Score<\/div>\n              <div class=\"icte-step__text\">\n                After answering several questions, click <strong>Band Score<\/strong> to get practice feedback.\n                Adjust the <strong>Pronunciation self-rating<\/strong> slider if needed.\n              <\/div>\n            <\/div>\n          <\/div>\n\n          <div class=\"icte-quickGuide__footer\">\n            <strong>Tip:<\/strong> You can also type your answer in <em>Current answer (live)<\/em>.\n            Say <strong>\u201cbye\u201d<\/strong> or <strong>\u201cgoodbye\u201d<\/strong> to end the test.\n          <\/div>\n        <\/div>\n      <\/div>\n    <\/section>\n\n    <header class=\"icte-ielts__intro\">\n      <h3 class=\"icte-ielts__title\">IELTS Speaking Test<\/h3>\n      <p class=\"icte-ielts__sub\">\n        Choose Part 1 \/ Part 2 \/ Part 3. The examiner reads questions aloud. Recording stays ON until you click Stop or say \u201cbye\/goodbye\u201d.\n      <\/p>\n    <\/header>\n\n    <!-- Mode \/ Settings -->\n    <section class=\"icte-ielts__panel\" aria-label=\"Mode &#038; Settings\">\n      <div class=\"icte-ielts__panelHead\">\n        <h3 class=\"icte-ielts__h3\">Choose Part<\/h3>\n        <div class=\"icte-ielts__headRight\">\n          <span class=\"icte-pill\" data-el=\"modePill\">Mode: Full test<\/span>\n          <span class=\"icte-pill\" data-el=\"autoPill\">Auto-advance: ON<\/span>\n          <span class=\"icte-pill\" data-el=\"micPill\">\ud83c\udf99 Mic: Checking\u2026<\/span>\n          <span class=\"icte-pill\" data-el=\"ttsPill\">\ud83d\udd0a Voice: Checking\u2026<\/span>\n        <\/div>\n      <\/div>\n\n      <div class=\"icte-ielts__qArea\">\n        <div class=\"icte-modeRow\">\n          <button class=\"icte-btn icte-btn--primary is-active\" type=\"button\" data-action=\"mode\" data-mode=\"full\">Full test<\/button>\n          <button class=\"icte-btn icte-btn--ghost\" type=\"button\" data-action=\"mode\" data-mode=\"p1\">Part 1<\/button>\n          <button class=\"icte-btn icte-btn--ghost\" type=\"button\" data-action=\"mode\" data-mode=\"p2\">Part 2<\/button>\n          <button class=\"icte-btn icte-btn--ghost\" type=\"button\" data-action=\"mode\" data-mode=\"p3\">Part 3<\/button>\n          <button class=\"icte-btn icte-btn--dark\" type=\"button\" data-action=\"toggle-auto\">Auto-advance<\/button>\n\n          <label class=\"icte-small\" style=\"display:flex;align-items:center;gap:.5rem;\">\n            Silence (sec):\n            <input data-el=\"silenceSec\" type=\"number\" min=\"2\" max=\"20\" step=\"0.5\" value=\"6\"\n              style=\"width:92px;padding:.45rem .55rem;border-radius:12px;border:1px solid rgba(0,0,0,.18);font-size:1rem;\">\n          <\/label>\n        <\/div>\n\n        <div class=\"icte-ttsRow\" style=\"margin-top:.8rem;\">\n          <div class=\"icte-ttsCol\">\n            <label class=\"icte-small\"><strong>Examiner Voice<\/strong><\/label>\n            <select data-el=\"voiceSelect\" aria-label=\"Select examiner voice\">\n              <option value=\"\">Loading voices\u2026<\/option>\n            <\/select>\n          <\/div>\n\n          <div class=\"icte-ttsCol\">\n            <label class=\"icte-small\"><strong>Voice Options<\/strong><\/label>\n            <div style=\"display:flex;gap:.6rem;flex-wrap:wrap;align-items:center;\">\n              <button class=\"icte-btn icte-btn--ghost\" type=\"button\" data-action=\"toggle-voice\">Toggle Voice<\/button>\n              <button class=\"icte-btn icte-btn--ghost\" type=\"button\" data-action=\"stop-voice\">Stop Voice<\/button>\n              <span class=\"icte-small\" data-el=\"voiceHint\" style=\"opacity:.85\"><\/span>\n            <\/div>\n          <\/div>\n        <\/div>\n\n        <div class=\"icte-warn\" data-el=\"supportBox\" style=\"display:none;margin-top:.85rem;\">\n          Speech Recognition is not supported on this browser\/device. Use Chrome on desktop, or type your answer and click \u201cNext Question\u201d.\n        <\/div>\n\n        <div class=\"icte-warn\" data-el=\"ttsBox\" style=\"display:none;margin-top:.85rem;\">\n          Robot examiner moves to the next question when you stop speaking for the set silence time.\n        <\/div>\n      <\/div>\n    <\/section>\n\n    <!-- Part Instructions -->\n    <section class=\"icte-ielts__panel\" aria-label=\"Part instructions\">\n      <div class=\"icte-ielts__panelHead\">\n        <h3 class=\"icte-ielts__h3\">Part Instructions<\/h3>\n      <\/div>\n      <div class=\"icte-ielts__reading\">\n        <div class=\"icte-partInfo\" data-el=\"partInfo\"><\/div>\n      <\/div>\n    <\/section>\n\n    <!-- Conversation -->\n    <section class=\"icte-ielts__panel\" aria-label=\"Examiner\">\n      <div class=\"icte-ielts__panelHead\">\n        <h3 class=\"icte-ielts__h3\">Conversation<\/h3>\n        <div class=\"icte-ielts__headRight\">\n          <button class=\"icte-btn icte-btn--info\" type=\"button\" data-action=\"begin\">\u25b6 Begin the Test<\/button>\n          <button class=\"icte-btn icte-btn--ghost\" type=\"button\" data-action=\"repeat\" disabled>Repeat<\/button>\n          <button class=\"icte-btn icte-btn--ghost\" type=\"button\" data-action=\"model\" disabled>Sample Answer<\/button>\n          <button class=\"icte-btn icte-btn--ghost\" type=\"button\" data-action=\"reset\">Reset<\/button>\n        <\/div>\n      <\/div>\n\n      <div class=\"icte-ielts__reading\">\n        <div class=\"icte-chat\" data-el=\"chat\" role=\"region\" aria-label=\"Chat\"><\/div>\n\n        <div class=\"icte-statusRow\">\n          <div class=\"icte-status\">\n            <strong>Current:<\/strong> <span data-el=\"currentMeta\">\u2014<\/span><br>\n            <span class=\"icte-qNow\" data-el=\"currentQ\">\u2014<\/span>\n            <div class=\"icte-small\" data-el=\"needHint\" style=\"margin-top:.45rem;\"><\/div>\n          <\/div>\n          <div class=\"icte-status\">\n            <strong>Recording:<\/strong> <span data-el=\"recState\">OFF<\/span><br>\n            <strong>Current answer words:<\/strong> <span data-el=\"ansWords\">0<\/span>\n            <div class=\"icte-small\">Say \u201cbye\/goodbye\u201d to end the test.<\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n    <\/section>\n\n    <!-- Controls -->\n    <section class=\"icte-ielts__panel\" aria-label=\"Controls\">\n      <div class=\"icte-ielts__panelHead\">\n        <h3 class=\"icte-ielts__h3\">Controls<\/h3>\n        <div class=\"icte-ielts__headRight\">\n          <span class=\"icte-loader\" data-el=\"loader\" aria-hidden=\"true\"><\/span>\n        <\/div>\n      <\/div>\n\n      <div class=\"icte-ielts__qArea\">\n        <div class=\"icte-ielts__btnRow\">\n          <button class=\"icte-btn icte-btn--primary\" type=\"button\" data-action=\"start-rec\">\u25b6 Start Recording<\/button>\n          <button class=\"icte-btn icte-btn--danger\" type=\"button\" data-action=\"stop-rec\" disabled>\u25a0 Stop Recording<\/button>\n          <button class=\"icte-btn icte-btn--info\" type=\"button\" data-action=\"next\">Next Question \u25b6<\/button>\n          <button class=\"icte-btn icte-btn--dark\" type=\"button\" data-action=\"score\">Band Score<\/button>\n          <button class=\"icte-btn icte-btn--ghost\" type=\"button\" data-action=\"clear-current\">Clear Current Answer<\/button>\n        <\/div>\n\n        <div class=\"icte-grid2\">\n          <div>\n            <div class=\"icte-small\"><strong>Current answer (live)<\/strong> \u2014 speech-to-text goes here. You can also type.<\/div>\n            <div class=\"icte-transcript\" contenteditable=\"true\" data-el=\"currentAnswer\" role=\"textbox\" aria-label=\"Current answer\" spellcheck=\"true\"><\/div>\n          <\/div>\n          <div>\n            <div class=\"icte-small\"><strong>Full test transcript<\/strong> (all answers)<\/div>\n            <div class=\"icte-transcript\" data-el=\"fullTranscript\" style=\"min-height:120px;background:rgba(255,255,255,.65);\" contenteditable=\"false\"><\/div>\n          <\/div>\n        <\/div>\n\n        <div class=\"icte-scoreBox\" data-el=\"scoreBox\" aria-live=\"polite\">\n          <div style=\"display:flex;gap:.8rem;flex-wrap:wrap;align-items:center;justify-content:space-between;\">\n            <div style=\"font-size:22px;font-weight:1000;\">Band: <span data-el=\"bandOut\">\u2014<\/span><\/div>\n            <div style=\"min-width:280px;\">\n              <div class=\"icte-small\"><strong>Pronunciation self-rating<\/strong> (0\u20139)<\/div>\n              <input class=\"icte-range\" type=\"range\" data-el=\"pronSelf\" min=\"0\" max=\"9\" step=\"0.5\" value=\"6.5\">\n              <div class=\"icte-small\">Value: <span data-el=\"pronVal\">6.5<\/span><\/div>\n            <\/div>\n          <\/div>\n\n          <div class=\"icte-scoreGrid\">\n            <div class=\"icte-metric\"><div class=\"k\">Fluency<\/div><div class=\"v\" data-el=\"mFlu\">\u2014<\/div><div class=\"n\" data-el=\"nFlu\"><\/div><\/div>\n            <div class=\"icte-metric\"><div class=\"k\">Lexical<\/div><div class=\"v\" data-el=\"mLex\">\u2014<\/div><div class=\"n\" data-el=\"nLex\"><\/div><\/div>\n            <div class=\"icte-metric\"><div class=\"k\">Grammar<\/div><div class=\"v\" data-el=\"mGram\">\u2014<\/div><div class=\"n\" data-el=\"nGram\"><\/div><\/div>\n            <div class=\"icte-metric\"><div class=\"k\">Pronunciation<\/div><div class=\"v\" data-el=\"mPron\">\u2014<\/div><div class=\"n\" data-el=\"nPron\"><\/div><\/div>\n          <\/div>\n\n          <div class=\"icte-tips\" data-el=\"tips\" style=\"display:none\"><\/div>\n        <\/div>\n      <\/div>\n    <\/section>\n\n    <style>\n\n\/* =========================\n   BALANCED TYPE SCALE (WP-safe)\n   Headings smaller, content bigger, overall nearly equal\n   ========================= *\/\n#icte-speaking-test{\n  font-family: Arial, sans-serif;\n  font-size: 14px;            \/* base for the whole widget *\/\n  line-height: 1.7;\n  color: #0f172a;\n}\n\n\/* Text helpers *\/\n#icte-speaking-test .icte-small{ font-size: 1rem; opacity:.88; }\n\n\/* Pills \/ badges \/ small UI labels *\/\n#icte-speaking-test .icte-pill{ font-size: 1rem; }\n#icte-speaking-test .icte-badge{ font-size: 1rem; }\n\n\/* Buttons *\/\n#icte-speaking-test .icte-btn{ font-size: 1rem; }\n\n\/* Main headings: keep modest *\/\n#icte-speaking-test .icte-ielts__title{ font-size: 1.3rem; }\n#icte-speaking-test .icte-ielts__h3{ font-size: 1.1rem; }\n#icte-speaking-test .icte-hero__title{ font-size: 1.35rem; }\n#icte-speaking-test .icte-hero__sub{ font-size: 1rem; }\n#icte-speaking-test .icte-ielts__sub{ font-size: 1rem; }\n\n\/* \u2705 The important parts: chat + transcript bigger *\/\n#icte-speaking-test .icte-chat{ font-size: 1.05rem; }\n#icte-speaking-test .icte-bubble{ font-size: 1.05rem; }\n#icte-speaking-test .icte-transcript{ font-size: 1.05rem; }\n\n\/* Mini chat bubbles (top demo) *\/\n#icte-speaking-test .icte-miniBubble .who{ font-size: 1rem; }\n#icte-speaking-test .icte-miniBubble .txt{ font-size: 1.05rem; }\n\n\n      \/* =========================\n         QUICK GUIDE\n         ========================= *\/\n      #icte-speaking-test .icte-quickGuide{\n        border:1px solid rgba(0,0,0,.10);\n        border-radius:14px;\n        background:rgba(255,255,255,.72);\n        padding:1rem;\n      }\n      #icte-speaking-test .icte-quickGuide__grid{\n        display:grid;\n        grid-template-columns:repeat(2, minmax(0, 1fr));\n        gap:.75rem;\n      }\n      @media (max-width: 900px){\n        #icte-speaking-test .icte-quickGuide__grid{ grid-template-columns:1fr; }\n      }\n      #icte-speaking-test .icte-step{\n        border:1px solid rgba(0,0,0,.08);\n        border-radius:14px;\n        background:rgba(248,250,252,.75);\n        padding:.85rem .9rem;\n        box-shadow:0 8px 16px rgba(0,0,0,.05);\n      }\n      #icte-speaking-test .icte-step__title{\n        display:flex;\n        align-items:center;\n        gap:.6rem;\n        font-weight:1000;\n        font-size:1.02rem;\n        margin-bottom:.35rem;\n        color:#0f172a;\n      }\n      #icte-speaking-test .icte-step__num{\n        width:30px; height:30px;\n        border-radius:999px;\n        display:inline-flex;\n        align-items:center;\n        justify-content:center;\n        font-weight:1000;\n        color:#064e3b;\n        background:rgba(34,197,94,.16);\n        border:1px solid rgba(34,197,94,.25);\n        flex:0 0 auto;\n      }\n      #icte-speaking-test .icte-step__text{\n        font-size:1rem;\n        color:#334155;\n        line-height:1.65;\n      }\n      #icte-speaking-test .icte-quickGuide__footer{\n        margin-top:.85rem;\n        padding:.75rem .85rem;\n        border-radius:14px;\n        border:1px solid rgba(14,165,233,.22);\n        background:rgba(14,165,233,.10);\n        color:#1e3a8a;\n        font-size:1rem;\n      }\n\n      \/* =========================\n         EXISTING STYLES + polish\n         ========================= *\/\n      #icte-speaking-test .icte-menu{\n        width:100%; max-width:100%; box-sizing:border-box;\n        display:flex; flex-wrap:wrap; gap:.5rem; justify-content:center; align-items:center;\n        padding:.8rem .95rem; margin:0 0 1rem 0;\n        background:#16a34a; border-radius:14px; box-shadow:0 2px 8px rgba(0,0,0,.10);\n      }\n      #icte-speaking-test .icte-menu a{\n        display:inline-block; text-decoration:none; font-weight:900; font-size:1rem; color:#fff;\n        padding:.6rem .9rem; border-radius:999px; border:1px solid rgba(255,255,255,.35);\n        background:rgba(255,255,255,.12);\n      }\n\n      #icte-speaking-test .icte-ielts{ width:100%; margin:1rem 0; }\n      #icte-speaking-test .icte-ielts__intro{\n        padding:1rem 1rem; border:1px solid rgba(0,0,0,.10); border-radius:14px;\n        background:rgba(255,255,255,.7); margin: 0 0 1rem 0;\n      }\n      #icte-speaking-test .icte-ielts__title{ margin:0 0 .35rem; font-weight:900; }\n      #icte-speaking-test .icte-ielts__sub{ margin:0; opacity:.9; }\n\n      #icte-speaking-test .icte-ielts__panel{\n        border:1px solid rgba(0,0,0,.10); border-radius:14px; background:rgba(255,255,255,.85);\n        overflow:hidden; margin-bottom:1rem;\n      }\n      #icte-speaking-test .icte-ielts__panelHead{\n        display:flex; align-items:center; justify-content:space-between; gap:.75rem;\n        padding:.9rem 1rem; border-bottom:1px solid rgba(0,0,0,.08); background:rgba(0,0,0,.03);\n        flex-wrap:wrap;\n      }\n      #icte-speaking-test .icte-ielts__h3{ margin:0; font-weight:900; }\n      #icte-speaking-test .icte-ielts__headRight{ display:flex; gap:.55rem; align-items:center; flex-wrap:wrap; }\n\n      #icte-speaking-test .icte-ielts__reading{ padding:1rem 1rem 1.05rem; }\n      #icte-speaking-test .icte-ielts__qArea{ padding:1rem 1rem 1.05rem; }\n\n      #icte-speaking-test .icte-btn{\n        appearance:none; border:1px solid transparent; border-radius:12px;\n        padding:.7rem .95rem; font-weight:900; cursor:pointer; font:inherit;\n      }\n      #icte-speaking-test .icte-btn:disabled{ opacity:.55; cursor:not-allowed; }\n      #icte-speaking-test .icte-btn--primary{ background:#16a34a; color:#fff; }\n      #icte-speaking-test .icte-btn--info{ background:#0ea5e9; color:#fff; }\n      #icte-speaking-test .icte-btn--danger{ background:#dc2626; color:#fff; }\n      #icte-speaking-test .icte-btn--dark{ background:#334155; color:#fff; }\n      #icte-speaking-test .icte-btn--ghost{ background:transparent; border-color:rgba(0,0,0,.20); color:inherit; }\n      #icte-speaking-test .icte-btn.is-active{\n        background:#16a34a !important;\n        color:#fff !important;\n        border-color:transparent !important;\n      }\n\n      #icte-speaking-test .icte-pill{\n        display:inline-block; padding:.3rem .65rem; border-radius:999px;\n        border:1px solid rgba(0,0,0,.12); background:rgba(255,255,255,.75);\n        font-weight:900;\n      }\n\n      #icte-speaking-test .icte-loader{\n        width:18px; height:18px; border-radius:999px;\n        border:3px solid rgba(0,0,0,.15); border-top-color:#0ea5e9;\n        display:none; animation: icteSpin 1s linear infinite;\n      }\n      @keyframes icteSpin{ to{ transform: rotate(360deg); } }\n\n      #icte-speaking-test .icte-chat{\n        border:1px solid rgba(0,0,0,.12);\n        border-radius:14px; background:rgba(255,255,255,.65);\n        padding:.85rem; min-height:220px; max-height:380px; overflow:auto;\n      }\n      #icte-speaking-test .icte-bubble{\n        max-width:92%; padding:.75rem .85rem; border-radius:14px; margin:.55rem 0;\n        border:1px solid rgba(0,0,0,.10); background:#fff;\n      }\n      #icte-speaking-test .icte-bubble.examiner{ background:#ecfdf5; border-color:rgba(22,163,74,.25); }\n      #icte-speaking-test .icte-bubble.student{ margin-left:auto; background:#fff; }\n\n      #icte-speaking-test .icte-statusRow{ display:flex; gap:.85rem; flex-wrap:wrap; margin-top:.85rem; }\n      #icte-speaking-test .icte-status{\n        flex:1; min-width:240px;\n        border:1px solid rgba(0,0,0,.10); border-radius:14px; background:rgba(255,255,255,.7);\n        padding:.85rem .95rem;\n      }\n      #icte-speaking-test .icte-qNow{ font-weight:1000; display:block; margin-top:.35rem; }\n\n      #icte-speaking-test .icte-modeRow{ display:flex; gap:.6rem; flex-wrap:wrap; align-items:center; }\n      #icte-speaking-test .icte-ttsRow{ display:flex; gap:.85rem; flex-wrap:wrap; align-items:flex-end; }\n      #icte-speaking-test .icte-ttsCol{ flex:1; min-width:260px; }\n      #icte-speaking-test select{\n        width:100%; max-width:520px; padding:.7rem .75rem; border-radius:12px;\n        border:1px solid rgba(0,0,0,.18); background:#fff; font:inherit; font-size:1rem;\n      }\n\n      #icte-speaking-test .icte-ielts__btnRow{ display:flex; gap:.6rem; flex-wrap:wrap; align-items:center; }\n      #icte-speaking-test .icte-grid2{ display:grid; grid-template-columns: 1fr 1fr; gap:.85rem; }\n      @media (max-width: 900px){ #icte-speaking-test .icte-grid2{ grid-template-columns:1fr; } }\n\n      #icte-speaking-test .icte-transcript{\n        border:1px solid rgba(0,0,0,.12); border-radius:14px;\n        background:rgba(255,255,255,.75);\n        padding:.95rem; min-height:130px; white-space:pre-wrap;\n      }\n\n      #icte-speaking-test .icte-warn{\n        border:1px solid #fecaca; background:#fff5f5; color:#7f1d1d;\n        border-radius:14px; padding:.85rem .95rem;\n      }\n\n      #icte-speaking-test .icte-range{ width:100%; }\n\n      #icte-speaking-test .icte-scoreBox{\n        margin-top:1rem;\n        border:1px solid rgba(14,165,233,.25); border-radius:14px;\n        background:rgba(219,234,254,.55);\n        padding:.95rem 1rem;\n      }\n      #icte-speaking-test .icte-scoreGrid{\n        display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.75rem; margin-top:.85rem;\n      }\n      @media (max-width: 700px){ #icte-speaking-test .icte-scoreGrid{ grid-template-columns:1fr; } }\n      #icte-speaking-test .icte-metric{\n        border:1px solid rgba(0,0,0,.10); border-radius:14px; background:#fff;\n        padding:.75rem .85rem;\n      }\n      #icte-speaking-test .icte-metric .k{ font-weight:900; font-size:.95rem; opacity:.75; }\n      #icte-speaking-test .icte-metric .v{ font-weight:1000; font-size:1.2rem; margin-top:.15rem; }\n      #icte-speaking-test .icte-metric .n{ font-size:.95rem; opacity:.75; margin-top:.25rem; }\n      #icte-speaking-test .icte-tips{\n        margin-top:.85rem;\n        border:1px solid rgba(22,163,74,.25); background:rgba(240,253,244,.85);\n        border-radius:14px; padding:.85rem .95rem;\n      }\n      #icte-speaking-test .icte-tips ul{ margin:.4rem 0 0 1.1rem; }\n      #icte-speaking-test .icte-tips li{ margin:.35rem 0; }\n\n      #icte-speaking-test .icte-partInfo{\n        border:1px solid rgba(0,0,0,.10);\n        border-radius:14px;\n        background:rgba(255,255,255,.75);\n        padding:.95rem 1rem;\n      }\n      #icte-speaking-test .icte-partInfo h4{\n        margin:.1rem 0 .55rem;\n        font-size:1.1rem;\n        font-weight:1000;\n      }\n      #icte-speaking-test .icte-partInfo .muted{ opacity:.88; }\n      #icte-speaking-test .icte-partInfo ul{ margin:.45rem 0 0 1.15rem; }\n      #icte-speaking-test .icte-partInfo li{ margin:.3rem 0; }\n      #icte-speaking-test .icte-partBadge{\n        display:inline-block;\n        font-weight:1000;\n        font-size:1rem;\n        padding:.22rem .6rem;\n        border-radius:999px;\n        border:1px solid rgba(22,163,74,.25);\n        background:rgba(240,253,244,.9);\n        margin-right:.5rem;\n      }\n\n      \/* =========================\n         HERO (Robot examiner)\n         ========================= *\/\n      #icte-speaking-test .icte-hero{\n        display:grid;\n        grid-template-columns: 1.1fr .9fr;\n        gap:14px;\n        align-items:stretch;\n        border:1px solid rgba(0,0,0,.10);\n        border-radius:16px;\n        padding:14px;\n        margin-bottom:14px;\n        background:\n          radial-gradient(900px 300px at 20% -10%, rgba(34,197,94,.18), transparent 60%),\n          radial-gradient(900px 300px at 100% 0%, rgba(14,165,233,.14), transparent 55%),\n          rgba(255,255,255,.85);\n        box-shadow:0 10px 24px rgba(0,0,0,.06);\n      }\n      #icte-speaking-test .icte-badges{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; }\n      #icte-speaking-test .icte-badge{\n        display:inline-block;\n        padding:.35rem .7rem;\n        border-radius:999px;\n        font-weight:1000;\n        color:#064e3b;\n        background:rgba(34,197,94,.14);\n        border:1px solid rgba(34,197,94,.25);\n      }\n      #icte-speaking-test .icte-badge--blue{\n        color:#1e3a8a;\n        background:rgba(14,165,233,.12);\n        border-color:rgba(14,165,233,.20);\n      }\n      #icte-speaking-test .icte-hero__title{\n        margin:0 0 6px;\n        font-weight:1000;\n        color:#0f5132;\n      }\n      #icte-speaking-test .icte-hero__sub{\n        margin:0;\n        color:#334155;\n        opacity:.95;\n      }\n      #icte-speaking-test .icte-hero__note{\n        margin-top:10px;\n        color:#334155;\n        padding:.65rem .8rem;\n        border-radius:14px;\n        border:1px solid rgba(34,197,94,.22);\n        background:rgba(240,253,244,.85);\n      }\n      #icte-speaking-test .icte-robotCard{\n        position:relative;\n        border-radius:16px;\n        overflow:hidden;\n        border:1px solid rgba(0,0,0,.10);\n        background:#f8fafc;\n        min-height: 260px;\n        box-shadow:0 12px 22px rgba(0,0,0,.08);\n      }\n      #icte-speaking-test .icte-robotCard img{\n        width:100%;\n        height:100%;\n        object-fit:cover;\n        display:block;\n      }\n      #icte-speaking-test .icte-robotOverlay{\n        position:absolute;\n        left:0; right:0; bottom:0;\n        padding:12px 14px;\n        background: linear-gradient(180deg, transparent, rgba(2,6,23,.65));\n        color:#fff;\n      }\n      #icte-speaking-test .icte-robotOverlay .t1{ font-weight:1000; font-size:1.05rem; }\n      #icte-speaking-test .icte-robotOverlay .t2{ font-size:.95rem; opacity:.92; }\n      #icte-speaking-test .icte-heroHint{\n        margin-top:10px;\n        border-radius:14px;\n        border:1px solid rgba(14,165,233,.22);\n        background: rgba(14,165,233,.10);\n        color:#1e3a8a;\n        padding:.75rem .85rem;\n      }\n      @media (max-width: 980px){\n        #icte-speaking-test .icte-hero{ grid-template-columns:1fr; }\n      }\n    <\/style>\n\n    <script>\n      document.addEventListener(\"DOMContentLoaded\", function(){\n        const root = document.getElementById(\"icte-speaking-test\");\n        if(!root) return;\nfunction cancelGuideSpeech(){\n  guideIsPlaying = false;\n  guideRunId++;      \/\/ invalidate the current guide run immediately\n  stopVoice();       \/\/ stop TTS now\n}\n\n        const $ = (sel)=> root.querySelector(sel);\n        const clamp = (n,a,b)=> Math.max(a, Math.min(b,n));\n        const round1 = (n)=> Math.round(n*10)\/10;\n\/\/ \u2705 Speak and call done() only when TTS finishes (prevents cut-off)\nfunction speakExaminerWait(text, done){\n  if(!speechOk || !ttsEnabled){\n    done && done();\n    return;\n  }\n\n  stopVoice(); \/\/ cancel anything currently speaking\n\n  try{\n    const u = new SpeechSynthesisUtterance(String(text || \"\"));\n    u.lang = (examinerVoice && examinerVoice.lang) ? examinerVoice.lang : \"en-US\";\n    if(examinerVoice) u.voice = examinerVoice;\n    u.rate = 1.0;\n    u.pitch = 1.0;\n\n    u.onend = ()=> { done && done(); };\n    u.onerror = ()=> { done && done(); };\n\n    window.speechSynthesis.speak(u);\n  }catch(e){\n    done && done();\n  }\n}\n\n        function escapeHtml(str){\n          return (str||\"\")\n            .replace(\/&\/g,\"&amp;\").replace(\/<\/g,\"&lt;\").replace(\/>\/g,\"&gt;\")\n            .replace(\/\"\/g,\"&quot;\").replace(\/'\/g,\"&#039;\");\n        }\n\n        function showLoader(show){\n          const el = $('[data-el=\"loader\"]');\n          if(el) el.style.display = show ? \"inline-block\" : \"none\";\n        }\n\n        \/\/ ===== SETTINGS =====\n        let autoAdvance = true;\n\n        function setModePill(mode){\n          const el = $('[data-el=\"modePill\"]');\n          const map = { full:\"Full test\", p1:\"Part 1\", p2:\"Part 2\", p3:\"Part 3\" };\n          el.textContent = \"Mode: \" + (map[mode] || \"Full test\");\n        }\n\n        function refreshAutoPill(){\n          const pill = $('[data-el=\"autoPill\"]');\n          pill.textContent = \"Auto-advance: \" + (autoAdvance ? \"ON\" : \"OFF\");\n          pill.style.background = autoAdvance ? \"rgba(240,253,244,.85)\" : \"rgba(254,226,226,.9)\";\n          pill.style.borderColor = autoAdvance ? \"rgba(22,163,74,.25)\" : \"rgba(220,38,38,.25)\";\n        }\n        refreshAutoPill();\n\n        function setMicPill(text, good){\n          const el = $('[data-el=\"micPill\"]');\n          el.textContent = text;\n          el.style.background = good ? \"rgba(240,253,244,.85)\" : \"rgba(254,226,226,.9)\";\n          el.style.borderColor = good ? \"rgba(22,163,74,.25)\" : \"rgba(220,38,38,.25)\";\n        }\n\n        \/\/ ===== TEXT-TO-SPEECH =====\n        const speechOk = (\"speechSynthesis\" in window) && (\"SpeechSynthesisUtterance\" in window);\n        let ttsEnabled = true;\n        let voicesUsable = [];\n        let examinerVoice = null;\n        let speakRunId = 0;\n\n        function setTtsPill(text, good){\n          const el = $('[data-el=\"ttsPill\"]');\n          el.textContent = text;\n          el.style.background = good ? \"rgba(240,253,244,.85)\" : \"rgba(254,226,226,.9)\";\n          el.style.borderColor = good ? \"rgba(22,163,74,.25)\" : \"rgba(220,38,38,.25)\";\n        }\n\n        function stopVoice(){\n          if(!speechOk) return;\n          speakRunId++;\n          try{ window.speechSynthesis.cancel(); }catch(e){}\n        }\n\n        function speakExaminer(text){\n          if(!speechOk || !ttsEnabled) return;\n          stopVoice();\n          const myRun = ++speakRunId;\n\n          try{\n            const u = new SpeechSynthesisUtterance(String(text||\"\"));\n            u.lang = (examinerVoice && examinerVoice.lang) ? examinerVoice.lang : \"en-US\";\n            if(examinerVoice) u.voice = examinerVoice;\n            u.rate = 1.0;\n            u.pitch = 1.0;\n            if(myRun !== speakRunId) return;\n            window.speechSynthesis.speak(u);\n          }catch(e){}\n        }\n\n        function populateVoices(){\n          if(!speechOk) return;\n\n          const voicesAll = window.speechSynthesis.getVoices() || [];\n          const en = voicesAll.filter(v => (v.lang||\"\").toLowerCase().startsWith(\"en\"));\n          voicesUsable = en.length ? en : voicesAll;\n\n          const sel = $('[data-el=\"voiceSelect\"]');\n          if(!sel) return;\n\n          sel.innerHTML = \"\";\n          if(!voicesUsable.length){\n            sel.innerHTML = '<option value=\"\">No voices found<\/option>';\n            setTtsPill(\"\ud83d\udd0a Voice: Not available\", false);\n            const ttsBox = $('[data-el=\"ttsBox\"]');\n            if(ttsBox) ttsBox.style.display = \"block\";\n            return;\n          }\n\n          voicesUsable.forEach((v, idx) => {\n            const opt = document.createElement(\"option\");\n            opt.value = String(idx);\n            opt.textContent = `${v.name} (${v.lang})`;\n            sel.appendChild(opt);\n          });\n\n          examinerVoice = voicesUsable[0] || null;\n          sel.value = \"0\";\n          sel.onchange = () => {\n            examinerVoice = voicesUsable[Number(sel.value)] || voicesUsable[0] || null;\n          };\n\n          setTtsPill(\"\ud83d\udd0a Voice: Ready\", true);\n          const hint = $('[data-el=\"voiceHint\"]');\n          if(hint) hint.textContent = \"Tip: click Begin once to allow audio in the browser.\";\n        }\n\n        function initTts(){\n          if(!speechOk){\n            setTtsPill(\"\ud83d\udd0a Voice: Not supported\", false);\n            const ttsBox = $('[data-el=\"ttsBox\"]');\n            if(ttsBox) ttsBox.style.display = \"block\";\n            return;\n          }\n          setTtsPill(\"\ud83d\udd0a Voice: Loading\u2026\", true);\n          populateVoices();\n          window.speechSynthesis.onvoiceschanged = populateVoices;\n        }\n\n        \/\/ ===== Part descriptions & instructions =====\nconst PART_INFO = {\n  p1: {\n    title: \"Part 1\",\n    desc: \"The examiner asks the candidate about him\/herself, his\/her home, work or studies and other familiar topics.\",\n    notesTitle: \"Example topic: Clothes\",\n    bullets: [\n      \"Where do you buy most of your clothes? Why?\",\n      \"How often do you buy new clothes for yourself? Why?\",\n      \"How do you decide which clothes to buy? Why?\",\n      \"Have the kinds of clothes you like changed in recent years? Why\/Why not?\"\n    ]\n  },\n  p2: {\n    title: \"Part 2\",\n    desc: \"You will have to talk about the topic for one to two minutes.\",\n    bullets: [\n      \"You have one minute to think about what you are going to say.\",\n      \"You can make some notes to help you if you wish.\"\n    ]\n  },\n  p3: {\n    title: \"Part 3\",\n    desc: \"Discussion topics and more general questions related to the Part 2 theme.\",\n    bullets: [\n      \"Money and young people (example questions)\",\n      \"Money and society (example questions)\"\n    ]\n  }\n};\n\n\n        function renderPartInfo(key, highlightPartTitle){\n          const box = root.querySelector('[data-el=\"partInfo\"]');\n          if(!box) return;\n\n          const info = PART_INFO[key];\n          if(!info){\n            box.innerHTML = '<div class=\"muted\">Choose a part to see instructions.<\/div>';\n            return;\n          }\n\n          let html = `\n            <h4><span class=\"icte-partBadge\">${escapeHtml(highlightPartTitle || info.title)}<\/span>${escapeHtml(info.title)} Instructions<\/h4>\n            <div class=\"muted\">${escapeHtml(info.desc)}<\/div>\n          `;\n\n          if(info.notesTitle){\n            html += `<div style=\"margin-top:.6rem;font-weight:1000;\">${escapeHtml(info.notesTitle)}<\/div>`;\n          }\n\n          if(info.bullets && info.bullets.length){\n            html += \"<ul>\" + info.bullets.map(b => `<li>${escapeHtml(b)}<\/li>`).join(\"\") + \"<\/ul>\";\n          }\n\n          box.innerHTML = html;\n        }\n\n        function modeToInfoKey(mode){\n          if(mode === \"p1\") return \"p1\";\n          if(mode === \"p2\") return \"p2\";\n          if(mode === \"p3\") return \"p3\";\n          return \"p1\";\n        }\n\n        \/\/ ===== CONTENT =====\nconst P1 = [\n  {\n    q: \"Where do you buy most of your clothes? Why?\",\n    model:\n      \"I usually buy my clothes online and sometimes at shopping malls. Online shopping is convenient because I can compare prices and styles quickly.\"\n  },\n  {\n    q: \"How often do you buy new clothes for yourself? Why?\",\n    model:\n      \"Not very often\u2014maybe every few months. I only buy new clothes when I really need something, such as for work or special occasions.\"\n  },\n  {\n    q: \"How do you decide which clothes to buy? Why?\",\n    model:\n      \"I mainly consider comfort, quality, and whether the clothes fit my lifestyle. I also check the material and reviews to make sure it\u2019s worth the money.\"\n  },\n  {\n    q: \"Have the kinds of clothes you like changed in recent years? Why or why not?\",\n    model:\n      \"Yes, they have. I used to prefer trendy styles, but now I like simple and practical clothes because they are easier to mix and match.\"\n  }\n];\n\n\nconst P2 = [{\n  title: \"Describe an interesting discussion you had about how you spend your money.\",\n  body:\n`You should say:\n\u2022 who you had the discussion with\n\u2022 why you discussed this topic\n\u2022 what the result of the discussion was\n\nand explain why this discussion was interesting for you.`,\n  model:\n    \"I\u2019d like to describe an interesting discussion I had with a close friend about how I spend my money. \" +\n    \"We talked about this because we were both trying to save more, but we noticed that small daily expenses were adding up quickly. \" +\n    \"During the discussion, we compared our spending habits, like food delivery, shopping, and entertainment. \" +\n    \"As a result, we decided to set a monthly budget and track our expenses using a simple app. \" +\n    \"This discussion was interesting for me because it helped me see my habits more clearly, and it also gave me practical ideas to manage my money better.\"\n}];\n\n\nconst P3 = [\n  \/\/ Money and young people\n  {\n    group: \"Money and young people\",\n    q: \"Why do some parents give their children money to spend each week?\",\n    model:\n      \"I think parents do that to teach children independence and responsibility. It helps them learn how to budget and make choices, even with a small amount.\"\n  },\n  {\n    group: \"Money and young people\",\n    q: \"Do you agree that schools should teach children how to manage money?\",\n    model:\n      \"Yes, I agree. Basic financial skills like budgeting, saving, and understanding debt are essential for adult life, but many students don\u2019t learn them at home.\"\n  },\n  {\n    group: \"Money and young people\",\n    q: \"Do you think it is a good idea for students to earn money while studying?\",\n    model:\n      \"It can be a good idea if the job is part-time and doesn\u2019t affect their studies. It teaches discipline and work skills, but too much work can cause stress.\"\n  },\n\n  \/\/ Money and society\n  {\n    group: \"Money and society\",\n    q: \"Do you think it is true that in today's society money cannot buy happiness?\",\n    model:\n      \"I partly agree. Money can provide comfort, security, and opportunities, which reduce stress. But real happiness also depends on relationships, health, and purpose.\"\n  },\n  {\n    group: \"Money and society\",\n    q: \"What disadvantages are there in a society where the gap between rich and poor is very large?\",\n    model:\n      \"A large gap can create social tension and reduce trust in society. It may also limit opportunities for poorer people, leading to unfairness and long-term poverty.\"\n  },\n  {\n    group: \"Money and society\",\n    q: \"Do you think richer countries have a responsibility to help poorer countries?\",\n    model:\n      \"In my opinion, yes, to some extent. Helping poorer countries through fair trade, education, and development support can create global stability and benefit everyone.\"\n  }\n];\n\n        \/\/ ===== CHAT =====\n        const chat = $('[data-el=\"chat\"]');\n        function addBubble(role, text){\n          const div = document.createElement(\"div\");\n          div.className = \"icte-bubble \" + (role===\"examiner\" ? \"examiner\" : \"student\");\n          div.innerHTML = escapeHtml(text).replace(\/\\n\/g,\"<br>\");\n          chat.appendChild(div);\n          chat.scrollTop = chat.scrollHeight;\n        }\n\n        \/\/ ===== FLOW =====\n        let flow = [];\n        let idx = -1;\n        let running = false;\n        let awaitingAnswer = false;\n\/\/ \u2705 Guide (spoken instructions) control\nlet guideIsPlaying = false;\nlet guideRunId = 0; \/\/ increment to cancel an ongoing guide speech sequence\n\n        let answerStartTs = 0;      \/\/ when an answer begins\n        let lastAutoAdvanceTs = 0;  \/\/ anti-double-trigger safety\n\n        let currentAnswerText = \"\";\n        let fullTranscriptText = \"\";\n\n        function stopRecording(clearCurrent){\n          manualStop = true;\n          if(recognition && recognizing){\n            try{ recognition.stop(); }catch(e){}\n          }\n          if(clearCurrent){\n            currentAnswerText = \"\";\n            $('[data-el=\"currentAnswer\"]').textContent = \"\";\n            $('[data-el=\"ansWords\"]').textContent = \"0\";\n          }\n        }\n\n        function setCurrentCard(card){\n          $('[data-el=\"currentMeta\"]').textContent = card.part + \" \u2022 \" + card.meta;\n          $('[data-el=\"currentQ\"]').textContent = card.prompt;\n          $('[data-el=\"needHint\"]').textContent = card.needsAnswer\n            ? \"Speak your answer (recording can stay ON). Auto-advance happens after silence OR click Next Question.\"\n            : \"Instruction\/transition (auto-continue).\";\n\n          root.querySelector('[data-action=\"repeat\"]').disabled = false;\n          root.querySelector('[data-action=\"model\"]').disabled = !card.model;\n\n          const startBtn = root.querySelector('[data-action=\"start-rec\"]');\n          const stopBtn  = root.querySelector('[data-action=\"stop-rec\"]');\n\n          if(card.lockRec){\n            stopRecording(false);\n            if(startBtn) startBtn.disabled = true;\n            if(stopBtn)  stopBtn.disabled = true;\n          } else {\n            if(startBtn && !recognizing) startBtn.disabled = false;\n            if(stopBtn) stopBtn.disabled = !recognizing;\n          }\n        }\n\n        function buildFlow(mode){\n          const out = [];\n\n          out.push({\n            part:\"Start\",\n            meta:\"Identity\",\n            prompt:\"Good morning. My name is Robot examiner. Can you tell me your full name, please?\",\n            model:\"My full name is Bella.\",\n            needsAnswer:true\n          });\n\n          if(mode === \"p1\"){\n            out.push({ part:\"Part 1\", meta:\"Start\", prompt:\"We will practice Part 1 only. Let\u2019s begin.\", model:\"\", needsAnswer:false });\n          }\n          if(mode === \"p3\"){\n            out.push({ part:\"Part 3\", meta:\"Start\", prompt:\"We will practice Part 3 only. Let\u2019s begin.\", model:\"\", needsAnswer:false });\n          }\n\n          \/\/ \u2705 Part 1\n          if(mode===\"full\" || mode===\"p1\"){\n            out.push({ part:\"Part 1\", meta:\"Health\", prompt:\"Let\u2019s begin with some questions about health.\", model:\"\", needsAnswer:false });\n            P1.forEach((it,i)=> out.push({\n              part:\"Part 1\",\n              meta:\"Q\"+(i+1),\n              prompt:it.q,\n              model:it.model,\n              needsAnswer:true\n            }));\n          }\n\n          \/\/ \u2705 Part 2 (ONLY ONCE \u2014 fixed duplicate)\n          if(mode===\"full\" || mode===\"p2\"){\n  const card = P2[0];\n\n  \/\/ 1) Tell them what will happen (robot speaks this)\n  out.push({\n    part:\"Part 2\",\n    meta:\"Instructions\",\n    prompt:\"Now I\u2019m going to give you a topic. You have one minute to prepare, then speak for one to two minutes.\",\n    model:\"\",\n    needsAnswer:false,\n    autoDelay: 900\n  });\n\n  \/\/ 2) \u2705 SHOW TOPIC IMMEDIATELY + lock recording + wait 60 seconds\n  out.push({\n    part:\"Part 2\",\n    meta:\"Cue card (Preparation time: 1 minute)\",\n    prompt:\n      \"Preparation time starts now. You have 1 minute. You may write notes. (Recording is locked during preparation.)\\n\\n\"\n      + card.title + \"\\n\\n\" + card.body,\n    model:\"\",\n    needsAnswer:false,\n    autoDelay: 60000,   \/\/ \u2705 wait 60 seconds while the topic is visible\n    lockRec: true       \/\/ \u2705 lock Start\/Stop Recording during prep\n  });\n\n  \/\/ 3) After 60 seconds, allow speaking (show cue again + needsAnswer true)\n  out.push({\n    part:\"Part 2\",\n    meta:\"Answer\",\n    prompt:\"Time is up. Please begin speaking now.\\n\\n\" + card.title + \"\\n\\n\" + card.body,\n    model: card.model,\n    needsAnswer:true\n  });\n}\n\n\n          \/\/ \u2705 Part 3\n          if(mode===\"full\" || mode===\"p3\"){\n            out.push({\n              part:\"Part 3\",\n              meta:\"Transition\",\n              prompt:\"Now I\u2019d like to discuss some more general questions.\",\n              model:\"\",\n              needsAnswer:false\n            });\n\nout.push({ part:\"Part 3\", meta:\"Money and young people\", prompt:\"First, let\u2019s talk about money and young people.\", model:\"\", needsAnswer:false });\nP3.filter(x=>x.group===\"Money and young people\").forEach((it,i)=> out.push({\n  part:\"Part 3\",\n  meta:\"Money and young people \u2022 Q\"+(i+1),\n  prompt:it.q,\n  model:it.model,\n  needsAnswer:true\n}));\n\nout.push({ part:\"Part 3\", meta:\"Money and society\", prompt:\"Now, let\u2019s move on to money and society.\", model:\"\", needsAnswer:false });\nP3.filter(x=>x.group===\"Money and society\").forEach((it,i)=> out.push({\n  part:\"Part 3\",\n  meta:\"Money and society \u2022 Q\"+(i+1),\n  prompt:it.q,\n  model:it.model,\n  needsAnswer:true\n}));\n\n\n          }\n\n          out.push({\n            part:\"End\",\n            meta:\"Finish\",\n            prompt:\"Thank you. That is the end of the speaking test.\",\n            model:\"\",\n            needsAnswer:false\n          });\n\n          return out;\n        }\n\n        function getActiveMode(){\n          const activeBtn = root.querySelector('[data-action=\"mode\"].is-active');\n          return activeBtn ? (activeBtn.getAttribute(\"data-mode\") || \"full\") : \"full\";\n        }\n\n        function askNext(){\n          if(!running) return;\n\n          idx++;\n          if(idx >= flow.length){\n            running = false;\n            awaitingAnswer = false;\n            addBubble(\"examiner\", \"Test finished.\");\n            speakExaminer(\"Thank you. That is the end of the speaking test.\");\n            return;\n          }\n\n          const card = flow[idx];\n\n          \/\/ full-test: update instruction panel while moving\n          const activeMode = getActiveMode();\n          if(activeMode === \"full\"){\n            if(card.part === \"Part 1\") renderPartInfo(\"p1\", \"Now: Part 1\");\n            else if(card.part === \"Part 2\") renderPartInfo(\"p2\", \"Now: Part 2\");\n            else if(card.part === \"Part 3\") renderPartInfo(\"p3\", \"Now: Part 3\");\n          }\n\n          setCurrentCard(card);\n\n          addBubble(\"examiner\", card.prompt);\n\nawaitingAnswer = !!card.needsAnswer;\n\n\/\/ \u2705 If it's an instruction\/transition card, WAIT for speech to finish, THEN continue\nif(!awaitingAnswer){\n  const delayAfterSpeech =\n    (typeof card.autoDelay === \"number\") ? card.autoDelay : 350; \/\/ small natural pause after speech\n\n  speakExaminerWait(card.prompt, ()=>{\n    setTimeout(()=>{ if(running) askNext(); }, delayAfterSpeech);\n  });\n\n} else {\n  \/\/ \u2705 For real questions, also ensure the whole question is spoken (but do NOT auto-advance)\n  speakExaminerWait(card.prompt, ()=>{ \/* ready for student *\/ });\n\n  currentAnswerText = \"\";\n  $('[data-el=\"currentAnswer\"]').textContent = \"\";\n  $('[data-el=\"ansWords\"]').textContent = \"0\";\n}\n\n        }\n\n        function repeatQ(){\n          if(idx < 0 || idx >= flow.length) return;\n          const card = flow[idx];\n          addBubble(\"examiner\", \"(Repeat) \" + card.prompt);\n          speakExaminerWait(card.prompt, ()=>{});\n\n        }\n\n        function showModel(){\n          if(idx < 0 || idx >= flow.length) return;\n          const card = flow[idx];\n          if(!card.model) return;\n          addBubble(\"examiner\", \"Sample answer:\\n\" + card.model);\n        }\n\n        \/\/ ===== Recognition (continuous) =====\n        const SR = window.SpeechRecognition || window.webkitSpeechRecognition;\n        let recognition = null;\n        let recognizing = false;\n        let manualStop = false;\n\n        let lastResultTs = 0;\n        let silenceInterval = null;\n\n        function getSilenceMs(){\n          const base = Number($('[data-el=\"silenceSec\"]').value || 6);\n          const part = (flow[idx] && flow[idx].part) ? flow[idx].part : \"\";\n\n          let minSilence = 5;         \/\/ Part 1 \/ general\n          if(part === \"Part 2\") minSilence = 10;\n          if(part === \"Part 3\") minSilence = 7;\n\n          const sec = clamp(Math.max(base, minSilence), 2, 20);\n          return sec * 1000;\n        }\n\n        function getMinAnswerMs(){\n          const part = (flow[idx] && flow[idx].part) ? flow[idx].part : \"\";\n          if(part === \"Part 2\") return 45000;\n          if(part === \"Part 3\") return 12000;\n          if(part === \"Part 1\") return 6000;\n          return 5000;\n        }\n\n        function startSilenceWatcher(){\n          stopSilenceWatcher();\n          silenceInterval = setInterval(()=>{\n            if(!autoAdvance) return;\n            if(!running) return;\n            if(!recognizing) return;\n            if(!awaitingAnswer) return;\n\n            const typedNow = ($('[data-el=\"currentAnswer\"]').textContent||\"\").trim();\n            if(!currentAnswerText.trim() && !typedNow) return;\n\n            const now = Date.now();\n            const silenceMs = getSilenceMs();\n\n            if(lastResultTs && (now - lastResultTs) >= silenceMs){\n              const answeredLongEnough = (answerStartTs && (now - answerStartTs) >= getMinAnswerMs());\n              if(answeredLongEnough){\n                if(now - lastAutoAdvanceTs > 1200){\n                  lastAutoAdvanceTs = now;\n                  finalizeAnswerAndAdvance(true);\n                }\n              }\n            }\n          }, 250);\n        }\n\n        function stopSilenceWatcher(){\n          if(silenceInterval){\n            clearInterval(silenceInterval);\n            silenceInterval = null;\n          }\n        }\n\n        function norm(text){\n          return String(text||\"\")\n            .toLowerCase()\n            .replace(\/[^\\w\\s']\/g,\" \")\n            .replace(\/\\s+\/g,\" \")\n            .trim();\n        }\n\n        function containsStopCommand(text){\n          const t = \" \" + norm(text) + \" \";\n          return (\n            t.includes(\" bye \") ||\n            t.includes(\" goodbye \") ||\n            t.includes(\" good bye \") ||\n            t.includes(\" stop \")\n          );\n        }\n\n        function initSpeech(){\n          if(!SR){\n            setMicPill(\"\ud83c\udf99 Mic: Not supported\", false);\n            const supportBox = $('[data-el=\"supportBox\"]');\n            if(supportBox) supportBox.style.display = \"block\";\n            return;\n          }\n          setMicPill(\"\ud83c\udf99 Mic: Ready\", true);\n\n          recognition = new SR();\n          recognition.lang = \"en-US\";\n          recognition.continuous = true;\n          recognition.interimResults = false;\n\n          recognition.onstart = ()=>{\n            recognizing = true;\n            $('[data-el=\"recState\"]').textContent = \"ON\";\n            setMicPill(\"\ud83c\udf99 Recording\u2026\", true);\n            root.querySelector('[data-action=\"start-rec\"]').disabled = true;\n            root.querySelector('[data-action=\"stop-rec\"]').disabled = false;\n            startSilenceWatcher();\n          };\n\n          recognition.onresult = (event)=>{\n            let chunk = \"\";\n            for(let i=event.resultIndex;i<event.results.length;i++){\n              chunk += (event.results[i][0].transcript || \"\") + \" \";\n            }\n            chunk = chunk.trim();\n            if(!chunk) return;\n\n            lastResultTs = Date.now();\n\n            if(containsStopCommand(chunk)){\n              addBubble(\"student\", chunk);\n              stopRecording(false);\n              running = false;\n              awaitingAnswer = false;\n              addBubble(\"examiner\", \"Thank you. The test is finished. Goodbye!\");\n              speakExaminer(\"Thank you. The test is finished. Goodbye!\");\n              return;\n            }\n\n            if(awaitingAnswer){\n              currentAnswerText = (currentAnswerText ? (currentAnswerText + \" \" + chunk) : chunk).replace(\/\\s+\/g,\" \").trim();\n              $('[data-el=\"currentAnswer\"]').textContent = currentAnswerText;\n\n              const wc = currentAnswerText.trim() ? currentAnswerText.trim().split(\/\\s+\/).length : 0;\n              $('[data-el=\"ansWords\"]').textContent = String(wc);\n            }\n          };\n\n          recognition.onend = ()=>{\n            recognizing = false;\n            $('[data-el=\"recState\"]').textContent = \"OFF\";\n            setMicPill(\"\ud83c\udf99 Stopped\", true);\n            root.querySelector('[data-action=\"start-rec\"]').disabled = false;\n            root.querySelector('[data-action=\"stop-rec\"]').disabled = true;\n            stopSilenceWatcher();\n\n            if(running && !manualStop){\n              try{ recognition.start(); }catch(e){}\n            }\n            manualStop = false;\n          };\n\n          recognition.onerror = ()=>{\n            recognizing = false;\n            $('[data-el=\"recState\"]').textContent = \"OFF\";\n            setMicPill(\"\ud83c\udf99 Mic error\", false);\n            root.querySelector('[data-action=\"start-rec\"]').disabled = false;\n            root.querySelector('[data-action=\"stop-rec\"]').disabled = true;\n            stopSilenceWatcher();\n          };\n        }\n\/\/ \u2705 Robot \u201cHow to use\u201d script (spoken before the test starts)\nconst HOW_TO_SPEAK = [\n  \"Before we start, here is a quick guide to use the buttons.\",\n  \"Step 1. Click Full test, Part 1, Part 2, or Part 3.\",\n  \"Step 2. Click Begin the Test. The robot shows the first question.\",\n  \"Step 3. Click Start Recording and speak. Click Stop Recording anytime.\",\n  \"Step 4. When you finish, click Next Question. Your answer is saved into the full test transcript.\",\n  \"Step 5. Click Sample Answer to view an example response for the current question.\",\n  \"Step 6. After answering several questions, click Band Score to get practice feedback. You can adjust the pronunciation self rating slider if needed.\",\n  \"Okay. Let\u2019s begin.\"\n];\n\n\/\/ \u2705 Speak lines in order (waits for each sentence to finish)\nfunction speakSequence(lines, done){\n  \/\/ Start a NEW guide run; any old run becomes invalid\n  const myGuideRun = ++guideRunId;\n  guideIsPlaying = true;\n\n  if(!speechOk || !ttsEnabled){\n    guideIsPlaying = false;\n    done && done();\n    return;\n  }\n\n  stopVoice(); \/\/ cancel anything currently speaking\n\n  const list = (lines || []).filter(Boolean);\n  let i = 0;\n\n  const speakNext = ()=>{\n    \/\/ \u2705 If user canceled (mode change \/ next \/ reset), stop immediately\n    if(myGuideRun !== guideRunId || !guideIsPlaying){\n      guideIsPlaying = false;\n      return;\n    }\n\n    if(i >= list.length){\n      guideIsPlaying = false;\n      done && done();\n      return;\n    }\n\n    const u = new SpeechSynthesisUtterance(String(list[i++]));\n    u.lang = (examinerVoice && examinerVoice.lang) ? examinerVoice.lang : \"en-US\";\n    if(examinerVoice) u.voice = examinerVoice;\n    u.rate = 1.0;\n    u.pitch = 1.0;\n\n    u.onend = ()=> speakNext();\n    u.onerror = ()=> speakNext();\n\n    try{ window.speechSynthesis.speak(u); }catch(e){ speakNext(); }\n  };\n\n  speakNext();\n}\n\n\n        function startRecording(){\n          if(!recognition) return;\n          manualStop = false;\n          try{ recognition.start(); }catch(e){}\n        }\n\n        \/\/ (stopRecording is declared above)\n\n        function finalizeAnswerAndAdvance(auto){\n          if(!running) return;\n\n          if(!awaitingAnswer){\n            askNext();\n            return;\n          }\n\n          const typed = ($('[data-el=\"currentAnswer\"]').textContent || \"\").trim();\n          const finalAnswer = (typed || currentAnswerText || \"\").trim();\n\n          if(!finalAnswer){\n            if(!auto) askNext();\n            return;\n          }\n\n          addBubble(\"student\", finalAnswer.length > 320 ? (finalAnswer.slice(0,317) + \"\u2026\") : finalAnswer);\n\n          const q = (flow[idx] && flow[idx].prompt) ? flow[idx].prompt : \"\";\n          fullTranscriptText += (fullTranscriptText ? \"\\n\\n\" : \"\") + \"Q: \" + q + \"\\nA: \" + finalAnswer;\n          $('[data-el=\"fullTranscript\"]').textContent = fullTranscriptText;\n\n          currentAnswerText = \"\";\n          $('[data-el=\"currentAnswer\"]').textContent = \"\";\n          $('[data-el=\"ansWords\"]').textContent = \"0\";\n          awaitingAnswer = false;\n\n          setTimeout(()=>{ if(running) askNext(); }, auto ? 450 : 250);\n        }\n\n        \/\/ ===== Band scoring (unchanged logic) =====\n        function tokenize(text){\n          return (text||\"\").toLowerCase().replace(\/[^a-z0-9\\s']\/g,\" \").split(\/\\s+\/).filter(Boolean);\n        }\n        const STOP = new Set((\"a an the and or but so because if when while where which that to of in on at for with without from as is are was were be been being do does did have has had will would can could should may might i you we they he she it my your our their me him her us them this these those there here just really very\").split(\" \"));\n        function lexicalDiversity(tokens){\n          const content=tokens.filter(t=>!STOP.has(t));\n          const total=content.length||1;\n          return (new Set(content).size)\/total;\n        }\n        function countFillers(tokens){\n          const ph=[[\"um\"],[\"uh\"],[\"erm\"],[\"like\"],[\"you\",\"know\"],[\"sort\",\"of\"],[\"kind\",\"of\"],[\"actually\"]];\n          let c=0;\n          for(let i=0;i<tokens.length;i++){\n            for(const p of ph){\n              let ok=true;\n              for(let j=0;j<p.length;j++){ if(tokens[i+j]!==p[j]){ ok=false; break; } }\n              if(ok) c++;\n            }\n          }\n          return c;\n        }\n        function grammarSignals(text){\n          const t=(text||\"\").toLowerCase();\n          return {\n            hasPast:\/\\b\\w+ed\\b\/.test(t),\n            hasFuture:\/\\bwill\\b|\\bgoing to\\b\/.test(t),\n            hasPerfect:\/\\b(have|has|had)\\b\/.test(t),\n            modals:(t.match(\/\\b(would|could|should|might|may)\\b\/g)||[]).length,\n            linkers:(t.match(\/\\b(however|although|because|therefore|for example|for instance|on the other hand|whereas|despite|in addition)\\b\/g)||[]).length\n          };\n        }\n        function sentenceStats(text){\n          const parts=(text||\"\").split(\/[.!?]+\/).map(s=>s.trim()).filter(Boolean);\n          const lens=parts.map(s=>tokenize(s).length).filter(n=>n>0);\n          const count=parts.length||0;\n          const avg=lens.length?(lens.reduce((a,b)=>a+b,0)\/lens.length):0;\n          return {count,avg};\n        }\n        function clearScore(){\n          $('[data-el=\"bandOut\"]').textContent=\"\u2014\";\n          $('[data-el=\"mFlu\"]').textContent=\"\u2014\";\n          $('[data-el=\"mLex\"]').textContent=\"\u2014\";\n          $('[data-el=\"mGram\"]').textContent=\"\u2014\";\n          $('[data-el=\"mPron\"]').textContent=\"\u2014\";\n          $('[data-el=\"nFlu\"]').textContent=\"\";\n          $('[data-el=\"nLex\"]').textContent=\"\";\n          $('[data-el=\"nGram\"]').textContent=\"\";\n          $('[data-el=\"nPron\"]').textContent=\"\";\n          const tips = $('[data-el=\"tips\"]');\n          tips.style.display=\"none\";\n          tips.innerHTML=\"\";\n        }\n        function scoreNow(){\n          const text = (fullTranscriptText || \"\").trim();\n          if(!text){ alert(\"No transcript yet. Answer at least one question first.\"); return; }\n\n          const tokens = tokenize(text);\n          const words = tokens.length;\n          const minutes = Math.max(1, words \/ 130);\n          const wpm = words \/ minutes;\n\n          const fillerPerMin = countFillers(tokens)\/minutes;\n          const ttr = lexicalDiversity(tokens);\n          const gs = grammarSignals(text);\n          const sent = sentenceStats(text);\n          const pronSelf = Number($('[data-el=\"pronSelf\"]').value || 0);\n\n          let flu=6.0;\n          if(wpm>=120 && wpm<=170) flu+=1.0;\n          else if(wpm>=100 && wpm<120) flu+=0.4;\n          else if(wpm<85) flu-=1.0;\n          if(fillerPerMin<=2) flu+=0.5;\n          else if(fillerPerMin>5) flu-=0.9;\n          if(gs.linkers>=2) flu+=0.4;\n          if(sent.count>=3 && sent.avg>=9) flu+=0.3;\n          flu=clamp(flu,0,9);\n\n          let lex=6.0;\n          const lenF=clamp(words\/80,0.6,1.0);\n          if(ttr>=0.55) lex+=1.2*lenF;\n          else if(ttr>=0.45) lex+=0.6*lenF;\n          else if(ttr<0.35) lex-=0.8*lenF;\n          if(gs.linkers>=2) lex+=0.3;\n          lex=clamp(lex,0,9);\n\n          let gram=6.0;\n          let variety=0;\n          if(gs.hasPast) variety++;\n          if(gs.hasFuture) variety++;\n          if(gs.hasPerfect) variety++;\n          if(gs.modals>=1) variety++;\n          if(variety>=3) gram+=0.8;\n          else if(variety===2) gram+=0.4;\n          else if(variety===0) gram-=0.8;\n          if(sent.avg>0 && sent.avg<7) gram-=0.6;\n          gram=clamp(gram,0,9);\n\n          const pron = clamp(pronSelf,0,9);\n          const overall = round1((flu+lex+gram+pron)\/4);\n\n          $('[data-el=\"bandOut\"]').textContent = overall;\n          $('[data-el=\"mFlu\"]').textContent = round1(flu);\n          $('[data-el=\"mLex\"]').textContent = round1(lex);\n          $('[data-el=\"mGram\"]').textContent = round1(gram);\n          $('[data-el=\"mPron\"]').textContent = round1(pron);\n\n          $('[data-el=\"nFlu\"]').textContent = \"WPM~ \" + Math.round(wpm) + \" \u2022 Fillers\/min: \" + round1(fillerPerMin);\n          $('[data-el=\"nLex\"]').textContent = \"Lexical variety (TTR): \" + round1(ttr);\n          $('[data-el=\"nGram\"]').textContent = \"Sentences: \" + sent.count + \" \u2022 Avg length: \" + round1(sent.avg);\n          $('[data-el=\"nPron\"]').textContent = \"Self-rated pronunciation\";\n\n          const tipsEl = $('[data-el=\"tips\"]');\n          const tips = [];\n          if(words<40) tips.push(\"Speak longer and develop your ideas with reasons and examples.\");\n          if(fillerPerMin>5) tips.push(\"Reduce fillers (um\/uh\/like). Pause silently instead.\");\n          if(gs.linkers<2) tips.push(\"Use more linking words: because, however, for example, in addition.\");\n          if(variety<2) tips.push(\"Show more grammar range: past, future, modals, present perfect.\");\n          if(sent.avg<7) tips.push(\"Combine short sentences into complex ones.\");\n\n          tipsEl.style.display=\"block\";\n          tipsEl.innerHTML = \"<strong>Feedback:<\/strong><ul>\" +\n            (tips.length ? tips : [\"Good work! Add clearer examples and use more precise vocabulary.\"])\n            .map(t=>\"<li>\"+escapeHtml(t)+\"<\/li>\").join(\"\") +\n            \"<\/ul>\";\n        }\n\n        \/\/ ===== Mode handling =====\n        function setActiveMode(mode){\n          root.querySelectorAll('[data-action=\"mode\"]').forEach(b=>{\n            b.classList.remove(\"is-active\",\"icte-btn--primary\");\n            b.classList.add(\"icte-btn--ghost\");\n          });\n          const btn = root.querySelector('[data-action=\"mode\"][data-mode=\"'+mode+'\"]');\n          if(btn){\n            btn.classList.add(\"is-active\",\"icte-btn--primary\");\n            btn.classList.remove(\"icte-btn--ghost\");\n          }\n          setModePill(mode);\n        }\n        setActiveMode(\"full\");\n        renderPartInfo(\"p1\");\n\n        \/\/ ===== Buttons wiring =====\n        function bind(action, fn){\n          const el = root.querySelector('[data-action=\"'+action+'\"]');\n          if(el) el.addEventListener(\"click\", fn);\n        }\n\n        root.querySelectorAll('[data-action=\"mode\"]').forEach(btn=>{\n  btn.addEventListener(\"click\", ()=>{\n    cancelGuideSpeech(); \/\/ \u2705 stop reading instructions if user switches parts\n\n    const mode = btn.getAttribute(\"data-mode\") || \"full\";\n    setActiveMode(mode);\n    renderPartInfo(modeToInfoKey(mode));\n\n    if(running){\n      chat.innerHTML = \"\";\n      clearScore();\n      currentAnswerText = \"\";\n      fullTranscriptText = \"\";\n      $('[data-el=\"currentAnswer\"]').textContent = \"\";\n      $('[data-el=\"fullTranscript\"]').textContent = \"\";\n      $('[data-el=\"ansWords\"]').textContent = \"0\";\n      idx = -1;\n\n      flow = buildFlow(mode);\n      running = true;\n      awaitingAnswer = false;\n\n      \/\/ \u2705 Start immediately after switching modes (no guide re-read)\n      askNext();\n    }\n  });\n});\n\n\n        bind(\"toggle-auto\", ()=>{\n          autoAdvance = !autoAdvance;\n          refreshAutoPill();\n        });\n\n        bind(\"toggle-voice\", ()=>{\n          ttsEnabled = !ttsEnabled;\n          if(!ttsEnabled) stopVoice();\n          setTtsPill(\"\ud83d\udd0a Voice: \" + (ttsEnabled ? \"ON\" : \"OFF\"), true);\n        });\n\n        bind(\"stop-voice\", ()=>{ cancelGuideSpeech(); });\n\n        bind(\"begin\", ()=>{\n  const mode = getActiveMode();\n  setModePill(mode);\n  renderPartInfo(modeToInfoKey(mode)); \/\/ show matching instructions\n\n  showLoader(false);\n  chat.innerHTML = \"\";\n  clearScore();\n\n  currentAnswerText = \"\";\n  fullTranscriptText = \"\";\n  $('[data-el=\"currentAnswer\"]').textContent = \"\";\n  $('[data-el=\"fullTranscript\"]').textContent = \"\";\n  $('[data-el=\"ansWords\"]').textContent = \"0\";\n  $('[data-el=\"needHint\"]').textContent = \"\";\n  idx = -1;\n\n  flow = buildFlow(mode);\n  running = true;\n  root.querySelector('[data-action=\"repeat\"]').disabled = false;\n\n  \/\/ \u2705 Speak instructions first, THEN start the test\n  speakSequence(HOW_TO_SPEAK, ()=>{\n    if(running) askNext();\n  });\n});\n\n\n        bind(\"repeat\", repeatQ);\n        bind(\"model\", showModel);\n\n        bind(\"reset\", ()=>{\n  cancelGuideSpeech(); \/\/ \u2705 stop guide if playing\n          running = false;\n          awaitingAnswer = false;\n          idx = -1;\n          chat.innerHTML = \"\";\n          currentAnswerText = \"\";\n          fullTranscriptText = \"\";\n          $('[data-el=\"currentAnswer\"]').textContent = \"\";\n          $('[data-el=\"fullTranscript\"]').textContent = \"\";\n          $('[data-el=\"ansWords\"]').textContent = \"0\";\n          $('[data-el=\"currentMeta\"]').textContent = \"\u2014\";\n          $('[data-el=\"currentQ\"]').textContent = \"\u2014\";\n          $('[data-el=\"needHint\"]').textContent = \"\";\n          clearScore();\n          stopVoice();\n          stopRecording(true);\n          renderPartInfo(modeToInfoKey(getActiveMode()));\n        });\n\n        bind(\"start-rec\", ()=> startRecording());\n        bind(\"stop-rec\", ()=> stopRecording(false));\n        bind(\"next\", ()=>{\n  \/\/ \u2705 If the guide is speaking, stop it and start the test immediately\n  if(guideIsPlaying){\n    cancelGuideSpeech();\n    if(running && idx === -1){\n      askNext(); \/\/ start first card right away\n    }\n    return;\n  }\n\n  \/\/ normal behavior during the test\n  finalizeAnswerAndAdvance(false);\n});\n\n        bind(\"clear-current\", ()=>{\n          currentAnswerText = \"\";\n          $('[data-el=\"currentAnswer\"]').textContent = \"\";\n          $('[data-el=\"ansWords\"]').textContent = \"0\";\n        });\n        bind(\"score\", scoreNow);\n\n        $('[data-el=\"pronSelf\"]').addEventListener(\"input\", function(){\n          $('[data-el=\"pronVal\"]').textContent = this.value;\n        });\n\n        \/\/ ===== Init mic + TTS =====\n        initSpeech();\n        initTts();\n      });\n    <\/script>\n\n  <\/section>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Test 1 Test 2 Test 3 Test 4 Listening Reading Writing IELTS Speaking Robot Examiner Practice with a Robot Examiner<\/p>\n","protected":false},"author":1,"featured_media":253,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"colormag_page_layout":"default_layout","footnotes":""},"categories":[25,29,32],"tags":[],"class_list":["post-511","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ielts","category-speaking","category-test-3"],"_links":{"self":[{"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/posts\/511","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/comments?post=511"}],"version-history":[{"count":19,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/posts\/511\/revisions"}],"predecessor-version":[{"id":572,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/posts\/511\/revisions\/572"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/media\/253"}],"wp:attachment":[{"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/media?parent=511"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/categories?post=511"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/tags?post=511"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}