:root {
  --bg: #fbf9f4;
  --surface: #ffffff;
  --line: #e7e3d8;
  --ink: #2c2c2a;
  --muted: #6f6d65;
  --faint: #9b988f;
  --accent: #185fa5;
  --accent-bg: #e6f1fb;
  --amber: #854f0b;
  --amber-bg: #faeeda;
  --green: #3b6d11;
  --green-bg: #eaf3de;
  --radius: 12px;
  --font: "Segoe UI", system-ui, -apple-system, "PingFang TC", "Microsoft JhengHei", sans-serif;
  --mono: "Cascadia Code", "Consolas", ui-monospace, monospace;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--ink); font-family: var(--font); line-height: 1.6; }
a { color: inherit; text-decoration: none; }
.sitenav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 13px;
  background: var(--surface); border-bottom: 1px solid var(--line); padding: 8px 18px; position: sticky; top: 0; z-index: 6; }
.sitenav .navlink { color: var(--muted); padding: 4px 10px; border-radius: 99px; }
.sitenav .navlink:hover { background: var(--bg); color: var(--ink); }
.sitenav .navlink.active { background: var(--accent-bg); color: var(--accent); }
.sitenav .navwho { margin-left: auto; color: var(--faint); }
/* Remaining AI-generation credits. Amber while there are some left (a budget, not an alarm),
   red once spent — at which point the 易/中/難 buttons are disabled too. */
.sitenav .navquota { color: var(--amber); background: var(--amber-bg); border-radius: 99px;
  padding: 4px 10px; white-space: nowrap; }
.sitenav .navquota.empty { color: #9a2c2c; background: #f9e4e4; }
.sitenav .navlogout { border: 1px solid var(--line); background: var(--surface); color: var(--muted);
  border-radius: 99px; padding: 4px 12px; font: inherit; cursor: pointer; }
.sitenav .navlogout:hover { border-color: var(--accent); color: var(--accent); }

.topbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  background: var(--surface); border-bottom: 1px solid var(--line); padding: 12px 18px; position: sticky; top: 40px; z-index: 5; }
.brand { display: flex; align-items: center; gap: 9px; font-size: 16px; white-space: nowrap; }
/* 站名在手機上收成「Silent Tutor」：完整名稱留在瀏覽器分頁與登入頁。頂欄本身允許換行，
   窄到連收短的站名加上計數都排不下時，讓 brand 與 stats 各佔一行，而不是把字擠成兩三行。 */
@media (max-width: 560px) { .brand-zh { display: none; } }
.brand-badge { width: 30px; height: 30px; border-radius: 9px; background: var(--accent-bg); color: var(--accent);
  display: grid; place-items: center; font-size: 13px; }
.stats { display: flex; align-items: center; gap: 16px; font-size: 14px; color: var(--muted); }
.stat { display: inline-flex; align-items: center; gap: 5px; }
.stat .ti { font-size: 17px; color: var(--amber); }
.stat.progress { gap: 8px; color: var(--muted); }
.bar { width: 90px; height: 8px; border-radius: 99px; background: var(--bg); display: inline-block; overflow: hidden; }
.bar > span { display: block; height: 100%; background: var(--accent); border-radius: 99px; }
.lesson-title { display: flex; flex-direction: column; line-height: 1.3; text-align: center; }
.lesson-title .muted { font-size: 12px; color: var(--faint); }
.back { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 14px; }
.streak-meter { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--green); }

.map { max-width: 620px; margin: 0 auto; padding: 22px 18px 60px; }
.chapter { font-size: 12px; color: var(--faint); margin: 22px 0 10px 4px; }
.node { display: flex; align-items: center; gap: 14px; margin: 0 0 14px; position: relative; }
.node + .node::before { content: ""; position: absolute; left: 23px; top: -14px; height: 14px;
  border-left: 2px dotted var(--line); }
.node-circle { width: 48px; height: 48px; border-radius: 50%; flex: none; display: grid; place-items: center;
  background: var(--surface); border: 1px solid var(--line); color: var(--faint); font-size: 21px; }
