/* ============================================================
   NUS Coding Tutor — visual language of the NUS bridging course
   ------------------------------------------------------------
   Matched to the built lesson pages (system_py/*.html) so a
   checkpoint reads as part of the course rather than a separate
   product:

     Inter for prose, JetBrains Mono for code
     NUS navy #003D7C, NUS orange #FF6B35
     white ground with a very faint navy/orange radial wash
     cards: 135deg #f7fafc -> #fff, 2px rgba(48,105,152,.22),
            shadow 0 6px 20px rgba(48,105,152,.08)
     small uppercase section labels, 12px/700, .6px tracking
     run output: light-on-dark green, error light-on-dark red
     results: white with a 4px left rule, navy / green / red

   TEXT-FIRST. The course labels its controls with words, and
   keeps glyphs for results only — a tick or a cross on a marked
   answer. Buttons here say Run, Stop, Send. No icons.
   ============================================================ */

:root {
  /* NUS brand, taken from the lesson pages */
  --brand:        #003D7C;
  --brand-dark:   #002a58;
  --brand-mid:    #0052a3;
  --brand-soft:   #e8eff7;
  --brand-tint:   #f4f8fc;
  --brand-line:   rgba(48,105,152,.22);
  --brand-line-2: rgba(48,105,152,.35);

  --accent:       #FF6B35;
  --accent-soft:  #fff1ea;
  --accent-ink:   #b8410f;

  --bg:          #ffffff;
  --bg-subtle:   #f7fafc;
  --surface:     #ffffff;
  --surface-2:   #fbfcfd;

  --ink:    #1a202c;
  --ink-2:  #2c3e50;
  --muted:  #4a5568;
  --faint:  #718096;
  --line:   #e3e9f0;
  --line-strong: #cdd8e4;

  /* code + console, from the lesson pages' code cards */
  --code-bg:   #0f172a;
  --code-bg-2: #172033;
  --code-ink:  #e2e8f0;
  --code-line: #1e293b;

  --out-bg:  #052e16;
  --out-ink: #bbf7d0;
  --out-rule:#22c55e;
  --err-bg:  #450a0a;
  --err-ink: #fecaca;
  --err-rule:#ef4444;

  --ok:   #2ea36b;
  --ok-2: #2b8a3e;
  --warn: #f59f00;
  --err:  #c94f4f;

  --r-sm: 6px;
  --r:    8px;
  --r-lg: 10px;
  --r-pill: 999px;

  --shadow-1: 0 1px 2px rgba(48,105,152,.06);
  --shadow-2: 0 6px 20px rgba(48,105,152,.08);
  --shadow-3: 0 18px 44px rgba(15,23,42,.18);

  --t-fast: 120ms cubic-bezier(.2,0,.2,1);
  --t:      180ms cubic-bezier(.2,0,.2,1);

  --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: "JetBrains Mono", Menlo, Consolas, "Courier New", monospace;
}

* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
  font-family: var(--font-sans);
  color: var(--ink-2);
  background: var(--bg);
  display: flex; flex-direction: column;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  position: relative;
}
/* the lesson pages' background wash, at the same weight */
body::before {
  content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(circle at 20% 50%, rgba(0,61,124,.04) 0%, transparent 50%),
    radial-gradient(circle at 80% 80%, rgba(255,107,53,.04) 0%, transparent 50%);
}
button { font-family: inherit; }
input, textarea, select { font-family: inherit; color: inherit; }

/* small uppercase label, the course's section-label treatment */
.meta-label, .pane-title, .code-editor-label {
  font-size: 12px; font-weight: 700;
  letter-spacing: .6px; text-transform: uppercase;
  color: var(--faint);
}

/* ---------- topbar ---------- */
.topbar {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: 20px;
  height: 60px; padding: 0 20px;
  background: var(--surface);
  border-bottom: 2px solid var(--brand);
  position: relative; z-index: 10;
}
.brand {
  display: flex; align-items: baseline; gap: 10px;
  font-weight: 700; font-size: 16px; letter-spacing: -.2px;
  color: var(--brand);
  white-space: nowrap;
}
.brand-mark {
  font-size: 11px; font-weight: 700;
  letter-spacing: .6px; text-transform: uppercase;
  color: #fff; background: var(--brand);
  padding: 3px 7px; border-radius: var(--r-sm);
  align-self: center;
}
.tag {
  display: inline-flex; align-items: center;
  padding: 3px 9px;
  font-size: 11px; font-weight: 700;
  letter-spacing: .5px; text-transform: uppercase;
  background: var(--accent-soft); color: var(--accent-ink);
  border: 1px solid rgba(255,107,53,.3);
  border-radius: var(--r-pill);
}
.topbar-right { display: flex; align-items: center; gap: 14px; }

.status {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px; font-size: 11.5px; font-weight: 600;
  color: var(--muted);
  background: var(--bg-subtle);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.status::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--ok); box-shadow: 0 0 0 3px rgba(46,163,107,.16);
}

/* ---------- tier switch (Easy / Medium / Hard) ---------- */
.tierbar {
  display: flex; align-items: center; gap: 12px;
  min-width: 0;
}
.tierbar .meta-label { white-space: nowrap; }
.tiers {
  display: inline-flex; padding: 3px;
  background: var(--bg-subtle);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}
.tier-btn {
  appearance: none; border: 0; background: transparent;
  padding: 7px 18px;
  font-size: 13px; font-weight: 600; color: var(--muted);
  border-radius: var(--r-sm); cursor: pointer; white-space: nowrap;
  transition: background var(--t-fast), color var(--t-fast), box-shadow var(--t-fast);
}
.tier-btn:hover { color: var(--brand); }
.tier-btn[aria-pressed="true"] {
  background: linear-gradient(135deg, var(--brand) 0%, var(--brand-mid) 100%);
  color: #fff;
  box-shadow: 0 2px 8px rgba(0,61,124,.24);
}
.tier-btn .tier-n {
  font-size: 11px; font-weight: 600; opacity: .68;
  margin-left: 6px; font-variant-numeric: tabular-nums;
}

/* ---------- select ---------- */
.select {
  appearance: none; -webkit-appearance: none;
  background: var(--surface) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'><path d='M3 4.5l3 3 3-3' stroke='%234a5568' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>") no-repeat right 10px center;
  background-size: 12px;
  border: 1.5px solid var(--brand-line);
  border-radius: var(--r);
  padding: 8px 32px 8px 12px;
  font-size: 13px; font-weight: 500; color: var(--ink);
  min-width: 220px; cursor: pointer;
  transition: border-color var(--t), box-shadow var(--t);
}
.select:hover { border-color: var(--brand-line-2); }
.select:focus {
  outline: none; border-color: var(--brand);
  box-shadow: 0 0 0 3px rgba(0,61,124,.14);
}
.select-sm { min-width: 260px; padding: 6px 30px 6px 10px; font-size: 12.5px; }

/* ---------- split layout ---------- */
/* The 14px gutter is no longer a flex gap: it is the drag handle
   (#split-resizer), which is exactly 14px wide. Same spacing as before, but
   now it is grabbable. --split-left is written by js/ui/resize.js; the
   fallback is the 56% the pane always had, so the page is unchanged until a
   student drags it. */
.split {
  flex: 1 1 auto; display: flex; min-height: 0;
  padding: 14px;
}
.pane {
  display: flex; flex-direction: column;
  min-width: 0; min-height: 0;
  background: var(--surface);
  border: 2px solid var(--brand-line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-2);
  overflow: hidden;
}
/* The left pane is the one that is sized; the right takes what is left.
   flex-basis 0 on the side pane matters: with `auto` its base size is its own
   content width, so a wide chat bubble ate into the free space and the left
   pane came out narrower than the width it had just been dragged to. */
.editor-pane { flex: 0 0 var(--split-left, 56%); }
.side-pane   { flex: 1 1 0; }
.chat-pane   { flex: 1 1 44%; }   /* kept: older markup may still use it */

/* ---------- tabs: Question | Tutor ---------- */
.tabs-head { padding: 0 16px; gap: 0; }
.tabs { display: flex; align-items: stretch; gap: 4px; }
.tab {
  appearance: none; border: 0; background: transparent;
  padding: 13px 16px 11px;
  font-size: 13px; font-weight: 700;
  letter-spacing: .4px; text-transform: uppercase;
  color: var(--faint); cursor: pointer;
  border-bottom: 3px solid transparent;
  transition: color var(--t-fast), border-color var(--t-fast);
  position: relative;
}
.tab:hover { color: var(--brand); }
.tab[aria-selected="true"] {
  color: var(--brand);
  border-bottom-color: var(--brand);
}
/* something new in the tutor while the student is reading the question */
.unread {
  display: inline-block; width: 7px; height: 7px; margin-left: 7px;
  border-radius: 50%; background: var(--accent);
  box-shadow: 0 0 0 3px rgba(255,107,53,.18);
  vertical-align: middle;
}