.node.done .node-circle { background: var(--green-bg); border-color: #c0dd97; color: var(--green); }
.node.current .node-circle { background: var(--amber-bg); border: 2px solid #efb45a; color: var(--amber); }
.node-body { flex: 1; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 12px 15px; display: flex; align-items: center; justify-content: space-between; }
.node.current .node-body { border-color: #efb45a; }
.node.soon .node-label { color: var(--faint); }
.node-label { font-size: 15px; }
.stars { color: var(--green); letter-spacing: 2px; }
.badge { font-size: 12px; padding: 2px 10px; border-radius: 99px; background: var(--amber-bg); color: var(--amber); }
.node.soon .badge { background: var(--bg); color: var(--faint); }

/* 820px ≈ 45 個中文字一行，還在舒適閱讀範圍，但長程式碼（AI 生成的第 3 階）少橫捲很多。
   手機吃不到這條（視窗本來就窄於 820），走的是 padding 滿版。地圖頁維持 620：那是一串節點，
   拉寬只是把每個節點抻長。 */
.lesson { max-width: 820px; margin: 0 auto; padding: 22px 18px 60px; }
.card-panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
/* Long code, long answers and long 正解 must all stay readable: the answer area wraps under the
   code line instead of running off the panel (which is overflow:hidden). */
.code-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 11px 14px; border-top: 1px solid var(--line); flex-wrap: wrap; }
.code-row:first-child { border-top: none; }
.code-row.predict { background: var(--accent-bg); }
/* Code-displaying elements all use --mono (Cascadia Code), which ligates "!=" -> "≠", ">=" -> "≥",
   "<=" -> "≤" by default — Python doesn't accept those Unicode symbols, so showing them would
   teach students to type something that's a syntax error. Both properties are needed: some
   browsers/fonts implement these via "liga", others via "calt" (contextual alternates). */
.code, .chip, input, .row-right textarea, .feedback code, .explain-code, .inline-fb code, .history-chip {
  font-variant-ligatures: none;
  font-feature-settings: "liga" 0, "calt" 0;
}
.code { font-family: var(--mono); font-size: 14px; white-space: pre; max-width: 100%; overflow-x: auto; }
.row-right { display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: flex-end;
  gap: 9px; flex: 1 1 auto; min-width: 0; }
.tag { font-size: 11px; color: var(--faint); }
.chip { background: var(--accent-bg); color: var(--accent); padding: 2px 10px; border-radius: 6px; font-family: var(--mono); font-size: 13px;
  max-width: 100%; white-space: pre-wrap; overflow-wrap: anywhere; }
/* A submitted answer is final text, not an editable field — shown as a chip so it wraps in full. */
.chip.answer { background: var(--surface); color: var(--ink); border: 1px solid var(--line); }
.chip.answer.wrong { background: #fdeeee; color: #a32d2d; border-color: #f0c9c9; }
.chip.answer.blank { color: var(--faint); font-family: var(--font); }
input { font: inherit; font-family: var(--mono); width: 110px; padding: 6px 10px; border: 1px solid var(--line);
  border-radius: 8px; background: var(--surface); max-width: 100%; }
/* The answer field is a textarea, not an input: a predict step whose result is print output can be
   several lines (`for i in range(3): print(i)`), and a single-line input cannot hold a newline at
   all. It starts one row tall and grows with the answer. Scoped to .row-right so the admin pages'
   own textareas are untouched. */
.row-right textarea { font: inherit; font-family: var(--mono); width: 110px; max-width: 100%;
  padding: 6px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface);
  line-height: 1.45; resize: vertical; overflow-y: auto; }
/* While typing, grow the box with the answer where the browser supports it (Chromium 123+). */
@supports (field-sizing: content) {
  .row-right input, .row-right textarea { field-sizing: content; width: auto; min-width: 110px; }
}
input:focus, .row-right textarea:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
button { font: inherit; cursor: pointer; padding: 7px 13px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface); display: inline-flex; align-items: center; gap: 6px; }
button:hover { background: var(--bg); }
button.check { background: var(--accent); color: #fff; border-color: var(--accent); }
/* Teacher-only 停用, next to 送出 on a generated card. Deliberately quiet — it is a destructive
   action sitting beside the button students press, so it must not compete for the same click. */
button.retire-btn { padding: 5px 10px; font-size: 12px; color: var(--muted); }
button.retire-btn:hover:not(:disabled) { color: #a32d2d; border-color: #e0b4b4; }
button.retire-btn:disabled { cursor: default; opacity: 0.7; }
/* The card stays readable and answerable once retired; the dimming just says "this one is out". */
.exercise.retired { opacity: 0.65; }
.feedback { min-height: 24px; margin: 12px 2px; font-size: 14px; display: flex; align-items: center; gap: 7px; }
.feedback.ok { color: var(--green); }
.feedback.bad { color: #a32d2d; }
.feedback code { font-family: var(--mono); background: var(--surface); border: 1px solid var(--line); padding: 1px 6px; border-radius: 5px; }
.handoff { margin-top: 14px; display: flex; align-items: center; justify-content: space-between; gap: 10px;
  background: var(--accent-bg); border: 1px solid #b5d4f4; border-radius: var(--radius); padding: 13px 15px; color: var(--accent); font-size: 14px; }
.oj-btn { background: var(--accent); color: #fff; padding: 8px 14px; border-radius: 8px; display: inline-flex; align-items: center; gap: 7px; }
.actions { margin-top: 16px; display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.exec-note { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: var(--faint); }
/* Teacher-only: a multi-shape ladder's labels are what the card bank records, so reordering the
   shapes silently invalidates the balance. Warned where the editing actually happens. */
.shape-warn { margin: 0 0 8px; padding: 8px 10px; border-radius: 8px; font-size: 12px; line-height: 1.6;
  color: var(--amber); background: var(--amber-bg); border: 1px solid var(--amber); }
.shape-warn[hidden] { display: none; }
.empty { color: var(--muted); text-align: center; padding: 40px 0; }
#status { text-align: center; color: var(--faint); font-size: 13px; padding: 8px; }
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin: 0 0 18px; }
.tab { border: none; border-bottom: 2px solid transparent; border-radius: 0; background: none; padding: 8px 14px; color: var(--muted); font-size: 14px; }
.tab:hover { background: none; color: var(--ink); }
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.flow { display: flex; flex-direction: column; gap: 16px; }
.flow[hidden] { display: none; }
.explain { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; }
.explain-title { font-weight: 500; font-size: 15px; margin-bottom: 6px; }
.explain-body { color: var(--ink); font-size: 14px; line-height: 1.7; }
.explain-code { font-family: var(--mono); font-size: 13px; background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; margin: 10px 0 0; overflow-x: auto; white-space: pre; }
/* Demo box under each explanation: the snippet, an optional stdin field, 執行, and the console.
   Students see the snippet as a plain <pre>; only the teacher-only editor module (fetched from
   /api/playground/editor.js) swaps it for .pg-editable. */
.playground { margin-top: 10px; }
.playground .pg-code { margin-top: 0; }
textarea.pg-editable { display: block; width: 100%; font: inherit; font-family: var(--mono); font-size: 13px;
  line-height: 1.6; resize: none; overflow: hidden; white-space: pre; }
textarea.pg-editable:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.pg-stdin { margin-top: 8px; }
.pg-label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
.pg-stdin textarea { display: block; width: 100%; font: inherit; font-family: var(--mono); font-size: 13px;
  padding: 7px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); resize: vertical; }
.pg-stdin textarea:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.pg-bar { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
button.pg-run { padding: 5px 12px; font-size: 13px; color: var(--accent); border-color: #b5d4f4; background: var(--accent-bg); }
button.pg-run.stop { color: #a32d2d; border-color: #f0c9c9; background: #fdeeee; }
.pg-note { font-size: 12px; color: var(--muted); }
.pg-out { display: none; font-family: var(--mono); font-size: 13px; line-height: 1.5; background: #2c2c2a; color: #f2efe6;
  border-radius: 8px; padding: 10px 12px; margin: 8px 0 0; max-height: 40vh; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
.pg-out.shown { display: block; }
.pg-line { display: block; }
.pg-line.echo { color: #8fc8f5; }        /* a line consumed from 輸入資料, echoed like a terminal */
.pg-line.err { color: #ff9d9d; }
.pg-line.note { color: #a09d94; }        /* our own messages: 已停止, 逾時, 截斷… */
.inline-fb { font-size: 13px; display: inline-flex; align-items: center; flex-wrap: wrap; gap: 5px;
  max-width: 100%; min-width: 0; }
.inline-fb.ok { color: var(--green); }
.inline-fb.bad { color: #a32d2d; }
.inline-fb code { font-family: var(--mono); background: var(--surface); border: 1px solid var(--line); padding: 1px 6px; border-radius: 5px;
  max-width: 100%; white-space: pre-wrap; overflow-wrap: anywhere; }
/* Each knowledge point's practice sits inline in the walkthrough, under the explanation and cards
   that teach it — so it needs to read as its own block, not as more lesson text. Accent left edge
   + accent-tinted background (not var(--bg), which is the page colour and made it vanish) marks "this is the part you answer". */
.practice-section { display: flex; flex-direction: column; gap: 10px; background: var(--accent-bg);
  border: 1px solid #c5dcf2; border-left: 4px solid var(--accent);
  border-radius: var(--radius); padding: 12px 14px; }
.practice-section-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.practice-section-head h3 { color: var(--accent); }
.practice-section-head h3 { margin: 0; font-size: 15px; font-weight: 500; }
.practice-progress { font-size: 12px; color: var(--muted); white-space: nowrap; }
.practice-progress.mastered { color: var(--green); font-weight: 500; }
/* Teacher-only prompt viewer. A plain <details>, so it costs no overlay/focus-trap CSS and stays
   folded away from the lesson; only the type sizes need saying. */
.prompt-viewer { font-size: 12px; color: var(--muted); border-top: 1px dashed var(--line);
  padding-top: 8px; }
.prompt-viewer summary { cursor: pointer; }
.prompt-viewer .prompt-viewer-body { margin-top: 8px; display: flex; flex-direction: column; gap: 8px; }
.prompt-viewer .prompt-role { margin: 6px 0 2px; font-weight: 500; color: var(--fg); }
.prompt-viewer pre.code { max-height: 40vh; overflow: auto; white-space: pre-wrap;
  overflow-wrap: anywhere; font-size: 11px; }
.guide-editor { margin: 6px 0 10px; }
.guide-editor > summary { cursor: pointer; color: var(--muted); }
.guide-editor > textarea { margin-top: 6px; }
.guide-actions select { font: inherit; padding: 6px 8px; border: 1px solid var(--line);
  border-radius: 8px; background: var(--surface); color: var(--ink); }
.prompt-preview:not(:empty) { margin-top: 10px; padding: 8px 10px; border-radius: var(--radius);
  background: var(--accent-bg); }
.guide-editor textarea { width: 100%; font: inherit; line-height: 1.6; padding: 8px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);
  color: var(--fg); resize: vertical; }
.guide-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* One chip per difficulty rung (易/中/難) — the gate needs a correct answer at each. Shared by
   the sticky overview bar and each 練習題 section, so both read as the same thing. */
.rung-chip { display: inline-block; font-style: normal; font-size: 11px; line-height: 1.5;
  padding: 1px 7px; border-radius: 99px; background: var(--bg); color: var(--faint);
  border: 1px solid var(--line); transition: background-color .15s, color .15s; }
.rung-chip + .rung-chip { margin-left: 3px; }
/* Cleared rung: SOLID green, not a pale tint. --green-bg (#eaf3de) against --bg (#fbf9f4) are
   both near-white, so a student who had just answered correctly could not see anything change. */
.rung-chip.done { background: var(--green); color: #fff; border-color: var(--green); font-weight: 600; }

/* The overview bar jumps to whatever starts a knowledge point (an explain block, sometimes a
   card or a practice section) — every flow child is a potential jump target under two pinned
   bars, so keep them all clear of both. */
.flow > * { scroll-margin-top: var(--sticky-offset, 170px); }
/* Where a new knowledge point starts (set from JS). One knowledge point reads as one block —
   explanation, its cards, its practice — so widen the gap there and draw a hairline in the middle
   of it. The line is a pseudo-element rather than a border on the box itself: .explain is already
   a bordered card and a border-top would just thicken its own frame. */
.kp-start:not(:first-child) { margin-top: 48px; position: relative; }
.kp-start:not(:first-child)::before { content: ""; position: absolute; left: 0; right: 0;
  top: -24px; border-top: 2px solid var(--faint); }

/* Sticky overview of every knowledge point's 易/中/難 state, pinned at the top of the page so a
   student never has to scroll to see where they stand. `top` is set from JS to sit exactly under
   the sticky topbar. */
.mastery-bar { position: sticky; z-index: 4; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius); padding: 10px 12px;
  margin: 0 0 16px; }
.mastery-bar:empty { display: none; }
.mastery-bar-head { font-size: 12px; color: var(--muted); margin: 0 0 8px; }
.mastery-bar-head b { color: var(--ink); font-weight: 600; }
.mastery-bar-list { display: flex; flex-wrap: wrap; gap: 7px; max-height: 32vh; overflow-y: auto; }
.kp { display: inline-flex; align-items: center; gap: 6px; font: inherit; font-size: 12px;
  color: var(--ink); background: var(--surface); border: 1px solid var(--line);
  border-radius: 99px; padding: 3px 6px 3px 11px; cursor: pointer; }
.kp:hover { border-color: var(--accent); }
.kp.done { border-color: var(--green); }
.kp-label { white-space: nowrap; }
/* Teacher-only: the knowledge point's tag (new_construct) after its title, in the overview bar and
   the practice heading. Monospace and muted so it reads as an identifier, not as part of the title. */
.kp-code { margin-left: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px; color: var(--muted); }
.regen-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
/* "再來一題" is now a label in front of one button per rung (易/中/難) rather than a button itself. */
.regen-label { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; color: var(--muted); }
.regen-label.spent { color: #9a2c2c; }   /* same red as the exhausted nav chip */
.regen-btn { align-self: flex-start; background: var(--accent-bg); color: var(--accent); border-color: #b5d4f4; }
/* Fixed width so swapping the label for the spinner doesn't make the row jump. */
.regen-btn.rung { min-width: 52px; justify-content: center; }
.regen-btn:hover { background: #d8e9f9; }
.regen-btn:disabled { opacity: .6; cursor: default; }
/* Cleared rung: the SAME solid green as the .rung-chip.done state, plus a ✓ in front of the label
   (added in refreshBadge). The button is the status display now — there is no separate chip row
   next to it — so the two must not use two different colour languages for one fact. Blue is
   left meaning exactly one thing: practice still owed. */
.regen-btn.mastered { background: var(--green); color: #fff; border-color: var(--green); font-weight: 600; }
.regen-btn.mastered:hover { background: #34600f; border-color: #34600f; }
/* Fixed width above assumes a bare 易/中/難; the ✓ needs the room. */
.regen-btn.rung.mastered { min-width: 72px; }
.regen-slot:empty { display: none; }
.generated { border: 1px solid #b5d4f4; border-radius: var(--radius); padding: 12px; background: var(--accent-bg); }
.generated .card-panel { background: var(--surface); }
/* Not a flex row any more: the badge now holds a whole sentence (難度／全班答對率／編號 in front of
   the AI mark), and flex `gap` would push each text run apart and wrap them mid-phrase. */
.gen-badge { font-size: 11px; color: var(--accent); line-height: 1.7; margin: 0 0 10px 2px; }
.gen-badge .ti { margin-right: 3px; vertical-align: -1px; }
/* The provenance is reference information, not the point of the card — keep the numbers legible
   but the rest quiet. The id is monospace so it can be read out or pasted into the card bank. */
.gen-badge b { color: var(--ink); font-weight: 600; }
.gen-badge code { font-family: var(--mono); color: var(--muted); }
.regen-btn .ti-loader-2 { animation: regen-spin 1s linear infinite; }
.history-strip { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 8px 14px; border-top: 1px solid var(--line); font-size: 12px; }
.history-strip-label { color: var(--faint); display: inline-flex; align-items: center; gap: 4px; }
.history-chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 99px; font-family: var(--mono); }
.history-chip.ok { background: var(--green-bg); color: var(--green); }
.history-chip.bad { background: #fbe9e9; color: #a32d2d; }
.history-answered { opacity: .85; }
.history-answered .gen-badge { color: var(--faint); }
@keyframes regen-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; scroll-behavior: auto !important; } }
@media (max-width: 520px) { .stats { gap: 10px; } .bar { width: 56px; } }
/* The chips jump to that knowledge point's practice, the name to its explanation — show which. */
.kp-rungs:hover .rung-chip { border-color: var(--accent); }