.panel { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.panel.hidden { display: none !important; }

.pane-head, .console-head {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 10px;
  padding: 11px 16px;
  background: linear-gradient(135deg, #f7fafc 0%, #ffffff 100%);
  border-bottom: 1px solid var(--line);
}
.pane-title { display: inline-flex; align-items: center; gap: 8px; }
.pane-title .dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--line-strong);
  transition: background var(--t);
}
.pane-title.is-ready .dot {
  background: var(--ok); box-shadow: 0 0 0 3px rgba(46,163,107,.16);
}
.spacer { flex: 1 1 auto; }

/* ---------- question card ---------- */
/* Only the prompt scrolls. The heading and the action row are pinned, so a
 * long question — the hard tier runs to a dozen lines — can never push the
 * question stepper out of reach. */
/* The question BAR, not the question. The prompt itself now lives in the
   Question tab on the right, where it has full height to be read in. What is
   left here is identity (which question, passed or not, tier progress) and the
   controls that act on it — which must never be hidden behind a tab. */
.question-card {
  flex: 0 0 auto;
  display: flex; flex-direction: column;
  padding: 12px 16px 11px;
  background: var(--brand-tint);
  border-bottom: 1px solid var(--line);
  border-left: 4px solid var(--brand);
}
.q-head { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.q-title {
  font-size: 14px; font-weight: 700; color: var(--brand);
  letter-spacing: -.1px;
}
/* Full height of the Question tab. A hard-tier question runs to a dozen lines
   and is meant to be read carefully, so it gets a column of its own rather
   than four lines above the editor. */
/* md.js:renderQuestion now emits real paragraphs, lists and definition rows,
   so the panel no longer needs pre-wrap to hold the shape of the text — the
   blocks that genuinely depend on their own spacing set it themselves. */
.q-prompt {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: 20px 22px;
  font-size: 14.5px; line-height: 1.7; color: var(--ink-2);
}
.q-prompt::-webkit-scrollbar { width: 8px; }
.q-prompt::-webkit-scrollbar-thumb {
  background: rgba(0,61,124,.2); border-radius: 4px;
}
.q-prompt::-webkit-scrollbar-track { background: transparent; }

/* When there is more question below the fold, fade the last line instead of
   guillotining it. Without this a clipped sentence reads as a broken layout
   rather than as "scroll for the rest". chrome.js sets the class, and clears
   it once the student has scrolled to the end. */
.q-prompt.is-clipped {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent 100%);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent 100%);
}

/* The old "Hide question" collapse is gone: the question now has its own tab,
   so there is nothing above the editor left to collapse. */
.q-prompt code {
  background: var(--brand-soft); color: var(--brand-dark);
  padding: 1px 5px; border-radius: 4px;
  font-family: var(--font-mono); font-size: 12px;
}
.q-prompt pre {
  margin: 8px 0;
  background: var(--code-bg); color: var(--code-ink);
  padding: 12px 14px; border-radius: var(--r);
  overflow-x: auto;
  font-family: var(--font-mono); font-size: 12.5px; line-height: 1.55;
}

/* ----- structured question prompt ----- */
.q-prompt p { margin: 0 0 14px; }
.q-prompt > :last-child { margin-bottom: 0; }

/* name / description pairs. The grid is what actually aligns them: the terms
   share one max-content column, so every description starts at the same x
   however long the longest name is. This is the thing spaces could not do. */
.q-prompt .q-defs {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 7px 18px;
  margin: 0 0 16px;
  padding: 12px 14px;
  background: var(--brand-tint);
  border-left: 3px solid var(--brand);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
}
.q-prompt .q-defs dt { min-width: 0; }
.q-prompt .q-defs dd { margin: 0; min-width: 0; }
/* The term already sits in its own column, so it does not need a filled chip
   as well — a plain mono weight is enough to read it as a name. */
.q-prompt .q-defs dt code {
  background: transparent; padding: 0;
  color: var(--brand-dark); font-weight: 500; font-size: 12.5px;
}
/* A nested example spans both columns rather than pretending to be a term. */
.q-prompt .q-defs .q-literal { grid-column: 1 / -1; }

.q-prompt .q-steps { margin: 0 0 16px; padding-left: 24px; }
.q-prompt .q-steps li { margin: 0 0 7px; padding-left: 3px; }
.q-prompt .q-steps li:last-child { margin-bottom: 0; }
.q-prompt .q-steps::marker, .q-prompt .q-steps li::marker {
  color: var(--brand); font-weight: 600;
}

/* Indented text that is not a list or a pair: kept exactly as written. */
.q-prompt .q-block {
  margin: 0 0 16px;
  padding: 2px 0 2px 15px;
  border-left: 3px solid var(--brand-line);
  white-space: pre-wrap;
}

/* An exact string — a message to raise, a line to print, a row of data. */
.q-prompt .q-literal {
  margin: 7px 0;
  padding: 7px 11px;
  background: var(--brand-soft); color: var(--brand-dark);
  border-radius: var(--r-sm);
  font-family: var(--font-mono); font-size: 12.5px; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word;
}
.q-prompt .q-block > .q-literal:first-child { margin-top: 0; }
.q-prompt .q-block > .q-literal:last-child { margin-bottom: 0; }
.q-foot {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 12px;
  padding-top: 10px; border-top: 1px dashed rgba(0,61,124,.16);
}

/* question stepper: Previous | Question 3 of 10 | Next */
.q-nav { display: flex; align-items: center; gap: 8px; }
.q-count {
  font-size: 12px; font-weight: 600; color: var(--muted);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  display: inline-flex; align-items: center;
  padding: 3px 9px; font-size: 11.5px; font-weight: 500;
  background: var(--surface); color: var(--ink-2);
  border: 1px solid var(--line); border-radius: var(--r-pill);
  font-family: var(--font-mono);
}
.q-chip {
  display: inline-flex; align-items: center;
  padding: 4px 11px; font-size: 11.5px; font-weight: 700;
  letter-spacing: .3px;
  border-radius: var(--r-pill);
}
.q-chip.pass { background: rgba(46,163,107,.1); color: var(--ok-2); border: 1px solid rgba(46,163,107,.4); }
/* Not red: "not yet" is now the state of every Run before the answer is
   finished, not a mark against the student. Green stays for the pass. */
.q-chip.fail { background: var(--bg-subtle); color: var(--muted); border: 1px solid var(--line); }

/* tier progress, e.g. "4 of 10 passed" */
.progress {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 11.5px; font-weight: 600; color: var(--muted);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.progress-bar {
  width: 76px; height: 6px; border-radius: 3px;
  background: rgba(0,61,124,.12); overflow: hidden;
}
.progress-fill {
  display: block; height: 100%; width: 0%;
  background: var(--ok); transition: width 300ms var(--t);
}

/* ---------- editor (Monaco) ---------- */
/* The editor takes whatever the question card and the output pane leave, and
   both of those now size to their content — so on a short question with no
   output yet, the place the student actually types is the biggest thing on
   the screen. */
.editor { flex: 1 1 auto; min-height: 170px; background: var(--code-bg); }

/* runtime status, beside the filename */
.runtime-status {
  font-size: 11.5px; font-weight: 500; font-style: italic;
  color: var(--faint);
}

/* The keystroke that runs the code, said in words next to the button that
   does the same thing. Students do not find Shift+Enter on their own. */
.key-hint {
  font-size: 11.5px; font-weight: 500;
  color: var(--faint); letter-spacing: .2px;
  white-space: nowrap;
}

/* ---------- console ---------- */
.console-head {
  background: var(--code-bg-2);
  border-top: 1px solid var(--code-line);
  border-bottom: 1px solid var(--code-line);
}
.console-head .pane-title { color: #93a3b8; }
/* Sized to what it holds, not to a fixed third of the pane. Empty it is a
   thin strip; a long traceback grows it to 42% and then scrolls. A fixed 30%
   meant a big empty panel on every load and a squeezed editor. */
.console {
  flex: 0 1 auto; min-height: 84px; max-height: 42%;
  margin: 0; padding: 13px 16px;
  background: var(--out-bg); color: var(--out-ink);
  border-left: 4px solid var(--out-rule);
  font-family: var(--font-mono); font-size: 12.5px; line-height: 1.6;
  overflow: auto; white-space: pre-wrap; word-break: break-word;
}
.console:empty {
  background: var(--code-bg); color: #64748b;
  border-left-color: var(--code-line);
}
.console:empty::before {
  content: "Output appears here when you press Run.";
  font-style: italic;
}
.console .err { color: var(--err-ink); }

/* A dragged output pane keeps the height it was given instead of sizing to
   what it holds. min/max have to go with it, or the 42% cap fights the drag. */
.editor-pane.is-console-sized .console {
  flex: 0 0 var(--console-h);
  min-height: 0; max-height: none;
}

/* ---------- resize handles ---------- */
/* Three splits a student can drag: the two panes, the editor against the
   output, and the height of the message box. Each handle is a plain hit strip
   with a hairline down the middle — no glyph, in keeping with the rest of the
   page. It turns navy on hover, focus and while dragging.

   All three are focusable separators: arrow keys nudge them (Shift for a
   bigger step), Enter or a double-click puts them back. js/ui/resize.js. */
.resizer {
  flex: 0 0 auto;
  position: relative;
  background: transparent;
  border: 0; padding: 0;
  touch-action: none;                 /* the drag is ours, not the scroller's */
}
.resizer::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  background: var(--line-strong);
  border-radius: var(--r-pill);
  transition: background var(--t-fast), transform var(--t-fast);
}
.resizer:hover::after,
.resizer:focus-visible::after,
.resizer.is-active::after { background: var(--brand); }
.resizer:focus-visible { outline: none; }
.resizer:focus-visible::after { box-shadow: 0 0 0 3px rgba(0,61,124,.18); }

/* between the two panes */
.resizer-col { width: 14px; cursor: col-resize; align-self: stretch; }
.resizer-col::after { width: 2px; height: 44px; }
.resizer-col:hover::after,
.resizer-col.is-active::after { transform: scaleY(1.8); }

/* between the editor and the output — sits on the dark side of the pane */
.resizer-row { height: 8px; cursor: row-resize; background: var(--code-bg); }
.resizer-row::after { height: 2px; width: 44px; background: var(--code-line); }
.resizer-row:hover::after,
.resizer-row:focus-visible::after,
.resizer-row.is-active::after { background: #7d8aa3; transform: scaleX(1.5); }

/* the top edge of the message box */
.resizer-composer { height: 9px; cursor: row-resize; background: var(--surface); }
.resizer-composer::after { height: 2px; width: 40px; }
.resizer-composer:hover::after,
.resizer-composer.is-active::after { transform: scaleX(1.5); }

/* A drag must not select text or flicker the cursor as it crosses a pane. */
body.is-resizing { user-select: none; }
body.is-resizing-col, body.is-resizing-col * { cursor: col-resize !important; }
body.is-resizing-row, body.is-resizing-row * { cursor: row-resize !important; }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 34px; padding: 0 16px;
  font-size: 13px; font-weight: 600; line-height: 1;
  color: var(--ink-2);
  background: var(--surface);
  border: 1.5px solid var(--brand-line);
  border-radius: var(--r);
  cursor: pointer; white-space: nowrap;
  transition: background var(--t-fast), border-color var(--t-fast),
              color var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast);
}
.btn:hover { background: var(--bg-subtle); border-color: var(--brand-line-2); }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn:disabled:hover { background: var(--surface); border-color: var(--brand-line); transform: none; }

.btn-primary {
  background: linear-gradient(135deg, var(--brand) 0%, var(--brand-mid) 100%);
  color: #fff; border-color: transparent;
  box-shadow: 0 4px 12px rgba(0,61,124,.22);
}
.btn-primary:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 8px 20px rgba(0,61,124,.28);
  background: linear-gradient(135deg, var(--brand-dark) 0%, var(--brand) 100%);
}
.btn-primary:disabled { box-shadow: none; }

.btn-run {
  background: linear-gradient(135deg, #228be6 0%, #1c7ed6 100%);
  color: #fff; border-color: transparent;
  box-shadow: 0 4px 12px rgba(34,139,230,.25);
}
.btn-run:hover:not(:disabled) {
  transform: translateY(-1px); box-shadow: 0 8px 20px rgba(34,139,230,.3);
}
.btn-run:disabled { background: #e9edf2; color: var(--faint); box-shadow: none; }

.btn-stop {
  background: var(--surface); color: #c92a2a;
  border-color: rgba(201,79,79,.45);
}
.btn-stop:hover:not(:disabled) { background: rgba(201,79,79,.06); }

.btn-ghost {
  background: var(--surface); border-color: transparent; color: var(--muted);
  height: 30px; padding: 0 10px; font-size: 12.5px;
}
.btn-ghost:hover { background: var(--bg-subtle); color: var(--brand); border-color: transparent; }

/* ---------- chat ---------- */
.chat {
  flex: 1 1 auto; overflow-y: auto;
  padding: 18px 20px;
  display: flex; flex-direction: column; gap: 16px;
  background: var(--surface);
}
.chat::-webkit-scrollbar { width: 10px; }
.chat::-webkit-scrollbar-thumb {
  background: var(--line-strong); border-radius: 5px; border: 2px solid var(--surface);
}
.chat::-webkit-scrollbar-track { background: transparent; }

.msg { display: flex; align-items: flex-end; gap: 10px; animation: msg-in 200ms var(--t); }
.msg.student { flex-direction: row-reverse; }
@keyframes msg-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

.avatar {
  flex: 0 0 28px; width: 28px; height: 28px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 700; user-select: none;
}
.msg.tutor .avatar {
  background: linear-gradient(135deg, var(--brand) 0%, var(--brand-mid) 100%);
  color: #fff;
}
.msg.student .avatar {
  background: var(--bg-subtle); color: var(--muted); border: 1px solid var(--line);
}

.bubble {
  max-width: 78%;
  padding: 11px 15px;
  border-radius: var(--r-lg);
  font-size: 14px; line-height: 1.6;
  white-space: pre-wrap; word-wrap: break-word;
}
.msg.tutor .bubble {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 4px solid var(--brand);
  border-bottom-left-radius: 4px;
  color: var(--ink-2);
  box-shadow: var(--shadow-1);
}
.msg.student .bubble {
  background: linear-gradient(135deg, var(--brand) 0%, var(--brand-mid) 100%);
  color: #fff; border-bottom-right-radius: 4px;
}
.msg.tutor .bubble code {
  background: var(--brand-soft); color: var(--brand-dark);
  padding: 1px 5px; border-radius: 4px;
  font-family: var(--font-mono); font-size: 12.5px;
}
.msg.student .bubble code {
  background: rgba(255,255,255,.2); color: #fff;
  padding: 1px 5px; border-radius: 4px;
  font-family: var(--font-mono); font-size: 12.5px;
}
.bubble pre {
  margin: 8px 0 4px;
  background: var(--code-bg); color: var(--code-ink);
  padding: 11px 13px; border-radius: var(--r);
  overflow-x: auto;
  font-family: var(--font-mono); font-size: 12.5px; line-height: 1.55;
}
.msg.student .bubble pre { background: rgba(0,0,0,.3); }

/* solution-leak guard: the course flags in orange */
.msg.flagged .bubble { border-left-color: var(--accent); background: var(--accent-soft); }

.typing-dots { display: inline-flex; align-items: center; gap: 4px; height: 18px; }
.typing-dots span {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--line-strong);
  animation: typing 1.3s ease-in-out infinite;
}
.typing-dots span:nth-child(2) { animation-delay: .15s; }
.typing-dots span:nth-child(3) { animation-delay: .30s; }
@keyframes typing {
  0%, 60%, 100% { transform: translateY(0); opacity: .45; }
  30%           { transform: translateY(-3px); opacity: 1; }
}

/* ---------- composer ---------- */
.composer {
  flex: 0 0 auto;
  display: flex; align-items: flex-end; gap: 10px;
  padding: 12px 14px;
  background: var(--surface);
  border-top: 1px solid var(--line);
}
.composer-wrap {
  flex: 1 1 auto; position: relative;
  background: var(--surface-2);
  border: 1.5px solid var(--brand-line);
  border-radius: var(--r);
  transition: border-color var(--t), box-shadow var(--t);
}
.composer-wrap:focus-within {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px rgba(0,61,124,.14);
  background: var(--surface);
}
.composer textarea {
  width: 100%; resize: none; border: 0; background: transparent;
  padding: 10px 12px;
  font-size: 14px; line-height: 1.5;
  outline: none; max-height: 140px;
}
/* Dragged to a height: the 140px autosize cap no longer applies, and what is
   typed scrolls inside the box rather than growing it. */
.composer textarea.is-user-sized { max-height: none; overflow-y: auto; }
.composer .btn-primary { height: 40px; padding: 0 18px; }

/* ---------- sign-in ---------- */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(15,23,41,.5);
  display: flex; align-items: center; justify-content: center;
  animation: fade-in 160ms ease-out;
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.modal {
  background: var(--surface);
  border-top: 5px solid var(--brand);
  border-radius: var(--r-lg);
  padding: 30px;
  width: min(460px, 92vw);
  box-shadow: var(--shadow-3);
  animation: pop-in 200ms var(--t);
}
@keyframes pop-in {
  from { opacity: 0; transform: scale(.97) translateY(8px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}
.modal-mark {
  display: inline-block; margin-bottom: 14px;
  font-size: 11px; font-weight: 700;
  letter-spacing: .8px; text-transform: uppercase;
  color: #fff; background: var(--brand);
  padding: 5px 10px; border-radius: var(--r-sm);
}
.modal h2 {
  margin: 0 0 6px;
  font-size: 21px; font-weight: 700; letter-spacing: -.3px;
  color: var(--brand);
}
.modal p { margin: 6px 0; color: var(--muted); font-size: 13.5px; line-height: 1.6; }
.modal label {
  display: block; margin-top: 20px;
  font-size: 12px; font-weight: 700;
  letter-spacing: .6px; text-transform: uppercase; color: var(--faint);
}
.modal input {
  display: block; width: 100%;
  margin: 7px 0 16px; padding: 11px 13px;
  font-size: 14px;
  background: var(--surface-2);
  border: 1.5px solid var(--brand-line);
  border-radius: var(--r);
  transition: border-color var(--t), box-shadow var(--t);
}
.modal input:focus {
  outline: none; background: var(--surface);
  border-color: var(--brand);
  box-shadow: 0 0 0 3px rgba(0,61,124,.14);
}
.modal .btn-primary { width: 100%; height: 42px; }
.hint {
  margin-top: 16px !important;
  font-size: 12px !important;
  color: var(--faint) !important;
  padding-top: 12px; border-top: 1px solid var(--line);
}

/* ---------- pass modal ----------
   Shown the moment a question's check passes; see js/ui/questions.js
   openPassModal(). Deliberately flat — a plain surface and a rule, no
   gradient, no blur, no card shadow, matching the "no glass, no shadow"
   rule this component was built under. Motion is opt-in: both animations
   below only run when the visitor has not asked for reduced motion. */
.pass-modal {
  padding: 0; margin: auto;
  border: none; border-radius: var(--r-lg);
  background: var(--surface); color: var(--ink-2);
  width: min(560px, 92vw);
  max-height: min(640px, 86vh);
}
.pass-modal::backdrop { background: rgba(15,23,41,.5); }
@media (prefers-reduced-motion: no-preference) {
  .pass-modal { animation: pop-in 200ms var(--t); }
  .pass-modal::backdrop { animation: fade-in 160ms ease-out; }
}
.pass-modal-form {
  display: flex; flex-direction: column;
  max-height: min(640px, 86vh);
}
.pass-modal-head {
  display: flex; align-items: center; gap: 10px;
  padding: 22px 24px 16px;
  border-bottom: 1px solid var(--line);
}
.pass-modal-title { font-size: 15.5px; font-weight: 700; color: var(--brand); }
.pass-modal-review {
  padding: 16px 24px;
  overflow-y: auto;
  font-size: 14px; line-height: 1.6; color: var(--ink-2);
}
.pass-modal-review-waiting {
  display: flex; align-items: center; gap: 10px;
  color: var(--muted); font-size: 13.5px;
}
.pass-modal-review pre {
  margin: 8px 0 4px;
  background: var(--code-bg); color: var(--code-ink);
  padding: 11px 13px; border-radius: var(--r);
  overflow-x: auto;
  font-family: var(--font-mono); font-size: 12.5px; line-height: 1.55;
}
.pass-modal-review code {
  background: var(--brand-soft); color: var(--brand-dark);
  padding: 1px 5px; border-radius: 4px;
  font-family: var(--font-mono); font-size: 12.5px;
}
.pass-modal-lastmsg {
  margin: 0; padding: 4px 24px 16px;
  color: var(--muted); font-size: 13.5px; line-height: 1.6;
}
.pass-modal-actions {
  display: flex; justify-content: flex-end; gap: 10px;
  padding: 16px 24px; border-top: 1px solid var(--line);
}

/* ---------- Gemini key panel (per-student key; see js/ui/geminiKey.js) ----------
   Reuses .modal-backdrop / .modal / .modal-mark / .modal h2 / .modal p /
   .modal label / .modal input as-is (the sign-in modal above) -- flat surface,
   1px rule, no new gradient or shadow added here. Text only: no icons. */
.gemini-key-modal { text-align: left; }
.gemini-key-modal a { color: var(--brand); font-weight: 600; }
/* Situation copy: what's true right now (required / optional / in use) --
   see situationText() in js/ui/geminiKey.js. Same size/colour as .modal p,
   just its own class so it can be targeted and re-rendered independently. */
.gemini-key-situation {
  margin: 6px 0 14px;
  color: var(--muted); font-size: 13.5px; line-height: 1.6;
}
.gemini-key-hint {
  margin: -10px 0 0 !important;
  font-size: 12px !important; color: var(--faint) !important; line-height: 1.5;
}
.gemini-key-status {
  margin: 14px 0 2px;
  font-size: 12.5px; font-weight: 600;
  color: var(--muted);
}
.gemini-key-status.present { color: var(--ok-2); }
.gemini-key-actions {
  display: flex; gap: 10px; margin-top: 14px;
}
.gemini-key-actions .btn { flex: 0 0 auto; height: 38px; padding: 0 16px; width: auto; }
/* The explicit opt-in ("Use my own Gemini key instead") and the privacy
   sentence that sits with it -- plain text and a native checkbox, same flat
   surface as the rest of the panel, no new colour or effect. */
.gemini-key-optin-row {
  margin-top: 16px; padding-top: 14px;
  border-top: 1px solid var(--line);
}
.gemini-key-optin-label {
  display: flex; align-items: baseline; gap: 8px;
  font-size: 13.5px; font-weight: 600; color: var(--ink-2);
  cursor: pointer;
}
.gemini-key-optin-label input[type="checkbox"] { margin: 0; }
.gemini-key-privacy {
  margin: 8px 0 0; color: var(--muted); font-size: 12.5px; line-height: 1.55;
}
.gemini-key-result {
  margin: 10px 0 0 !important;
  font-size: 12.5px !important; color: var(--muted) !important; line-height: 1.5;
}
.gemini-key-result.form-error { color: var(--err) !important; }
.gemini-key-close { width: 100% !important; margin-top: 18px; }
/* Which AI the own key is for (2026-10-01). */
.gemini-key-modal #own-ai-select { width: 100%; margin: 0 0 14px; }

/* "The AI tutor is unavailable" (js/main.js refreshRoute). Calm, not an alarm:
   the editor, Run and the automatic check all still work. Text only. */
.tutor-offline {
  margin: 10px 12px 0; padding: 10px 12px;
  border: 1px solid var(--line-strong); border-left: 3px solid var(--warn);
  border-radius: var(--r); background: var(--bg-subtle);
  color: var(--muted); font-size: 13px; line-height: 1.55;
}
.tutor-offline strong { color: var(--ink); margin-right: 4px; }

/* ---------- browser-only version: AI panel, Gemini steps, usage meter ----------
   (js/ui/aiPanel.js; docs/BROWSER_VERSION.md). Same flat surface as the key panel. */
.ai-panel { text-align: left; max-width: 520px; }
.ai-panel #ai-choice { width: 100%; margin: 0 0 12px; }
.ai-steps { margin: 4px 0 14px; padding-left: 20px; color: var(--ink-2, var(--ink));
  font-size: 13.5px; line-height: 1.6; }
.ai-steps li { margin: 6px 0; }
.ai-steps .btn { height: 32px; padding: 0 12px; margin-right: 8px; width: auto; }
.ai-usage { display: flex; align-items: center; gap: 10px; margin: 6px 12px 10px;
  font-size: 12px; color: var(--muted); }
.ai-usage-bar { flex: 0 0 90px; height: 6px; border-radius: var(--r-pill);
  background: var(--line); overflow: hidden; }
.ai-usage-fill { display: block; height: 100%; width: 0; background: var(--ok); }
.record-note { margin: 0 12px 10px; font-size: 12px; color: var(--muted); }
.ai-usage.warn { color: var(--accent-ink); }
.ai-usage.warn .ai-usage-fill { background: var(--warn); }
.ai-usage.limit { color: var(--err); font-weight: 600; }
.ai-usage.limit .ai-usage-fill { background: var(--err); }

.hidden { display: none !important; }

/* ---------- responsive ---------- */
@media (max-width: 1100px) {
  .tierbar .meta-label { display: none; }
  .tier-btn { padding: 7px 12px; }
  .tier-btn .tier-n { display: none; }
}
@media (max-width: 1100px) {
  .key-hint { display: none; }
}
@media (max-width: 880px) {
  .topbar { height: auto; flex-wrap: wrap; gap: 10px; padding: 10px 14px; }
  .split { flex-direction: column; padding: 10px; gap: 10px; }
  /* Stacked, so a left/right split has nothing to divide. resize.js sees the
     hidden handle and declines the drag; the panes go back to an even share. */
  .resizer-col { display: none; }
  .editor-pane, .side-pane, .chat-pane { flex: 1 1 50%; }
  .select, .select-sm { min-width: 150px; }
  .question-card { max-height: 40%; }
  .status { display: none; }
}
