/* The Celestial Compass palette, type, radii and motion curves all live in
   tokens.css, which index.html loads first. Everything below is specific to
   the student app and has no business in a shared token file. */
:root {
  /* The stack itself lives in tokens.css so both frontends share one. */
  font-family: var(--sans);
}

* { box-sizing: border-box; }

.hidden { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  /* 100vh on mobile Safari is calculated against the viewport with the
     toolbar collapsed, so a fixed-height layout gets cut off under the
     toolbar when it's visible (which is most of the time). 100dvh tracks
     the actual visible viewport live as the toolbar shows/hides -- the
     100vh line above it is just a fallback for browsers that don't
     understand dvh (none of note on iPhone 16/17, but harmless to keep). */
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}

/* Passcode lock screen, shown before the chat loads -- same look as the
   parent dashboard's lock screen (see parent.css), just her own passcode. */
.lock-screen {
  height: 100vh;
  height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  /* body is overflow:hidden, so a card taller than the viewport (a short
     landscape phone, a large accessibility font) has to scroll here or not
     at all -- see the margin:auto note on .lock-card. */
  overflow-y: auto;
}
.lock-card {
  /* Centres like align-items:center did, but degrades safely: when the card
     is taller than the screen, auto margins collapse to 0 and it starts at
     the top edge. Centring a flex item that overflows pushes its top edge
     off-screen, and no amount of scrolling reaches above a flex container's
     start edge -- the content is simply gone. */
  margin: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 32px;
  width: 280px;
  text-align: center;
}
.lock-title { font-family: var(--serif); font-weight: 400; font-size: 18px; margin-bottom: 4px; }
.lock-sub { font-size: 13px; color: var(--ink-soft); margin-bottom: 18px; }
.lock-card input {
  width: 100%;
  padding: 10px 12px;
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 14px;
  margin-bottom: 12px;
}
.lock-card button {
  width: 100%;
  padding: 10px;
  background: var(--accent);
  color: var(--on-accent);
  border: 1px solid var(--accent-border);
  border-radius: 10px;
  font-weight: 600;
  cursor: pointer;
}
.lock-error { color: var(--struggling-text); font-size: 12.5px; margin-top: 10px; }
/* Second step of the student sign-in on a device no parent has set up yet
   (Security Round 1): a short explanation and the parent-email field. */
.lock-device-step { margin: 2px 0 12px; }
.lock-device-step .lock-hint { font-size: 12.5px; line-height: 1.5; color: var(--ink-soft); text-align: left; margin: 0 0 10px; }
.lock-device-step input { margin-bottom: 0; }

/* First-run onboarding wizard (Northstar sections 67-69), shown once after
   her first successful unlock. Same card language as the lock screen, sized
   up for actual content and a step progress indicator. */
.onboarding-screen {
  height: 100vh;
  height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  /* Step 3 is the tall one: 612px of card, which overflows anything shorter
     than about 650px. */
  overflow-y: auto;
}

.onboarding-card {
  margin: auto;  /* see .lock-card -- centring an overflowing flex item eats its top */
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 36px 32px 28px;
  width: 100%;
  max-width: 380px;
  box-shadow: 0 8px 28px rgba(36, 31, 26, 0.08);
}

.onboarding-progress {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-bottom: 22px;
}

.onboarding-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--border);
  transition: background 0.2s ease, transform 0.2s ease;
}

.onboarding-dot.active {
  background: var(--accent-text);
  transform: scale(1.2);
}

.onboarding-dot.done {
  background: var(--accent-soft);
}

.onboarding-step {
  display: flex;
  flex-direction: column;
  text-align: center;
  /* nova-enter from the canonical library in tokens.css (section 123). */
  animation: nova-enter var(--dur-fast) var(--ease-out);
}

.onboarding-icon { font-size: 30px; margin-bottom: 10px; }

.onboarding-eyebrow {
  /* JetBrains Mono is loaded at 400 and 500 only -- 600+ would be faked. */
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-text);
  margin-bottom: 6px;
}

.onboarding-title {
  /* Marcellus ships a single weight, 400; asking for bold would make the
     browser synthesise one, which on a display serif reads as a misprint. */
  font-family: var(--serif);
  font-weight: 400;
  font-size: 18px;
  margin-bottom: 12px;
  line-height: 1.3;
}

.onboarding-body {
  font-size: 13.5px;
  color: var(--ink-soft);
  line-height: 1.55;
  margin-bottom: 22px;
  text-align: left;
}

.onboarding-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12.5px;
  color: var(--ink-soft);
  text-align: left;
  margin-bottom: 14px;
}

.onboarding-field select,
.onboarding-field input {
  font-family: inherit;
  font-size: 13.5px;
  color: var(--ink);
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg);
}
.onboarding-field select:focus,
.onboarding-field input:focus { outline: none; border-color: var(--accent-text); }

.onboarding-toggle-row {
  display: flex;
  align-items: center;
  gap: 10px;
  text-align: left;
  font-size: 13.5px;
  font-weight: 600;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 14px;
  cursor: pointer;
}
.onboarding-toggle-row input { width: 17px; height: 17px; accent-color: var(--accent-text); cursor: pointer; flex-shrink: 0; }

.onboarding-actions {
  display: flex;
  gap: 10px;
  margin-top: 4px;
}

.onboarding-next-btn,
.onboarding-back-btn {
  flex: 1;
  padding: 11px;
  border-radius: 10px;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
  border: none;
}

.onboarding-next-btn {
  background: var(--accent);
  color: var(--on-accent);
  border: 1px solid var(--accent-border);
}
.onboarding-next-btn:disabled { opacity: 0.6; cursor: not-allowed; }

.onboarding-back-btn {
  background: var(--bg);
  color: var(--ink-soft);
  border: 1px solid var(--border);
}
.onboarding-back-btn:hover { border-color: var(--accent-text); color: var(--accent-text); }

/* Step 1 has no Back button, so its lone Next button isn't stretched by
   .onboarding-actions' flex layout -- give it the same full-width treatment
   directly. */
.onboarding-step[data-step="1"] > .onboarding-next-btn {
  width: 100%;
  padding: 11px;
  border-radius: 10px;
}

/* ---------------------------------------------------------------------------
   Scrolling inside a fixed-viewport shell.

   .app is pinned to the viewport height and body is overflow:hidden, so a
   flex child taller than the room left over is not merely below the fold --
   it is unreachable. It cannot push the page down, and nothing scrolls it.
   Measured before this block existed, on a nearly-empty account: the "How I
   explain things" panel put 287px out of reach on a 375x667 phone and 100px
   on a 1280x800 desktop, and My Profile was worse. With real content in it,
   more.

   Two declarations per panel are needed, and the first is the one that is
   easy to miss: a flex item defaults to min-height:auto, which refuses to
   shrink below its content, and that silently defeats overflow-y on its own.
   min-height:0 is what lets the panel become smaller than what it holds so
   overflow-y:auto has something to do.

   The chrome around it is pinned with flex-shrink:0 so the panel absorbs the
   squeeze. Without that, flex would take the difference out of every sibling
   in proportion, shrinking the composer and the footer -- the composer in
   particular has to stay put and stay reachable.
   --------------------------------------------------------------------------- */
.home-panel,
.prefs-panel,
.profile-panel {
  min-height: 0;
  overflow-y: auto;
}

#shellChrome,
.compact-bar,
.banner,
.resume-banner,
.context-suggestion,
.session-header,
.status-bar,
.composer,
.footer-actions {
  flex-shrink: 0;
}

.app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;
  /* 100dvh tracks the browser toolbar showing/hiding, but not the on-screen
     keyboard -- iOS Safari doesn't shrink dvh when the keyboard opens, so
     the composer (last in this flex column) ends up positioned below the
     keyboard, effectively hidden. --app-vh is set from the VisualViewport
     API in app.js, which does account for the keyboard; this falls back to
     100dvh on browsers/contexts where that script hasn't run yet or the API
     isn't available. */
  height: var(--app-vh, 100dvh);
  max-width: 720px;
  margin: 0 auto;
  background: var(--bg);
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

.brand { display: flex; align-items: center; gap: 12px; }

.avatar {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  border: 1px solid var(--accent-border);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 16px;
}

.brand-name { font-family: var(--serif); font-weight: 400; font-size: 15px; }
.brand-sub { font-size: 12px; color: var(--ink-soft); }

/* position:relative so the "what your parent sees" popover can anchor to
   the whole action group rather than to the narrow i button, which pushed
   a 280px panel off the left edge of the screen. */
.header-actions { display: flex; align-items: center; gap: 8px; position: relative; }

.parent-link {
  font-size: 12px;
  color: var(--ink-soft);
  text-decoration: none;
  border: 1px solid var(--border);
  padding: 6px 12px;
  border-radius: 999px;
}
.parent-link:hover { border-color: var(--accent-text); color: var(--accent-text); }

.icon-btn {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--ink-soft);
  font-size: 15px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.icon-btn:hover { border-color: var(--accent-text); color: var(--accent-text); }

/* Header menu (Northstar sections 74/26/84) -- My Profile, Settings, and My
   Reviews all live under one ⚙ trigger rather than each getting their own
   header icon, so the header stays to a single icon on the right (plus the
   🏠 Home shortcut moved over by the brand/logo on the left) no matter how
   many panels get added in future rounds. */
.menu-wrap { position: relative; }

.menu-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 190px;
  z-index: 20;
}
.menu-dropdown.hidden { display: none; }

.menu-item {
  display: flex;
  align-items: center;
  gap: 9px;
  background: none;
  border: none;
  color: var(--ink);
  font-family: inherit;
  font-size: 13px;
  text-align: left;
  padding: 9px 10px;
  border-radius: 8px;
  cursor: pointer;
  white-space: nowrap;
}
.menu-item:hover { background: var(--bg); }
.menu-item-glyph { font-size: 14px; width: 18px; text-align: center; flex-shrink: 0; }

.prefs-panel {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.prefs-title { font-weight: 600; font-size: 13.5px; }

.prefs-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12.5px;
  color: var(--ink-soft);
}

.prefs-field select,
.prefs-field input,
.prefs-field textarea {
  font-family: inherit;
  font-size: 13.5px;
  color: var(--ink);
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg);
}
.prefs-field textarea { resize: vertical; }
.prefs-field select:focus,
.prefs-field input:focus,
.prefs-field textarea:focus { outline: none; border-color: var(--accent-text); }

.prefs-toggle-row {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 12.5px;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
  cursor: pointer;
}
.prefs-toggle-row input { width: 16px; height: 16px; accent-color: var(--accent-text); cursor: pointer; flex-shrink: 0; }

.prefs-save-btn {
  align-self: flex-start;
  background: var(--accent);
  color: var(--on-accent);
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  padding: 7px 16px;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
}

.prefs-hint { font-size: 11.5px; color: var(--ink-soft); }

/* Student Learning Profile (Northstar section 74) -- a real, visible "My
   Profile" panel assembled from three clearly separated tiers: what she's
   set herself, what her parent has set, and what's been observed from real
   mastery evidence. Styled consistently with .prefs-panel above. */
.profile-panel {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.profile-intro {
  font-size: 12px;
  color: var(--ink-soft);
  line-height: 1.4;
  margin-top: -4px;
}

.profile-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.profile-section:first-of-type { padding-top: 0; border-top: none; }

.profile-section-title {
  font-weight: 600;
  font-size: 12.5px;
  color: var(--ink);
}

.profile-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  font-size: 12.5px;
}

.profile-label { color: var(--ink-soft); flex-shrink: 0; }

.profile-value {
  color: var(--ink);
  text-align: right;
  font-weight: 500;
}

.profile-hint { font-size: 11.5px; color: var(--ink-soft); line-height: 1.4; }

.profile-chip-group {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.profile-chip {
  font-size: 11.5px;
  font-family: inherit;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 10px;
  cursor: pointer;
}
.profile-chip:hover { border-color: var(--accent-text); }
.profile-chip.strength { border-color: var(--accent-text); color: var(--accent-text); }
/* A strength whose evidence has aged (section 76). Softened, never struck
   through or greyed out: the evidence is retained and she did earn it --
   this says "worth another look", not "no longer true". */
.profile-chip.stale { border-style: dashed; }
.profile-chip-age { color: var(--ink-soft); font-size: 10.5px; }

/* A live moment marker (sections 110, 117). Quiet on purpose: section 110
   asks for brief and meaningful rather than constant, and anything that
   reads as praise for ordinary work teaches her the app cannot tell the
   difference. */
.moment-mark {
  margin-top: 6px;
  font-size: 11px;
  color: var(--ink-soft);
  letter-spacing: 0.02em;
}

/* Persona joined the thread (section 164). Deliberately quiet: this is
   the app telling her what changed, not an announcement. Uses the
   canonical nova-notice pattern via .motion-notice rather than any
   animation of its own. */
.persona-joined {
  align-self: flex-start;
  max-width: 100%;
  margin: 4px 0 8px;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-left: 2px solid var(--accent-text);
  border-radius: 10px;
  background: var(--bg);
}
.persona-joined-line { font-size: 12.5px; color: var(--ink); }
.persona-joined-sub { font-size: 11px; color: var(--ink-soft); margin-top: 1px; }
.persona-joined-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.persona-joined-btn {
  font-family: inherit;
  font-size: 11.5px;
  color: var(--ink);
  background: var(--canvas);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 10px;
  cursor: pointer;
}
.persona-joined-btn:hover { border-color: var(--accent-text); color: var(--accent-text); }

/* Voice input (sections 71, 223, 227). Recording used to be a border
   colour -- the same colour hover gives -- on an emoji no stylesheet can
   recolour, so she could not tell it was listening, or that it had
   stopped. Now the button itself turns into a red Stop, and the status
   line above the composer says in words what the mic is doing at every
   step. The pulse on the dot is the library's nova-recording pattern,
   the one sanctioned loop beside the composer (see tokens.css). */
.mic-btn { flex: none; gap: 6px; }
/* .composer button's 20px side padding is wider than a 32px icon button,
   which squeezed the drawn icon to nothing (an emoji only survived because
   text overflows). The icon never shrinks, and the button carries no
   padding of its own until it grows into Stop. */
.composer .mic-btn { padding: 0; }
.mic-btn svg { flex: none; }
.mic-btn .stop-icon,
.mic-btn .mic-label { display: none; }
.composer .mic-btn.recording {
  width: auto;
  padding: 0 14px;
  border-radius: 999px;
  background: var(--recording-tint);
  border: 2px solid var(--recording);
  color: var(--recording);
}
.mic-btn.recording .mic-icon { display: none; }
.mic-btn.recording .stop-icon,
.mic-btn.recording .mic-label { display: inline; }

.voice-status {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 20px;
  font-size: 13px;
  color: var(--ink-soft);
  background: var(--surface);
  border-top: 1px solid var(--border);
}
.voice-status.hidden { display: none; }
.voice-status.recording { color: var(--ink); }
.voice-dot {
  display: none;
  flex: none;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--recording);
}
.voice-status.recording .voice-dot { display: block; }
/* A level meter rather than a looping wave. Each bar's height is set from her
   actual microphone level (app.js), so it moves because she is talking and
   is still when she is not. Hidden until a level is really coming in, so
   it can never pretend to hear something. */
.voice-level {
  display: none;
  align-items: center;
  gap: 2px;
  height: 16px;
}
.voice-status.hearing .voice-level { display: inline-flex; }
.voice-level i {
  display: block;
  width: 3px;
  height: 16px;
  border-radius: 2px;
  background: var(--recording);
  transform: scaleY(0.25);
  transform-origin: center;
}
/* The loader while her last words are written down. nova-thinking, the
   library's "work is happening" pulse, staggered across three dots. It only
   exists in the busy state, which ends when the words arrive or after a
   6-second ceiling (app.js), so it can't loop on after the work is done. */
.voice-busy {
  display: none;
  align-items: center;
  gap: 3px;
}
.voice-status.busy .voice-busy { display: inline-flex; }
.voice-busy i {
  display: block;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent-text);
  opacity: 0.45;
}
.voice-status.busy .voice-busy i {
  animation: nova-thinking 1200ms var(--ease-in-out) infinite;
}
.voice-status.busy .voice-busy i:nth-child(2) { animation-delay: 200ms; }
.voice-status.busy .voice-busy i:nth-child(3) { animation-delay: 400ms; }
.voice-timer {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}

/* Saved Items (sections 74, 77, 80). The star sits inside a card that is
   itself a button, so it needs its own hit area big enough to hit on a
   phone without catching the card underneath. */
.save-star {
  position: absolute;
  top: 6px;
  right: 6px;
  min-width: 32px;
  min-height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  line-height: 1;
  color: var(--ink-soft);
  cursor: pointer;
  border-radius: 8px;
}
.save-star:hover, .save-star:focus-visible { color: var(--accent-text); }
.save-star[data-saved="true"] { color: var(--accent-text); }
/* The card has to establish a containing block, and leave room so a long
   question does not run under the star. */
.ask-curious-list .home-card { position: relative; padding-right: 40px; }

.saved-row { display: flex; align-items: stretch; gap: 6px; margin-bottom: 6px; }
.saved-row-main { flex: 1; min-width: 0; }
.saved-row-remove {
  flex: none;
  min-width: 36px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--ink-soft);
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
}
.saved-row-remove:hover { border-color: var(--accent-text); color: var(--accent-text); }

/* An active project (section 74) is a fact, not a judgement she can
   dispute, so it must not look tappable the way an evidence chip does --
   the pointer cursor and hover highlight are what promise "you can argue
   with this", and both are removed here. */
.profile-chip.project { cursor: default; }
.profile-chip.project:hover { border-color: var(--border); }

/* Profile Evidence & Student Agency (Northstar section 75) -- a chip is a
   button now, tapping it opens this tiny inline "is this accurate?" row
   rather than a modal or confirmation dialog, matching how lightweight
   everything else in this panel is. flex-basis:100% forces it onto its own
   line in the wrapping chip group instead of sitting awkwardly mid-row. */
.profile-chip-feedback-row {
  flex-basis: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  color: var(--ink-soft);
}

.profile-chip-feedback-btn {
  font-size: 11px;
  font-family: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 9px;
  cursor: pointer;
}
.profile-chip-feedback-btn:hover { border-color: var(--accent-text); color: var(--accent-text); }

/* A "Not really" response softens the chip rather than removing it -- it's a
   refinement signal on the presentation, not a request to erase the
   underlying evidence, which stays exactly as it was. */
.profile-chip.corrected { opacity: 0.55; }

.profile-chip-note {
  flex-basis: 100%;
  font-size: 11px;
  font-style: italic;
  color: var(--ink-soft);
}

.profile-chip-empty {
  font-size: 11.5px;
  color: var(--ink-soft);
  font-style: italic;
}

.profile-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.profile-list-label {
  /* JetBrains Mono is loaded at 400 and 500 only -- 600+ would be faked. */
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 500;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  margin-top: 4px;
}

.profile-list-row {
  font-size: 12px;
  color: var(--ink);
  display: flex;
  justify-content: space-between;
  gap: 10px;
}

/* Persistent Review Objects (Northstar section 84) -- a real, findable list
   of worksheets/writing shared in Review mode, reusing the .profile-panel
   shell (same "My ___" panel pattern as Profile and Home). List view is
   deliberately lightweight (no images); tapping a card swaps to the detail
   view within the same panel rather than opening something new. */
.reviews-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Contextual History (Northstar section 85) -- a bounded, independently
   scrollable region for a past conversation's messages, reusing the .msg
   bubble styles from the live chat. Unlike .chat (the live view, which fills
   its whole flex parent), this sits inside a normal-flow panel alongside
   other content, so it needs its own max-height rather than flex:1. */
.history-detail-messages {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-height: 50vh;
  overflow-y: auto;
  padding: 4px 2px;
}

.review-card {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
  cursor: pointer;
  font-family: inherit;
}
.review-card:hover { border-color: var(--accent-text); }

.review-card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

/* SHOW RECENT's thumbnail (section 85). The card is space-between, so the
   text beside a thumb has to claim the remaining width or it gets pushed to
   the right edge -- scoped to rows that actually have one, leaving every
   other .review-card (text + badge) laid out exactly as before. */
.history-thumb {
  flex: none;
  width: 44px;
  height: 44px;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--canvas);
}
.history-thumb + .review-card-text { flex: 1; }

.review-card-preview {
  font-size: 12.5px;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.review-card-date { font-size: 11px; color: var(--ink-soft); }

.review-card-badge {
  font-size: 10.5px;
  font-weight: 600;
  padding: 3px 9px;
  border-radius: 999px;
  flex-shrink: 0;
  white-space: nowrap;
}
.review-card-badge.open { background: var(--accent-soft); color: var(--accent-text); }
.review-card-badge.addressed { background: var(--bg); color: var(--ink-soft); border: 1px solid var(--border); }

.review-detail {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.review-detail-back { align-self: flex-start; }

.review-detail-image {
  max-width: 100%;
  border-radius: 10px;
  border: 1px solid var(--border);
}

.review-detail-note { font-size: 12.5px; color: var(--ink-soft); font-style: italic; }

.review-detail-comment-label {
  /* JetBrains Mono is loaded at 400 and 500 only -- 600+ would be faked. */
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 500;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.review-detail-comment {
  font-size: 13px;
  color: var(--ink);
  line-height: 1.5;
  white-space: pre-wrap;
}

.review-detail-subject {
  /* JetBrains Mono is loaded at 400 and 500 only -- 600+ would be faked. */
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 500;
  color: var(--accent-text);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

/* Anchored Comments & Navigation (Northstar section 98), text/location-
   reference fallback tier -- a simple readable list, not pixel-anchored
   highlighting on the image (see the Cluster 3 package's open question on
   why: vision-model coordinate guesses aren't reliable enough to trust for
   a Grade 8 student). Each row names where to look in the coach's own words
   and what to check. */
.review-detail-comments {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.review-comments-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.review-comment-row {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px 10px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.review-comment-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.review-comment-tag {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--accent-text);
  background: var(--accent-soft);
  border-radius: 999px;
  padding: 3px 9px;
  white-space: nowrap;
}

.review-comment-location {
  font-size: 11.5px;
  color: var(--ink-soft);
  font-weight: 600;
}

.review-comment-text {
  font-size: 12.5px;
  color: var(--ink);
  line-height: 1.45;
}

.review-detail-notes-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.review-detail-revised {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Home screen (Northstar section 86) -- scoped down deliberately to an
   optional panel reachable from a single 🏠 shortcut by the logo, rather than
   replacing the app's tested "open straight into chat" entry flow. Ties
   together Continue (#72's resume data) and For You (#77's recommendation)
   with a simple mode picker, all built from data the app already computes --
   no new curated content library, and no change to how the app opens. */
.home-panel {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.home-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Home — For You & Continue (Northstar section 86). That section's
   hierarchy is welcome -> CONTINUE -> WHAT DO YOU WANT TO DO? -> FOR YOU,
   and the two surfaces added since (the Daily Spark and the Curiosity Map)
   have to find a place in it.
   
   Ordered by how actionable each one is. Unfinished work and a waiting
   invitation sit with Continue at the top; the mode picker and
   recommendations follow as the section prescribes; the Curiosity Map goes
   last because it is reflective -- somewhere she has been, not something to
   pick up. It had been sitting ABOVE Recent topics, so a read-only view of
   past work outranked a thread she could resume in one tap.
   
   Done with flex order rather than by moving markup: the panel is already a
   flex column, and an ordering expressed in CSS cannot accidentally nest one
   section inside another, which is exactly what moving the blocks did. */
#homePanel > .home-section { order: 6; }   /* the mode picker, unnamed here */
#homePanel > #homeContinueSection        { order: 1; }
#homePanel > #sparkSection               { order: 2; }
#homePanel > #homeThreadsSection         { order: 3; }
#homePanel > #homeActiveProjectSection   { order: 4; }
#homePanel > #homeUnfinishedReviewsSection { order: 5; }
#homePanel > #homeForYouSection          { order: 7; }
#homePanel > #curiositySection           { order: 8; }

.home-section-title {
  font-weight: 600;
  font-size: 12.5px;
  color: var(--ink);
}

.home-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  width: 100%;
  text-align: left;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 14px;
  cursor: pointer;
  font-family: inherit;
}
.home-card:hover { border-color: var(--accent-text); }

.home-card-continue { border-left: 3px solid var(--accent); }
.home-card-foryou { border-left: 3px solid var(--accent); }
.review-attach-card { border-left: 3px solid var(--accent); }
.home-card-review { border-left: 3px solid var(--accent); }
.ask-suggestion-card { border-left: 3px solid var(--accent); }

/* Recommendation Transparency & Control (Northstar section 79), Cluster 2 --
   a small, unobtrusive dismiss control on each For You card, matching the
   in-chat recommendation banner's "Not now" but lighter-weight since this
   sits on a card rather than a full banner. position:relative anchors the
   absolutely-positioned "x" without disturbing the card's own flex layout. */
.home-card-dismissable { position: relative; padding-right: 30px; }
.home-card-dismiss-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 20px;
  height: 20px;
  border: none;
  background: none;
  color: var(--ink-soft);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  border-radius: 50%;
}
.home-card-dismiss-btn:hover { background: var(--border); color: var(--ink); }

/* Recommendation Engine (Northstar section 77), Cluster 2 round -- Home's For
   You section can now render more than one card (practice + visual), so the
   dynamic container needs the same stacking gap .ask-curious-list already
   uses for its own list of cards. */
#homeForYouCards {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* ASK Mode suggestions (Northstar section 80), Cluster 5's third pilot --
   the Curious? bank reuses the plain .home-card shell (no accent border,
   unlike the personalized interests/recent-study cards above) since these
   aren't drawn from anything specific to her, just a rotating general set. */
.ask-curious-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.home-card-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.home-card-sub {
  font-size: 11.5px;
  color: var(--ink-soft);
}

/* The next step a closed session left on its thread (Northstar 143).
   Carries --accent-text rather than the flat --star-gold: it sits on --bg
   as small text, so it needs the AA-safe text pairing, not the decoration
   colour. */
.home-card-next {
  font-size: 12px;
  color: var(--accent-text);
  margin-top: 1px;
}

/* The Daily Spark (section 184). Deliberately quiet: no badge, no counter,
   no colour that reads as an alert. It should look like something offered,
   not something owed. */
.spark-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: 12px;
  padding: 14px 16px;
}
.spark-ambient {
  font-size: 11.5px;
  color: var(--ink-soft);
  margin-bottom: 4px;
}
.spark-title { font-family: var(--serif); font-weight: 400; font-size: 16px; color: var(--ink); margin-bottom: 6px; }
.spark-body { font-size: 13.5px; color: var(--ink); line-height: 1.55; white-space: pre-wrap; }
.spark-why { font-size: 11.5px; color: var(--ink-soft); margin-top: 8px; }
/* Something current (section 190): the fact, the coach's take, and the
   source are each their own line, so one is never mistaken for another. */
.spark-fact { margin-bottom: 6px; }
.spark-thought { color: var(--ink-soft); font-style: italic; margin-bottom: 6px; }
.spark-source { font-size: 11.5px; color: var(--ink-soft); margin-top: 8px; white-space: normal; }
.spark-source a { color: var(--accent-text); }
.spark-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.spark-btn {
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 7px 12px;
  font-family: inherit;
  font-size: 12.5px;
  cursor: pointer;
}
.spark-btn-primary { background: var(--accent); color: var(--on-accent); border-color: var(--accent-border); font-weight: 600; }
/* "Not interested" stays plain and unemphasised -- it is a real choice,
   not a reluctant one (section 187: no guilt language). */
.spark-btn-quiet { color: var(--ink-soft); }
.spark-btn:hover { border-color: var(--accent-text); }

/* The Curiosity Map (section 115). No node is bigger, brighter or higher
   than another: the section forbids turning exploration into rank, and size
   or ordering is exactly how that creeps in. Colour separates subjects,
   which is categorical, not ranked. */
.curiosity-map {
  width: 100%;
  /* Capped so the graph does not balloon on a wide screen: the SVG scales
     with its container, and at full desktop width the labels rendered
     twice the size of the body text around them. */
  max-width: 400px;
  height: auto;
  display: block;
  margin: 0 auto 10px;
}
.curiosity-map .cm-spoke { stroke: var(--border); stroke-width: 1; }
.curiosity-map .cm-bridge { stroke: var(--accent); stroke-width: 1.4; stroke-dasharray: 3 3; fill: none; opacity: 0.9; }
.curiosity-map .cm-hub-glow { fill: var(--accent-soft); }
.curiosity-map .cm-hub { fill: var(--accent); }
.curiosity-map .cm-hub-label { font-size: 7.5px; letter-spacing: 0.08em; fill: var(--ink-soft); font-family: var(--mono); }
.curiosity-map .cm-node-glow { fill: var(--surface); }
.curiosity-map .cm-node { fill: none; stroke: var(--accent-border); stroke-width: 1.25; }
.curiosity-map .cm-mark { font-size: 11px; fill: var(--accent-text); font-family: var(--sans); }
.curiosity-map .cm-mark-initial { font-size: 10px; fill: var(--ink-soft); font-weight: 600; }
.curiosity-map .cm-label { font-size: 8.5px; fill: var(--ink); font-family: var(--sans); }
.curiosity-map .cm-more { font-size: 7.5px; fill: var(--ink-soft); font-family: var(--sans); }

/* Her constellation (section 116). Every point the same size, because a
   point that grows with anything is a score. */
.identity-star { width: 100%; max-width: 380px; height: auto; display: block; margin: 0 auto 8px; }
.identity-star .id-link { stroke: var(--accent); stroke-width: 1.25; stroke-dasharray: 3 2; opacity: 0.85; }
.identity-star .id-point { fill: var(--accent); }
.identity-star .id-glow { fill: var(--accent-soft); }
/* The same subjects in words. The drawing is aria-hidden, so this is what
   the constellation actually says -- kept quiet because on screen the
   labels in the drawing already say it. */
.identity-subjects {
  font-size: 12px;
  color: var(--ink-soft);
  margin-bottom: 6px;
}

.identity-star .id-label { font-size: 8px; fill: var(--ink-soft); font-family: var(--sans); }

.curiosity-map .cm-hit { fill: transparent; }
.curiosity-map .cm-subject { cursor: pointer; }
.curiosity-map .cm-subject:hover .cm-node,
.curiosity-map .cm-subject:focus-visible .cm-node { stroke: var(--accent-text); stroke-width: 2; }
.curiosity-map .cm-subject:focus { outline: none; }
.curiosity-map .cm-subject:focus-visible .cm-node-glow { fill: var(--accent-soft); }
.curiosity-map .cm-subject.selected .cm-node { stroke: var(--accent-text); stroke-width: 2.25; }
.curiosity-map .cm-subject.selected .cm-node-glow { fill: var(--accent-soft); }
.curiosity-map .cm-subject.selected .cm-label { font-weight: 700; fill: var(--ink); }
/* Same size on every node: the bubble states a number, it never makes one
   node bigger than another (section 115). */
.curiosity-map .cm-bubble { fill: var(--accent); }
.curiosity-map .cm-bubble-text { font-size: 7.5px; font-weight: 700; fill: var(--on-accent); font-family: var(--sans); }

.curiosity-summary { margin-bottom: 8px; }
.curiosity-summary-line { font-size: 12.5px; color: var(--ink); }
.curiosity-list { display: flex; flex-direction: column; gap: 6px; }
.curiosity-detail-head { font-size: 13px; font-weight: 600; color: var(--ink); }
.curiosity-topic-list {
  margin: 0;
  padding-left: 18px;
  font-size: 12.5px;
  color: var(--ink-soft);
  line-height: 1.55;
}
.curiosity-more-btn {
  align-self: flex-start;
  background: none;
  border: none;
  padding: 2px 0;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  color: var(--accent-text);
  cursor: pointer;
}
.curiosity-more-btn:hover { text-decoration: underline; }
.curiosity-bridges { font-size: 11.5px; color: var(--accent-text); margin-top: 3px; }

.home-mode-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.home-mode-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--ink);
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  padding: 10px 12px;
  border-radius: 10px;
  cursor: pointer;
}
.home-mode-btn:hover { border-color: var(--accent-text); color: var(--accent-text); }

.home-mode-glyph { font-size: 14px; }

/* PRACTICE Mode formats (Northstar section 82), pilot round -- Cluster 5 of
   the partial-coverage roadmap. Reuses the .home-card/.home-panel shell (same
   "screen you visit" pattern as Home, My Profile, and My Reviews) rather than
   inventing a new visual language, since this is the same kind of thing:
   an optional default screen for one mode, not a whole new UI system. */
.practice-recommended-card {
  cursor: default;
}
.practice-recommended-card:hover { border-color: var(--border); }

.practice-format-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Persistent-workspace groups in My Reviews (section 83). Same quiet
   grouping treatment as Practice's bands, for the same reason: these
   organise one list, they are not separate screens. */
.review-group-title {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 14px 0 6px;
}
.review-group-title:first-child { margin-top: 0; }
.review-group-sub {
  display: block;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  font-size: 11.5px;
  color: var(--ink-soft);
  margin-top: 1px;
}

/* Cognitive-demand bands (section 82). Quieter than a section title --
   these group the picker, they are not four new screens -- and spaced so
   the three groups read as distinct at a glance on a phone. */
.practice-band-title {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 14px 0 6px;
}
.practice-band-title:first-child { margin-top: 0; }
.practice-band-sub {
  display: block;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  font-size: 11.5px;
  color: var(--ink-soft);
  margin-top: 1px;
}

.practice-format-btn {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  text-align: left;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 11px 13px;
  cursor: pointer;
  font-family: inherit;
}
.practice-format-btn:hover { border-color: var(--accent-text); }

.practice-format-glyph {
  font-size: 16px;
  flex-shrink: 0;
  line-height: 1.4;
}

.practice-format-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.practice-format-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.practice-format-sub {
  font-size: 11.5px;
  color: var(--ink-soft);
}

/* "What your parent sees", behind the i in the top bar. Same sentence the
   permanent banner carried -- one tap away instead of three lines of the
   screen, every screen. */
#privacyInfoBtn {
  font-family: var(--serif);
  font-style: italic;
  font-size: 15px;
  line-height: 1;
}
.info-popover {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  left: auto;
  z-index: 60;
  width: min(280px, calc(100vw - 32px));
  background: var(--surface-raised, var(--surface));
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(36, 31, 26, 0.14);
  padding: 12px 14px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink);
  animation: nova-enter var(--dur-fast) var(--ease-out);
}

/* Session Continuity resume banner (Northstar section 72) -- offered once,
   on a genuinely fresh app open, when there's meaningful unfinished work to
   pick back up. Sits above the chat log rather than blocking it; ignoring it
   and just typing is always a valid way to start something new instead. */
/* The section 72 recap. Sits inside the banner rather than replacing it, so
   reading it never costs her the Continue button she came for. */
.resume-recap {
  margin-top: 8px;
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ink);
  animation: nova-enter var(--dur-fast) var(--ease-out);
}
.resume-recap .resume-recap-line { display: block; }
.resume-recap .resume-recap-label { color: var(--ink-soft); }

.resume-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 12px 20px;
}

.resume-banner.hidden { display: none; }

/* The resume offer (section 72) is one slim row: title and what it would
   resume share a line, and the buttons are smaller than a primary action.
   It sits above whatever she is doing, so it must not cost her the
   conversation underneath -- the version this replaced took half a phone
   screen. The recap, when she asks for it, wraps onto its own line. */
.resume-banner-compact { padding: 8px 20px; gap: 8px 12px; }
.resume-banner-compact .resume-banner-text {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 8px;
  flex: 1 1 240px;
  min-width: 0;
}
.resume-banner-compact .resume-banner-preview { margin-top: 0; max-width: 100%; }
.resume-banner-compact .resume-recap { flex-basis: 100%; }
.resume-banner-compact .resume-btn { padding: 6px 12px; }

.resume-banner-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.resume-banner-preview {
  font-size: 12.5px;
  color: var(--ink-soft);
  margin-top: 2px;
  max-width: 420px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.resume-banner-time {
  font-size: 11.5px;
  color: var(--ink-soft);
  margin-top: 2px;
}

.resume-banner-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

.resume-btn {
  font-size: 12.5px;
  padding: 8px 14px;
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
}

.resume-btn-primary {
  background: var(--accent);
  color: var(--on-accent);
  border: 1px solid var(--accent-border);
}

.resume-btn-primary:hover { opacity: 0.9; }

.resume-btn-secondary {
  background: none;
  border: 1px solid var(--border);
  color: var(--ink-soft);
}

.resume-btn-secondary:hover { border-color: var(--accent-text); color: var(--accent-text); }

/* Recommendation banner (Northstar section 77, "Recommended practice" --
   see server.js for the v1 scoping notes). Reuses the .resume-banner layout
   and button styles wholesale since the two are functionally siblings (a
   dismissible suggestion above the chat log), just with a left accent stripe
   so the two are still visually distinguishable when both could in theory
   appear -- though in practice the resume banner takes priority and this one
   only shows when there's nothing to resume. */
.recommendation-banner {
  border-left: 3px solid var(--accent);
}

/* Mode-switch notice (Round 6, Northstar #128/#147) -- same .resume-banner
   layout as Resume/Recommendation, just a neutral left accent instead of the
   recommendation banner's -- this one isn't a suggestion, it's an
   acknowledgment that a switch already happened on her behalf. */
/* nova-notice from the canonical library (section 123): these two are the
   notices the app raises mid-conversation without her asking. */
.mode-switch-notice {
  animation: nova-notice var(--dur-base) var(--ease-out);

  border-left: 3px solid var(--border);
}

.chat {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.msg {
  max-width: 80%;
  padding: 12px 16px;
  border-radius: var(--radius);
  line-height: 1.5;
  font-size: 14.5px;
  white-space: pre-wrap;
}

.msg.coach {
  align-self: flex-start;
  background: var(--coach-bubble);
  color: var(--on-coach-bubble);
  border: 1px solid var(--border);
  border-bottom-left-radius: 4px;
  white-space: normal;
}

/* Coach replies are rendered from a small Markdown subset (see
   coachMarkdownToHtml in app.js) -- these keep paragraphs/lists compact
   inside a chat bubble instead of using default browser spacing. */
/* PRACTICE's question surface (section 112). One edge, in the accent, on a
   coach turn that genuinely hands the work back -- the same design system,
   behaving differently, rather than a second bubble style. Purely emphasis:
   the question itself carries the meaning, so nothing is lost without the
   colour. */
.msg.coach.your-turn {
  border-left: 3px solid var(--accent);
  border-bottom-left-radius: var(--radius);
}

.msg.coach p { margin: 0 0 0.6em; }
.msg.coach p:last-child { margin-bottom: 0; }
.msg.coach ul, .msg.coach ol { margin: 0 0 0.6em; padding-left: 1.3em; }
.msg.coach ul:last-child, .msg.coach ol:last-child { margin-bottom: 0; }
.msg.coach li { margin-bottom: 2px; }
.msg.coach strong { font-weight: 700; }
.msg.coach code {
  background: var(--code-tint);
  padding: 1px 5px;
  border-radius: 4px;
  font-family: var(--mono);
  font-size: 0.9em;
}

/* Fenced code blocks (```...```) -- most visibly, ASCII-art diagrams Show
   mode falls back to without a GEMINI_API_KEY. white-space: pre keeps exact
   spacing intact (that's the whole point of ASCII art); overflow-x lets a
   wide diagram scroll horizontally in its own box instead of blowing out the
   chat bubble's width or wrapping into nonsense. */
.msg.coach pre {
  background: var(--code-tint);
  border-radius: 8px;
  padding: 10px 12px;
  margin: 0 0 0.6em;
  overflow-x: auto;
}
.msg.coach pre:last-child { margin-bottom: 0; }
.msg.coach pre code {
  background: none;
  padding: 0;
  white-space: pre;
  font-size: 0.85em;
}

.msg.user {
  align-self: flex-end;
  background: var(--student-bubble);
  color: var(--on-student-bubble);
  border-bottom-right-radius: 4px;
}

/* Wraps a worksheet photo so the inspection sweep (section 107) has a
   surface to cross. Only the live path wraps; replayed history does not,
   because the coach is not inspecting anything when she scrolls back. */
.msg-image-wrap {
  position: relative;
  display: inline-block;
  overflow: hidden;
  border-radius: 10px;
  line-height: 0;
}
.msg-image-wrap.inspecting::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(100deg, transparent 38%, var(--accent-soft) 50%, transparent 62%);
  animation: nova-inspect 1200ms var(--ease-in-out) both;
}

.msg-image {
  display: block;
  max-width: 220px;
  border-radius: 10px;
  margin-bottom: 6px;
}

/* SHOW Mode — Primary Visual Canvas (section 96). The bubble's 80% cap and
   padding took a generated diagram down to 220px on a 375px phone; here the
   visual gets the full column and the explanation sits beneath it rather
   than beside it. Only in SHOW: elsewhere a diagram is a supporting asset
   and the bubble is right. */
.msg.coach.visual-first {
  max-width: 100%;
  width: 100%;
  background: none;
  border: none;
  padding: 0;
}
.msg.coach.visual-first .msg-image-coach {
  max-width: 100%;
  margin-bottom: 10px;
}
/* The prose keeps its bubble so the surface still reads as a reply. */
.msg.coach.visual-first > div:not(.msg-followup-row) {
  background: var(--coach-bubble);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  border-bottom-left-radius: 4px;
  padding: 12px 16px;
}

/* Coach-generated diagrams/charts are meant to actually be read, not just
   previewed like a worksheet-photo thumbnail, so give them more room. */
.msg-image-coach {
  width: 100%;
  max-width: 340px;
  background: var(--coach-bubble);
  border: 1px solid var(--border);
  padding: 8px;
  border-radius: 10px;
}

/* Visual Display & Inspection Standard (Northstar v2.3, new): any image that
   can be inspected up close gets a hand cursor and a tiny always-visible
   affordance, so it reads as tappable rather than a plain static image. */
.msg-image,
.review-detail-image {
  cursor: zoom-in;
}

/* Lightbox overlay -- shared by SHOW-mode diagrams and REVIEW-mode worksheet
   photos alike, since both just need "open bigger, zoom, pan, close." */
.image-lightbox {
  position: fixed;
  inset: 0;
  z-index: 500;
  background: rgba(10, 10, 14, 0.92);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  touch-action: none;
}

.image-lightbox.hidden { display: none; }

.image-lightbox-toolbar {
  position: absolute;
  top: max(14px, env(safe-area-inset-top));
  right: 14px;
  display: flex;
  gap: 8px;
  z-index: 2;
}

.image-lightbox-btn {
  min-width: 38px;
  height: 38px;
  padding: 0 12px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}

.image-lightbox-btn:hover { background: rgba(255, 255, 255, 0.18); }

.image-lightbox-reset { font-size: 12.5px; font-weight: 600; }

.image-lightbox-close { font-size: 22px; }

.image-lightbox-viewport {
  width: 100%;
  height: 100%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: grab;
}

.image-lightbox-viewport.dragging { cursor: grabbing; }

.image-lightbox-img {
  max-width: 92vw;
  max-height: 86vh;
  will-change: transform;
  user-select: none;
  -webkit-user-drag: none;
}

.image-lightbox-hint {
  position: absolute;
  bottom: max(14px, env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, 0.6);
  font-size: 11.5px;
  text-align: center;
  pointer-events: none;
}

@media (hover: none) {
  .image-lightbox-hint { display: none; }
}

.msg.system {
  align-self: center;
  background: transparent;
  color: var(--ink-soft);
  font-size: 12.5px;
  font-style: italic;
}

/* Web & Info Access (Northstar section 63): the source list under a coach
   reply that used live web search -- deliberately understated (small,
   muted) so it reads as a footnote, not a competing headline. */
.msg-sources {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  line-height: 1.6;
  white-space: normal;
}

.msg-sources-label {
  color: var(--ink-soft);
  font-weight: 600;
}

.msg-sources a {
  color: var(--ink);
  text-decoration: none;
  word-break: break-word;
}

.msg-sources a:hover {
  text-decoration: underline;
}

/* SHOW Mode's lightweight guided-flow follow-ups (Northstar section 81) --
   small pill chips under a coach reply that included an image, offering
   natural next steps (see NEXT_ACTIONS.visual in app.js). Deliberately
   understated, same instinct as .msg-sources above -- a footnote under the
   reply, not competing with it. */
.msg-followup-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

.msg-followup-chip {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 5px 10px;
  font-size: 11.5px;
  font-family: inherit;
  color: var(--ink);
  cursor: pointer;
}
.msg-followup-chip:hover { border-color: var(--accent-text); color: var(--accent-text); }

.typing {
  align-self: flex-start;
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--ink-soft);
  font-size: 13px;
  padding: 0 16px;
}

/* NOVA star, Thinking state (Northstar §108, §114) -- a fade-only pulse per
   the Design System's motion philosophy (tokens/motion.css: "fades + small
   translates, no bounces, no spins"). Purely communicates "the coach is
   working," nothing decorative layered on top (§122 Motion Decision Test). */
/* NOVA Aura (Northstar section 103): a low-density field around the working
   state. Sits behind the row, never over it -- that section's last line
   forbids obscuring text, controls, work, diagrams or accessibility
   information, and the mark inside it is aria-hidden and unselectable.
   
   Static once it has faded in. An animation zone that keeps moving beside a
   reply she is beginning to read is precisely what section 109 prevents, so
   the only motion here is its arrival. It leaves with the indicator rather
   than dimming: "settle when the student needs to concentrate" and 109's
   handover are the same moment. */
.typing.has-aura { position: relative; }
.typing-aura {
  position: absolute;
  left: 6px;
  top: 50%;
  transform: translateY(-50%);
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--accent-soft) 0%, transparent 70%);
  opacity: 0.55;
  pointer-events: none;
  user-select: none;
  z-index: 0;
  animation: nova-fade var(--dur-base) var(--ease-out);
}
/* The part that adapts to subject or mode. Trails the label rather than
   sitting inside the field, so it cannot cover the star or the copy. */
.typing-aura-mark {
  position: relative;
  z-index: 1;
  font-size: 12px;
  color: var(--accent-text);
  opacity: 0.75;
  user-select: none;
  animation: nova-fade var(--dur-base) var(--ease-out);
}
/* Everything real in the row sits above the field. */
.typing.has-aura > .typing-star,
.typing.has-aura > .typing-label { position: relative; z-index: 1; }

.typing-star {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  /* nova-thinking from the canonical library in tokens.css (section 123). */
  animation: nova-thinking 1800ms var(--ease-in-out) infinite;
}
.typing-star path { fill: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  .typing-star {
    animation: none;
    opacity: 0.85;
  }
}

.status-bar {
  padding: 8px 20px;
  font-size: 12.5px;
  background: var(--struggling-soft);
  color: var(--struggling-text);
  border-top: 1px solid var(--status-error-border);
}
.status-bar.hidden { display: none; }

.composer {
  display: flex;
  gap: 10px;
  padding: 14px 20px;
  background: var(--surface);
  border-top: 1px solid var(--border);
}

.composer textarea {
  flex: 1;
  resize: none;
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 10px 14px;
  font-size: 14.5px;
  line-height: 1.35;
  font-family: inherit;
  /* rows="1" alone sizes to a single line's content box, so a placeholder
     that wraps to two lines on a narrow phone gets clipped rather than
     shown -- min-height guarantees room for two wrapped lines regardless of
     the JS auto-resize below, which only measures real typed content. */
  min-height: 56px;
  max-height: 120px;
}

.composer textarea:focus { outline: none; border-color: var(--accent-text); }

.composer button {
  background: var(--accent);
  color: var(--on-accent);
  border: 1px solid var(--accent-border);
  border-radius: 12px;
  padding: 0 20px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.composer button:disabled { opacity: 0.5; cursor: not-allowed; }

.attach-btn {
  flex-shrink: 0;
  font-size: 16px;
}
.scratch-btn { flex-shrink: 0; font-size: 16px; }
.attach-btn.has-attachment { border-color: var(--accent-text); color: var(--accent-text); background: var(--accent-soft); }

.attach-preview {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 20px;
  background: var(--surface);
  border-top: 1px solid var(--border);
}

.attach-preview img {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: 10px;
  border: 1px solid var(--border);
}

.attach-remove-btn {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--ink-soft);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.attach-remove-btn:hover { border-color: var(--accent-text); color: var(--accent-text); }

/* Several photos (or one PDF) waiting to send, each with its own remove
   button on its corner so taking one back never means starting over. */
.attach-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.attach-item { position: relative; display: inline-flex; }
.attach-item .attach-remove-btn {
  position: absolute;
  top: -8px;
  right: -8px;
}
.attach-count {
  font-size: 12px;
  color: var(--ink-soft);
}
.attach-count.hidden { display: none; }

/* Drag and drop (desktop, iPad). Covers the app while files are over it,
   so where to let go is never in doubt. Appears without animation. */
.drop-overlay {
  position: fixed;
  inset: 0;
  z-index: 450;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  pointer-events: none;
}
.drop-overlay.hidden { display: none; }
.drop-overlay-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 28px 40px;
  border: 2px dashed var(--accent-text);
  border-radius: 16px;
  background: var(--surface);
  color: var(--ink);
  font-size: 17px;
  font-weight: 600;
}
.drop-overlay-card span { font-size: 13px; font-weight: 400; color: var(--ink-soft); }

/* Her message with several photos: a row of thumbnails, each opening
   full-size, rather than a stack taking the whole screen. */
.msg-image-row { display: flex; flex-wrap: wrap; gap: 6px; }
.msg-image-row .msg-image { max-width: 104px; max-height: 140px; object-fit: cover; margin-bottom: 0; }
.review-detail-extra { display: flex; flex-direction: column; gap: 10px; }

/* Chat UI Modes (Northstar section 71) -- a sticky, per-message steer she
   picks right above the composer. Ask is the quiet default; the other three
   get a visible accent glyph so the active mode reads at a glance without
   needing to read the label. */
.mode-switcher {
  display: flex;
  gap: 6px;
  padding: 8px 20px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
}

/* Context Prompts Zone -- sits just above the chat pane. Surfaces a Claude-
   judged, topically-relevant session from a DIFFERENT mode she still has
   open, and lets her choose to bring a short briefing of it into the mode
   she's in now (per her explicit request: she stays in control, sessions
   never silently merge). Hidden by default; app.js shows/fills it only when
   /api/student/context-suggestion finds a genuine match. */
.context-suggestion {
  padding: 10px 20px;
  background: var(--accent-soft, rgba(200, 157, 60, .16));
  border-bottom: 1px solid var(--border);
}

.context-suggestion-offer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.context-suggestion-text {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13.5px;
  color: var(--ink-soft);
  flex: 1;
  min-width: 180px;
}

.context-suggestion-glyph {
  flex-shrink: 0;
}

.context-suggestion-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

/* Context Preview (Northstar #131): an explicit, opt-in expansion showing the
   fuller Claude-written briefing and its source mode, rather than making her
   commit to "Bring this in" based on the short reason phrase alone. */
.context-suggestion-preview {
  margin: 8px 0;
  padding: 8px 10px;
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink);
}

.context-suggestion-preview-source {
  /* JetBrains Mono is loaded at 400 and 500 only -- 600+ would be faked. */
  font-family: var(--mono);
  display: block;
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
  margin-bottom: 4px;
}

/* Context Removal & Reset (Northstar #134): once she accepts, the offer
   collapses into a small "attached" chip with an explicit way to undo before
   her next message actually sends it -- rather than the accept being final
   and silent the instant she taps it. */
.context-suggestion-attached {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--ink);
}

.context-suggestion-attached-text { flex: 1; }

.context-suggestion-remove {
  flex-shrink: 0;
  background: none;
  border: 1px solid var(--border);
  color: var(--ink-soft);
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  padding: 4px 10px;
  border-radius: 999px;
  cursor: pointer;
}
.context-suggestion-remove:hover { border-color: var(--accent-text); color: var(--accent-text); }

/* Session Header (Northstar #137): a lightweight "Mode · what this one's
   about" label above the chat, once a session actually has something in it
   -- built entirely from her own first message, no extra AI call needed. */
.session-header {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 8px 20px 0;
  font-size: 12.5px;
  color: var(--ink-soft);
}

/* Learning Thread Header (Northstar section 137): the thread's own label
   leads and the mode follows as a small indicator. The emphasis used to run
   the other way -- mode bold and gold, topic plain -- which is what that
   section means by "avoid making the technical source session the dominant
   label". Which experience she is in is still worth showing; it just isn't
   the name of what she is working on. */
.session-header-topic {
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.session-header-mode {
  flex-shrink: 0;
  font-size: 11.5px;
  font-weight: 500;
  color: var(--ink-soft);
}

/* NOVA Star as Behavioral Character (Northstar section 108).
   
   Two states, not the nine that section lists. THINKING is the fade-pulse
   below; WAITING FOR STUDENT is simply its absence, which is the whole
   point -- stillness is a state (section 109), not the lack of one.
   
   LISTENING ("gentle slow pulse" while she types) is deliberately not
   built. Section 109 says never use animation to pressure a fast response,
   and a mark pulsing at her while she composes an answer is exactly that.
   Where the two sections disagree, the one protecting her thinking wins.
   
   The remaining states -- CURIOUS, IDEA, CHALLENGE, INSIGHT, REVIEWING,
   MILESTONE -- have no live trigger in the app: nothing detects those
   moments in the chat as they happen (see moments.js, which detects three
   of them only after a session closes). Animating them on a guess is what
   section 108's own "do not anthropomorphize it so heavily that it becomes
   childish" warns against. */
.nova-star {
  width: 19px;
  height: 19px;
}
.nova-star path { fill: currentColor; }
.avatar.thinking .nova-star {
  animation: nova-thinking 1800ms var(--ease-in-out) infinite;
}
/* Must come after the rule above rather than joining the reduced-motion
   block earlier in this file: same specificity, so the later declaration
   wins, and declaring it first left the mark still animating and frozen at
   0.45 opacity for anyone who asks for reduced motion.
   
   Only animation is reset here. Unlike .typing-star -- which sits in a
   transient row and needs a declared resting opacity or it halts mid-fade
   and nearly vanishes -- stopping this animation leaves the mark at full
   opacity on its solid gold fill, which is exactly the resting state we
   want. Verified rather than assumed: a declared resting opacity on this
   element does not take effect, for reasons I could not pin down, so there
   is no point carrying a line that does nothing. */
@media (prefers-reduced-motion: reduce) {
  .avatar.thinking .nova-star { animation: none; }
}

.mode-btn {
  display: flex;
  align-items: center;
  gap: 5px;
  flex-shrink: 0;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--ink-soft);
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  /* 10px rather than 12px buys back the width the verb labels cost
     (Northstar section 130): "Talk / Show me / Practice / Review" measures
     388px at 12px, which overflows a 375px phone by 13px and pushes Review
     under the scroll edge. At 10px all four fit, measured at 375 and 390.
     Horizontal only -- the vertical padding is untouched, so these stay
     31px tall, which is under the 44px touch-target guideline. That
     predates this change and is not made worse by it, but it is real:
     fixing it means taller tabs, which costs the chat pane height on a
     phone, so it wants doing deliberately rather than in passing. */
  padding: 6px 10px;
  border-radius: 999px;
  cursor: pointer;
}
.mode-btn:hover { border-color: var(--accent-text); color: var(--accent-text); }
.mode-btn.active {
  background: var(--accent);
  border-color: var(--accent-border);
  color: var(--on-accent);
}
.mode-glyph { font-size: 13px; }

/* Unfinished-activity indicator (Northstar #135): a small dot on a mode tab
   that has a real, resumable session waiting -- previously the only way to
   know was to actually switch there and check. Hidden on whichever tab is
   currently active, since she's already looking at it. */
.mode-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent-text);
  margin-left: 1px;
  flex-shrink: 0;
}
.mode-btn.active .mode-dot { display: none; }

/* Mobile drawer collapse -- on a phone-width screen, once a conversation
   actually has messages in it (.chat-active, set alongside #sessionHeader),
   #shellChrome (header + mode switcher, untouched above) stops taking up
   permanent vertical space and becomes an off-canvas drawer instead, so the
   chat and composer get the room. Desktop, and mobile before any messages
   exist, keep the original always-visible layout. */
.drawer-wrap-up-btn { display: none; }

.compact-bar {
  display: none;
  align-items: center;
  gap: 12px;
  padding: 10px 20px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.compact-bar-title {
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
  font-size: 13px;
}

.compact-bar-mode { font-weight: 700; color: var(--accent-text); flex-shrink: 0; }

.compact-bar-topic {
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.drawer-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(10, 10, 14, 0.4);
  z-index: 400;
}

@media (max-width: 700px) {
  .app.chat-active #shellChrome {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: 84%;
    max-width: 320px;
    background: var(--surface);
    z-index: 410;
    overflow-y: auto;
    padding-bottom: max(16px, env(safe-area-inset-bottom));
    transform: translateX(-100%);
    transition: transform 0.25s ease;
    box-shadow: 2px 0 24px rgba(0, 0, 0, 0.15);
  }
  .app.chat-active.drawer-open #shellChrome { transform: translateX(0); }
  .app.chat-active.drawer-open #drawerOverlay { display: block; }
  .app.chat-active #compactBar { display: flex; }
  .app.chat-active #shellChrome .drawer-wrap-up-btn {
    display: block;
    width: calc(100% - 40px);
    margin: 4px 20px 0;
    background: none;
    border: 1px solid var(--border);
    color: var(--ink-soft);
    font-size: 12.5px;
    padding: 10px 16px;
    border-radius: 10px;
    cursor: pointer;
  }
  .app.chat-active .footer-actions { display: none; }
  /* #compactBar already shows the same mode/topic label -- suppressing the
     inline one here avoids showing it twice in the small vertical space
     mobile has to work with. */
  .app.chat-active #sessionHeader { display: none; }
}

/* A wrapped-up session (Northstar section 35). The transcript stays readable
   -- she may well want to re-read it -- but it is visibly finished, and the
   composer is gone rather than merely inert, because an editable box next to
   a closed conversation is what made the coach look like it had forgotten
   her. */
.app.session-ended #chat { opacity: 0.55; }
.app.session-ended #composer,
.app.session-ended .footer-actions { display: none; }

.ended-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  padding: 12px 20px 16px;
  flex-shrink: 0;
  border-top: 1px solid var(--border);
}
/* Section 111: "short, elegant, and proportional". One line, the accent's
   own text colour, no burst and no badge -- and absent entirely on the many
   sessions where nothing in particular happened. */
.ended-recognition {
  width: 100%;
  text-align: center;
  font-size: 13px;
  color: var(--accent-text);
  line-height: 1.5;
  margin-bottom: 4px;
}

.ended-note {
  width: 100%;
  text-align: center;
  font-size: 12.5px;
  color: var(--ink-soft);
  margin-bottom: 2px;
}
.wrap-up-btn-primary {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent-border);
  font-weight: 600;
}

/* Her closing self-assessment: tapped, never typed. */
.closing-choice-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 8px 0 4px;
}
.closing-choice-prompt {
  font-size: 12.5px;
  color: var(--ink-soft);
  margin-top: 10px;
}

.footer-actions {
  display: flex;
  justify-content: center;
  gap: 10px;
  /* env(safe-area-inset-bottom) is 0 unless the viewport meta tag has
     viewport-fit=cover -- on notched iPhones this keeps the button clear
     of the home-indicator gesture area instead of sitting flush against it. */
  padding: 8px 20px calc(16px + env(safe-area-inset-bottom));
  background: var(--bg);
}

.wrap-up-btn {
  background: none;
  border: 1px solid var(--border);
  color: var(--ink-soft);
  font-size: 12.5px;
  padding: 8px 16px;
  border-radius: 999px;
  cursor: pointer;
}
.wrap-up-btn:hover { border-color: var(--accent-text); color: var(--accent-text); }

/* ---- Someone else is here ------------------------------------------------
   A parent or sibling using her login. Their bubbles are drawn as not-hers
   (outlined, no fill, labelled) so a read-back never passes them off as her
   work, and the banner above the composer stays up for as long as their
   messages are being kept out of her record. */
.msg.user.guest {
  background: var(--surface);
  color: var(--ink);
  border: 1.5px dashed var(--border-focus);
}
.guest-tag {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  margin-bottom: 2px;
}
.guest-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 20px;
  font-size: 13px;
  color: var(--ink);
  background: var(--accent-soft);
  border-top: 1px solid var(--border);
}
.guest-banner.hidden { display: none; }
.guest-banner span { flex: 1; }
.guest-back-btn {
  flex: none;
  font: inherit;
  font-weight: 600;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--accent-border);
  background: var(--accent);
  color: var(--on-accent);
  cursor: pointer;
}
#guestBtn.active {
  border-color: var(--accent-text);
  color: var(--accent-text);
  background: var(--accent-soft);
}

/* A PDF (section 146): named, not pictured. */
.attach-doc {
  font-size: 13px;
  color: var(--ink);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px 12px;
  max-width: 70%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.attach-doc.hidden { display: none; }
.msg-doc {
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 4px;
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
   Desktop and iPad layout (900px and wider): rail | chat | context.
   Scaffolding: structure and behaviour, in today's tokens. The v3 Design
   System restyles it. Below 900px the wrapper is display: contents and the
   two new columns are hidden, so the phone layout is untouched.
   --------------------------------------------------------------------------- */
.main-column { display: contents; }
.desktop-rail, .context-panel { display: none; }
.conv-bar { display: contents; }
.conv-actions { display: none; }

@media (min-width: 900px) {
  .app {
    --rail-w: 232px;
    max-width: none;
    display: grid;
    grid-template-columns: var(--rail-w) minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
  }
  .app.rail-collapsed { --rail-w: 64px; }

  /* The rail now holds the header, the mode tabs and the footer actions. */
  #shellChrome, .footer-actions { display: none; }

  .main-column {
    display: flex;
    flex-direction: column;
    min-height: 0;
    min-width: 0;
    width: 100%;
    max-width: 820px;
    justify-self: center;
  }

  .desktop-rail {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-height: 0;
    overflow-y: auto;
    padding: 12px 10px;
    background: var(--surface);
    border-right: 1px solid var(--border);
  }
  .rail-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: 36px;
    padding: 7px 10px;
    border: 0;
    border-radius: 10px;
    background: none;
    color: var(--ink);
    font: inherit;
    font-size: 13.5px;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
  }
  .rail-item:hover { background: var(--accent-soft); }
  .rail-item.active,
  .rail-item[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent-text); font-weight: 600; }
  .rail-icon { flex-shrink: 0; width: 24px; text-align: center; line-height: 1; }
  .rail-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .rail-brand { margin-bottom: 6px; }
  .rail-brand .rail-label { font-family: var(--serif); font-size: 18px; }
  .rail-mark svg { width: 18px; height: 18px; fill: var(--accent); }
  .rail-collapse-glyph { margin-left: auto; color: var(--ink-soft); }
  .rail-section-title {
    padding: 14px 10px 4px;
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-soft);
  }
  .rail-dot { flex-shrink: 0; width: 7px; height: 7px; margin-left: auto; border-radius: 50%; background: var(--accent); }
  .rail-dot.hidden,
  .rail-item.active .rail-dot { display: none; }
  .rail-topics { display: flex; flex-direction: column; gap: 2px; }
  .rail-topics .rail-item { font-size: 13px; }
  .rail-topics-empty { padding: 4px 10px; font-size: 12px; color: var(--ink-soft); }
  .rail-spacer { flex: 1; min-height: 12px; }
  .rail-popover {
    margin: 2px 4px 6px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--bg);
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--ink);
  }
  .rail-popover.hidden { display: none; }

  /* Minimised: icons only. Labels stay in the accessibility tree (visually
     hidden, not display: none) and show as .rail-tip on hover and keyboard
     focus; section headings become thin dividers so the groups survive. */
  .app.rail-collapsed .desktop-rail { padding: 12px 8px; }
  .app.rail-collapsed .rail-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  .app.rail-collapsed .rail-section-title {
    height: 1px;
    margin: 8px 10px;
    padding: 0;
    overflow: hidden;
    font-size: 0;
    background: var(--border);
  }
  .app.rail-collapsed .rail-topics,
  .app.rail-collapsed .rail-popover,
  .app.rail-collapsed .rail-kbd { display: none; }
  .app.rail-collapsed .rail-item { justify-content: center; padding: 7px 0; }
  .app.rail-collapsed .rail-dot { position: absolute; top: 6px; right: 8px; margin: 0; }

  .rail-tip {
    position: fixed;
    z-index: 470;
    transform: translateY(-50%);
    padding: 5px 9px;
    border-radius: 8px;
    background: var(--ink);
    color: var(--bg);
    font-size: 12.5px;
    white-space: nowrap;
    pointer-events: none;
  }
  .rail-tip.hidden { display: none; }

  /* New topic: the rail's one primary action, first under the brand. */
  .rail-new-topic {
    margin-bottom: 6px;
    border: 1px solid var(--accent);
    color: var(--accent-text);
    font-weight: 600;
  }
  .rail-new-topic .rail-icon { color: var(--accent-text); }

  /* The open topic: named, not filled, so the mode stays the one highlight. */
  .rail-now { font-weight: 600; }
  .rail-when { flex-shrink: 0; margin-left: auto; font-size: 11.5px; font-weight: 400; color: var(--ink-soft); }
  .rail-sub .rail-item { padding-left: 22px; font-size: 13px; }

  .rail-account { position: relative; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border); }
  .rail-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent-text);
    font-size: 12px;
    font-weight: 600;
  }
  .rail-caret { margin-left: auto; color: var(--ink-soft); }
  .rail-account-btn[aria-expanded="true"] { background: var(--accent-soft); }
  .rail-kbd { margin-left: auto; font: inherit; font-size: 11.5px; color: var(--ink-soft); }
  /* Fixed rather than absolute so the rail's own scrolling can't clip it,
     collapsed or not. */
  .rail-account-menu {
    position: fixed;
    left: 10px;
    bottom: 64px;
    z-index: 450;
    display: flex;
    flex-direction: column;
    gap: 2px;
    width: 232px;
    padding: 6px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
  }
  .rail-account-menu.hidden { display: none; }
  .app.rail-collapsed .rail-account-menu { left: calc(var(--rail-w) + 6px); bottom: 12px; }
  .app.rail-collapsed .rail-account-menu .rail-item { justify-content: flex-start; padding: 7px 10px; }
  .app.rail-collapsed .rail-account-menu .rail-label { position: static; width: auto; height: auto; clip: auto; }
  .app.rail-collapsed .rail-account-menu .rail-kbd { display: inline; }
  .app.rail-collapsed .rail-account-menu .rail-popover:not(.hidden) { display: block; }

  /* The conversation's own actions, beside its topic. */
  .conv-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 40px;
    padding: 8px 20px 0;
  }
  .conv-bar .session-header { flex: 1; min-width: 0; padding: 0; }
  .conv-actions { display: flex; flex-shrink: 0; gap: 6px; margin-left: auto; }
  .conv-action {
    min-height: 32px;
    padding: 5px 11px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-size: 12.5px;
    cursor: pointer;
  }
  .conv-action:hover { background: var(--accent-soft); }
  .conv-action[aria-pressed="true"],
  .conv-action[aria-expanded="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-text); font-weight: 600; }
  .app.session-ended .conv-wrap-up { display: none; }

  /* A visible ring for keyboard focus only. */
  .rail-item:focus-visible,
  .conv-action:focus-visible,
  .context-panel button:focus-visible,
  .context-panel a:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }

}

/* ---------- The context column: "This conversation" ----------
   From 1200px a column of its own, folded to a slim tab while it has
   nothing to show. From 900 to 1199px it opens beside the chat from the
   conversation bar. Below 900px it is a full-screen sheet opened from the
   compact bar's Map button. */
.context-panel {
  flex-direction: column;
  gap: 16px;
  min-height: 0;
  overflow-y: auto;
  padding: 16px;
  background: var(--surface);
}
.context-body { display: contents; }
.context-tab, .context-fold, .compact-context-btn { display: none; }
.context-head { display: flex; align-items: center; gap: 6px; }
.context-head-title { flex: 1; font-family: var(--serif); font-size: 17px; color: var(--ink); }
.context-empty { font-size: 13px; line-height: 1.55; color: var(--ink-soft); }
.context-empty.hidden, .context-section.hidden, .context-next.hidden { display: none; }
.context-section { display: flex; flex-direction: column; gap: 8px; }
.context-section-title { font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft); }
.context-next { font-size: 13px; line-height: 1.5; font-weight: 600; color: var(--ink); }
.context-artifacts { display: flex; flex-wrap: wrap; gap: 8px; }
.context-artifact {
  width: 72px;
  height: 72px;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg);
  cursor: pointer;
}
.context-artifact img { width: 100%; height: 100%; object-fit: cover; display: block; }
.context-artifact img.flagged-img { filter: blur(4px); opacity: 0.6; }
.context-artifact svg { width: 100%; height: 100%; display: block; background: var(--surface); pointer-events: none; }
.context-artifact-doc {
  width: auto;
  height: auto;
  max-width: 100%;
  padding: 8px 10px;
  font: inherit;
  font-size: 12px;
  color: var(--ink);
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.context-map { width: 100%; height: auto; }
.context-map .map-edge { stroke: var(--border-strong, var(--ink-soft)); stroke-width: 1.5; }
.context-map .map-edge.cross { stroke-dasharray: 4 3; }
.context-map .map-edge.same { stroke-dasharray: 1 4; }
.context-map .map-node { fill: var(--bg); stroke: var(--accent); stroke-width: 1.5; }
.context-map .map-node.here { fill: var(--accent); }
.context-map .map-label { font-size: 10.5px; fill: var(--ink); }
.context-map .map-label.here { font-weight: 600; }
.context-map-list { display: flex; flex-direction: column; gap: 4px; font-size: 12.5px; color: var(--ink); }
.context-map-list .context-link-kind { color: var(--ink-soft); }
.concept-map { width: 100%; height: auto; }
.concept-map .cm-edge { stroke: var(--ink-soft); stroke-width: 1.25; fill: none; }
.concept-map .cm-edge.new { stroke: var(--accent); stroke-width: 2; }
.concept-map .cm-arrow { fill: var(--ink-soft); }
.concept-map .cm-arrow.new { fill: var(--accent); }
.concept-map .cm-edge-label { font-size: 11px; fill: var(--ink-soft); }
.concept-map .cm-edge-label.new { fill: var(--accent-text); font-weight: 600; }
.concept-map .cm-edge-label-bg { fill: var(--surface); }
.concept-map .cm-node { fill: var(--bg); stroke: var(--border); stroke-width: 1; }
.concept-map .cm-node.root { stroke: var(--accent); stroke-width: 1.5; }
.concept-map .cm-node.new { stroke: var(--accent); stroke-width: 1.5; }
.concept-map .cm-node-text { fill: var(--ink); font-size: 12.5px; }
.concept-map .cm-idea { cursor: pointer; outline: none; }
.concept-map .cm-idea:hover .cm-node { fill: var(--accent-soft); }
.concept-map .cm-idea:focus-visible .cm-node { stroke: var(--accent-text); stroke-width: 2.5; }
.concept-map-list { display: flex; flex-direction: column; gap: 3px; font-size: 12.5px; color: var(--ink); }
.concept-map-list .new { color: var(--accent-text); font-weight: 600; }
.context-still-open { display: flex; flex-direction: column; gap: 4px; font-size: 13px; line-height: 1.5; color: var(--ink); }
.context-panel button:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }

/* Something she jumped to from the column, outlined for a moment. */
.context-flash { animation: context-flash 1.5s ease-out; border-radius: 12px; }
@keyframes context-flash {
  0%, 40% { box-shadow: 0 0 0 3px var(--accent); }
  100% { box-shadow: 0 0 0 3px transparent; }
}
@media (prefers-reduced-motion: reduce) { .context-flash { animation: none; box-shadow: 0 0 0 3px var(--accent); } }

@media (max-width: 899px) {
  .compact-context-btn {
    display: inline-flex;
    flex-shrink: 0;
    align-items: center;
    margin-left: auto;
    min-height: 36px;
    padding: 6px 14px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg);
    color: var(--ink);
    font: inherit;
    font-size: 13px;
    font-weight: 600;
  }
  .app.context-open .context-panel {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 420;
    padding: max(16px, env(safe-area-inset-top)) 20px max(20px, env(safe-area-inset-bottom));
  }
}

@media (min-width: 900px) and (max-width: 1199px) {
  .app.context-open { grid-template-columns: var(--rail-w) minmax(0, 1fr) 300px; }
  .app.context-open .context-panel { display: flex; border-left: 1px solid var(--border); }
  .app.context-open.scratch-open { grid-template-columns: var(--rail-w) minmax(0, 1fr); }
}

@media (min-width: 1200px) {
  .app { grid-template-columns: var(--rail-w) minmax(0, 1fr) var(--ctx-w, 320px); }
  .app.context-folded { --ctx-w: 44px; }
  .context-panel { display: flex; border-left: 1px solid var(--border); }
  .context-close, .conv-context-toggle { display: none; }
  .context-fold { display: inline-flex; }
  .app.context-folded .context-panel { padding: 12px 0; overflow: hidden; }
  .app.context-folded .context-body { display: none; }
  .app.context-folded .context-tab {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    flex: 1;
    padding: 12px 0;
    border: 0;
    background: none;
    color: var(--ink-soft);
    font: inherit;
    font-size: 12.5px;
    letter-spacing: 0.04em;
    cursor: pointer;
  }
  .app.context-folded .context-tab:hover { color: var(--ink); background: var(--accent-soft); }
  .context-tab-label { writing-mode: vertical-rl; transform: rotate(180deg); white-space: nowrap; }
  .context-tab-dot { position: absolute; top: 14px; left: 50%; width: 8px; height: 8px; margin-left: -4px; border-radius: 50%; background: var(--accent); }
  .context-tab-dot.hidden { display: none; }
}

/* Finger-sized rows wherever the rail is tapped rather than clicked (her
   iPad in landscape, where the rail is the only navigation). */
@media (min-width: 900px) and (pointer: coarse) {
  .rail-item { min-height: 44px; }
  .conv-action { min-height: 44px; padding-inline: 14px; }
}

/* Keyboard shortcuts list. A centred card over a dimmed page; appears
   without animation. */
.shortcuts-dialog {
  position: fixed;
  inset: 0;
  z-index: 460;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: color-mix(in srgb, var(--ink) 35%, transparent);
}
.shortcuts-dialog.hidden { display: none; }
.shortcuts-card {
  width: 100%;
  max-width: 420px;
  max-height: 100%;
  overflow-y: auto;
  padding: 18px 20px;
  border-radius: 14px;
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
}
.shortcuts-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.shortcuts-title { font-family: var(--serif); font-size: 18px; }
.shortcuts-list { margin: 0; display: flex; flex-direction: column; gap: 8px; }
.shortcuts-list > div { display: flex; align-items: baseline; gap: 12px; }
.shortcuts-list dt { flex: 0 0 104px; margin: 0; }
.shortcuts-list dd { margin: 0; font-size: 13.5px; }
.shortcuts-dialog kbd {
  display: inline-block;
  min-width: 20px;
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--bg);
  font-family: inherit;
  font-size: 12px;
  text-align: center;
  color: var(--ink);
}
/* Only listed where they do something: [ needs the rail, ] the slide-over. */
.shortcut-wide, .shortcut-ipad { display: none !important; }
@media (min-width: 900px) { .shortcut-wide { display: flex !important; } }
@media (min-width: 900px) and (max-width: 1199px) { .shortcut-ipad { display: flex !important; } }
.shortcuts-toggle { display: flex; align-items: center; gap: 8px; margin-top: 16px; font-size: 13.5px; }
.shortcuts-note { margin-top: 4px; font-size: 12px; color: var(--ink-soft); }

/* My Projects: one card per project, its actions under the text. */
.project-card { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; text-align: left; }
.project-card-name { font-weight: 600; color: var(--ink); }
.project-card-meta { font-size: 12.5px; color: var(--ink-soft); }
.project-card-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 2px; }

/* iPhone and iPad Safari zoom the whole page in when a text box with text
   smaller than 16px is tapped, and stay zoomed -- she then had to pinch the
   app back to fit (reported 2026-09-27, iPhone 17). On touch screens every
   text box is at least 16px, which is the threshold Safari checks. Desktop
   sizes are unchanged. */
@media (pointer: coarse) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  textarea,
  select { font-size: 16px !important; }
}

/* Context she brought in from another conversation: readable, at the top
   of the chat, for as long as the conversation uses it. */
.brought-in-card {
  align-self: stretch;
  margin: 4px 0 8px;
  padding: 10px 14px;
  border: 1px dashed var(--accent-text);
  border-radius: 12px;
  background: var(--surface);
  color: var(--ink);
}
.brought-in-head { font-size: 12.5px; font-weight: 600; color: var(--accent-text); margin-bottom: 4px; }
.brought-in-body { font-size: 13.5px; line-height: 1.5; white-space: pre-wrap; }
.brought-in-note { display: flex; align-items: center; gap: 10px; margin-top: 6px; font-size: 12px; color: var(--ink-soft); }
.context-brought-in { font-size: 13px; line-height: 1.5; color: var(--ink); white-space: pre-wrap; }

/* Flashcard decks (Practice). The card is big and obviously tappable; the
   two sorting buttons sit either side of Flip, in the directions of the
   swipes they stand for. touch-action lets a horizontal swipe reach the
   card while vertical scrolling still scrolls the chat. */
.msg.coach.has-deck { max-width: 94%; }

/* Textbook formatting in coach replies (renderTextbookBoxes in app.js).
   One family: every box is a filled, tinted card with no edge, and an
   app-written label led by its own mark -- ◆ Definition, ✱ Key idea,
   ▸ Worked example, ✓ Check, ✦ Your turn. The tint says what kind of
   thing it is before she reads it; the mark and the words say it without
   colour. (Edges were dropped with Your turn's: in Practice the bubble that
   hands work back already carries the gold edge -- design review
   2026-09-28.) Each fill and label colour is a --tb-* token pair, checked
   to pass AA in both themes. */
.msg.coach .tb { margin: 10px 0; padding: 12px 14px; border: 0; border-radius: var(--radius-md); background: var(--tb-fill); }
.msg.coach .tb > :last-child { margin-bottom: 0; }
.msg.coach .tb p:first-of-type { margin-top: 0; }
.tb-label { display: flex; align-items: baseline; gap: 6px; margin-bottom: 4px; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--tb-text); }
.tb-label::before { content: var(--tb-mark); letter-spacing: 0; }
.tb-label .katex { text-transform: none; letter-spacing: normal; }
.tb-definition { --tb-fill: var(--tb-definition-fill); --tb-text: var(--tb-definition-text); --tb-mark: '◆'; }
.tb-key { --tb-fill: var(--tb-key-fill); --tb-text: var(--tb-key-text); --tb-mark: '✱'; }
.tb-example { --tb-fill: var(--tb-example-fill); --tb-text: var(--tb-example-text); --tb-mark: '▸'; }
.tb-check { --tb-fill: var(--tb-check-fill); --tb-text: var(--tb-check-text); --tb-mark: '✓'; }
.tb-try { --tb-fill: var(--try-fill); --tb-text: var(--star-gold-text); --tb-mark: '✦'; }
/* Maths house style: her working shown back, one tip, the answer she
   reached. The answer is an outline in gold rather than a fill, sized to
   what's in it. */
.tb-working { --tb-fill: var(--tb-check-fill); --tb-text: var(--tb-check-text); --tb-mark: '✎'; }
.tb-tip { --tb-fill: var(--tb-tip-fill); --tb-text: var(--tb-tip-text); --tb-mark: '💡'; }
/* Core style: her writing shown back (same green as her working), and a
   quoted source set like a quotation. */
.tb-draft { --tb-fill: var(--tb-check-fill); --tb-text: var(--tb-check-text); --tb-mark: '✎'; }
.tb-source { --tb-fill: var(--tb-example-fill); --tb-text: var(--tb-example-text); --tb-mark: '❝'; }
/* Visual Response Grammar (2026-10-27): steps, compare and timeline are laid
   out by the app from plain lines, so they are exact and need no image model.
   Static on purpose (section 119): no reveal, nothing moves. */
.tb-steps { --tb-fill: var(--tb-definition-fill); --tb-text: var(--tb-definition-text); --tb-mark: '→'; }
.tb-compare { --tb-fill: var(--tb-example-fill); --tb-text: var(--tb-example-text); --tb-mark: '⇄'; }
.tb-timeline { --tb-fill: var(--tb-example-fill); --tb-text: var(--tb-example-text); --tb-mark: '◷'; }
.msg.coach .tb-steps-list, .msg.coach .tb-timeline-list { list-style: none; margin: 6px 0 0; padding: 0; }
.msg.coach .tb-step { position: relative; display: flex; gap: 12px; align-items: flex-start; padding: 0 0 12px; margin: 0; }
.msg.coach .tb-step:last-child { padding-bottom: 0; }
/* The line joining one number to the next. */
.msg.coach .tb-step:not(:last-child)::after { content: ''; position: absolute; left: 11px; top: 26px; bottom: 2px; width: 2px; border-radius: 1px; background: var(--tb-text); opacity: 0.25; }
.tb-step-n { flex: 0 0 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; background: var(--tb-text); color: var(--tb-fill); font-size: 0.8rem; font-weight: 700; line-height: 1; }
.tb-step-text { flex: 1; min-width: 0; padding-top: 2px; }
/* One grid for the whole list, so every date column lines up. */
.msg.coach .tb-timeline-list { display: grid; grid-template-columns: max-content 1fr; column-gap: 12px; }
.msg.coach .tb-event { display: contents; }
.tb-when { padding-bottom: 10px; font-weight: 700; color: var(--tb-text); white-space: nowrap; }
.tb-what { min-width: 0; padding: 0 0 10px 12px; border-left: 2px solid color-mix(in srgb, var(--tb-text) 30%, transparent); }
.tb-event:last-child > .tb-when, .tb-event:last-child > .tb-what { padding-bottom: 0; }
.msg.coach .tb-compare-table { width: 100%; margin: 6px 0 0; border-collapse: collapse; font-size: inherit; }
.tb-compare-table th, .tb-compare-table td { padding: 8px 10px; text-align: left; vertical-align: top; border-bottom: 1px solid color-mix(in srgb, var(--tb-text) 18%, transparent); }
.tb-compare-table tbody tr:last-child th, .tb-compare-table tbody tr:last-child td { border-bottom: 0; }
.tb-compare-table thead th { color: var(--tb-text); font-weight: 700; }
.tb-compare-table tbody th { width: 28%; font-weight: 600; color: var(--ink-soft); }
/* Each cell's side, named only in the stacked phone layout below. */
.tb-compare-table .tb-side { display: none; font-weight: 700; color: var(--tb-text); }
/* A closing question under the layout gets a little room from it; when the
   layout ends the box, .tb > :last-child takes the margin away again. */
.msg.coach .tb-steps-list, .msg.coach .tb-timeline-list, .msg.coach .tb-compare-table { margin-bottom: 10px; }
/* An empty slot she fills in (attempt first, sections 5 and 6). */
.tb-blank { display: inline-block; min-width: 6em; padding: 1px 10px; border: 1.5px dashed var(--star-gold); border-radius: 6px; color: var(--star-gold-text); font-size: 0.85em; font-weight: 600; }
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* On a phone the comparison stacks: one card per aspect, each side named. */
@media (max-width: 520px) {
  .tb-compare-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .tb-compare-table, .tb-compare-table tbody, .tb-compare-table tr, .tb-compare-table th, .tb-compare-table td { display: block; width: auto; }
  .tb-compare-table tbody tr { padding: 8px 0; border-bottom: 1px solid color-mix(in srgb, var(--tb-text) 18%, transparent); }
  .tb-compare-table tbody tr:last-child { border-bottom: 0; }
  .tb-compare-table tbody th, .tb-compare-table td { padding: 2px 0; border: 0; width: auto; }
  .tb-compare-table .tb-side { display: inline; }
}
.msg.coach .tb-source > :not(.tb-label) { font-family: Georgia, "Times New Roman", serif; }
.msg.coach .tb-draft p { margin: 8px 0 0; }
.msg.coach .tb-draft p:first-of-type { margin-top: 0; }
.msg.coach .tb-comment { margin: 2px 0 10px 18px !important; font-size: 0.92em; color: var(--ink-soft); }
.nv-mark { background: var(--star-gold-tint); color: inherit; border-radius: 3px; padding: 0 2px; box-shadow: inset 0 -2px 0 var(--star-gold); }
.msg.coach .tb-answer { display: flex; width: fit-content; max-width: 100%; align-items: baseline; flex-wrap: wrap; gap: 4px 12px; padding: 8px 16px; border: 2px solid var(--star-gold); background: transparent; }
.msg.coach .tb-answer .tb-label { margin: 0; color: var(--star-gold-text); }
.msg.coach .tb-answer .tb-label::before { content: none; }
.msg.coach .tb-answer p { margin: 0; }
.msg.coach .tb-answer .katex-display { margin: 0; }
/* Marks inside the maths (app.js MATH_MACROS). */
.nv-hl { color: var(--star-gold-text); background: var(--star-gold-tint); border-radius: 4px; padding: 0 0.12em; font-weight: 600; }
.nv-ok { color: var(--tb-check-text); }
.nv-no { color: var(--tb-wrong-text); }
.nv-wrong { background: var(--tb-wrong-tint); border-radius: 4px; box-shadow: 0 0 0 2px var(--tb-wrong-tint); }
.nv-note { color: var(--ink-soft); }
/* Maths (KaTeX). A long line of working scrolls sideways inside the
   bubble rather than widening the page on a phone. */
.msg.coach .katex { font-size: 1.08em; white-space: nowrap; } /* "b =" / "8" split across lines otherwise; long working goes in \[ \] */
.msg.coach .katex-display { margin: 0.5em 0; overflow-x: auto; overflow-y: hidden; padding: 2px 0; }

/* Exact graphs (graph.js) -- drawn from the coach's numbers, so they are
   crisp at any size and follow the theme. Colours are the palette's
   non-text accents (3:1 is the bar for lines and dots); every word on the
   graph is ink, never a colour. */
.msg.coach.has-graph { max-width: 94%; }
.graph { margin: 10px 0 0; max-width: 440px; }
.graph-title { font-size: 0.9rem; font-weight: 600; color: var(--ink); margin-bottom: 6px; }
.graph-svg { display: block; width: 100%; height: auto; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); }
.graph-svg text { font-family: var(--sans); font-size: 15px; fill: var(--ink-soft); }
.graph-svg .g-axis-name { font-style: italic; font-size: 16px; }
.graph-svg .g-label { font-size: 16px; font-weight: 600; fill: var(--ink); paint-order: stroke; stroke: var(--surface); stroke-width: 3px; }
.graph-svg .g-grid line { stroke: var(--border); stroke-width: 1; }
.graph-svg .g-axis line { stroke: var(--ink-soft); stroke-width: 1.5; }
.graph-svg .g-caxis { color: var(--ink-soft); }
.graph-svg .g-stroke { stroke: currentColor; stroke-width: 2.5; stroke-linecap: round; fill: none; }
.graph-svg .g-thick { stroke-width: 4; }
.graph-svg .g-dashed { stroke-dasharray: 9 7; stroke-linecap: butt; }
.graph-svg .g-shade { fill: currentColor; fill-opacity: 0.16; stroke: none; }
.graph-swatch-dashed { background: repeating-linear-gradient(90deg, currentColor 0 5px, transparent 5px 8px); }
.graph-svg .g-fill { fill: currentColor; }
.graph-svg .g-closed { fill: currentColor; stroke: currentColor; stroke-width: 2; }
.graph-svg .g-open { fill: var(--surface); stroke: currentColor; stroke-width: 2.5; }
.g-c0 { color: var(--indigo-accent); }
.g-c1 { color: var(--status-error); }
.g-c2 { color: var(--status-strong); }
.g-c3 { color: var(--star-gold-dark); }
.graph-legend { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 0.85rem; color: var(--ink); }
.graph-legend li { display: flex; align-items: center; gap: 6px; }
.graph-swatch { width: 16px; height: 3px; border-radius: 2px; background: currentColor; }
.deck { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; outline: none; }
.deck:focus-visible { box-shadow: 0 0 0 2px var(--accent-text); border-radius: 12px; }
.deck-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.deck-title { font-weight: 600; font-size: 13.5px; color: var(--ink); }
.deck-progress { font-size: 12.5px; color: var(--ink-soft); }
.deck-card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  min-height: 150px;
  width: 100%;
  padding: 18px 16px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--bg);
  color: var(--ink);
  font: inherit;
  text-align: center;
  cursor: pointer;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
}
.deck-card.flipped { background: var(--accent-soft); border-color: var(--accent-text); }
.deck-card.hidden, .deck-done.hidden, .deck-actions.hidden { display: none; }
.deck-side { font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft); }
.deck-text { font-size: 17px; line-height: 1.4; }
.deck-hint { font-size: 12px; color: var(--ink-soft); }
.deck-actions { display: flex; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.deck-done { display: flex; flex-direction: column; gap: 8px; }
.deck-score { font-size: 15px; font-weight: 600; color: var(--ink); }
.deck-sent { margin: 4px 0 0; font-size: 13px; color: var(--status-strong, var(--ink-soft)); }
.deck-learning-label { font-size: 12.5px; color: var(--ink-soft); }
.deck-learning { margin: 0; padding-left: 18px; font-size: 13.5px; color: var(--ink); }



/* ---------- Scratchpad (scratchpad.js) ----------
   Phone: a full-screen sheet over the chat. From 700px (iPad, either way
   round): the right half of the screen, with the chat still usable on the
   left. Opens and closes without animation, like the other panels. The
   paper follows the theme; the picture she sends is always ink on white. */
.scratchpad {
  position: fixed;
  inset: 0;
  height: var(--app-vh, 100dvh);
  z-index: 440;
  display: flex;
  flex-direction: column;
  background: var(--canvas);
  color: var(--ink);
}
.scratchpad.hidden { display: none; }
.sp-head { flex-shrink: 0; display: flex; align-items: center; gap: 10px; padding: 10px 12px; padding-top: max(10px, env(safe-area-inset-top)); border-bottom: 1px solid var(--border); background: var(--surface); }
.sp-title { font-weight: 600; font-size: 0.95rem; white-space: nowrap; }
.sp-paper { display: inline-flex; margin-left: auto; border: 1px solid var(--border); border-radius: 999px; padding: 2px; background: var(--canvas); }
.sp-paper button { border: 0; background: transparent; color: var(--ink-soft); font: inherit; font-size: 0.82rem; padding: 6px 12px; border-radius: 999px; cursor: pointer; min-height: 32px; }
.sp-paper button[aria-checked="true"] { background: var(--surface-raised); color: var(--ink); font-weight: 600; }
.sp-close { flex-shrink: 0; font-size: 20px; line-height: 1; width: 36px; height: 36px; }
.sp-question { flex-shrink: 0; max-height: 28%; overflow-y: auto; padding: 10px 14px; background: var(--try-fill); border-bottom: 1px solid var(--border); font-size: 0.95rem; line-height: 1.45; }
.sp-question p { margin: 0 0 0.4em; }
.sp-question p:last-child { margin-bottom: 0; }
.sp-question .katex { font-size: 1.05em; }
.sp-page { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; background: var(--surface); }
.sp-sheet { position: relative; width: 100%; }
.sp-sheet.sp-grid {
  background-image: linear-gradient(to right, var(--sp-grid) 1px, transparent 1px), linear-gradient(to bottom, var(--sp-grid) 1px, transparent 1px);
}
.sp-canvas { display: block; touch-action: none; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; cursor: crosshair; }
.sp-more { display: block; width: 100%; padding: 14px; border: 0; border-top: 1px dashed var(--border); background: var(--canvas); color: var(--ink-soft); font: inherit; font-size: 0.9rem; cursor: pointer; }
.sp-more.hidden { display: none; }
.sp-tools {
  flex-shrink: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 8px 10px;
  padding-bottom: max(8px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
  background: var(--surface);
}
.sp-tool { min-width: 40px; height: 40px; padding: 0 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--canvas); color: var(--ink); font: inherit; font-size: 1.05rem; cursor: pointer; }
.sp-tool[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent-text); color: var(--accent-text); }
.sp-tool:disabled { opacity: 0.4; cursor: default; }
.sp-clear { font-size: 0.85rem; }
.sp-inks { display: inline-flex; gap: 4px; }
.sp-ink { width: 40px; height: 40px; border: 1px solid var(--border); border-radius: 10px; background: var(--canvas); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
.sp-ink i { width: 18px; height: 18px; border-radius: 50%; display: block; }
.sp-ink[data-ink="0"] i { background: var(--ink); }
.sp-ink[data-ink="1"] i { background: var(--sp-blue); }
.sp-ink[data-ink="2"] i { background: var(--sp-red); }
.sp-ink[aria-pressed="true"] { border-color: var(--accent-text); box-shadow: 0 0 0 2px var(--accent-soft); }
.sp-thick i { display: block; width: 18px; height: 6px; border-radius: 3px; background: currentColor; }
.sp-status { font-size: 0.78rem; color: var(--ink-soft); margin-left: auto; white-space: nowrap; }
.sp-send { height: 40px; padding: 0 16px; border: 0; border-radius: 999px; background: var(--star-gold-core); color: var(--star-gold-on); font: inherit; font-weight: 600; cursor: pointer; }
.sp-send:disabled { opacity: 0.45; cursor: default; }
.sp-type { font-size: 0.8rem; font-weight: 700; letter-spacing: 0.02em; }

/* Typing: the square she's on, and the floating number pad. */
.sp-cursor { position: absolute; pointer-events: none; box-sizing: border-box; border: 2px solid var(--accent-text); border-radius: 3px; background: var(--accent-soft); opacity: 0.85; }
.sp-cursor.hidden { display: none; }
.sp-cursor-pow::after { content: "n"; position: absolute; top: 1px; right: 3px; font-size: 10px; font-weight: 700; line-height: 1; color: var(--accent-text); }
.sp-overlay { position: absolute; left: 0; top: 0; pointer-events: none; }
.sp-keys {
  position: absolute;
  z-index: 2;
  width: 264px;
  padding: 0 8px 8px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-raised);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
}
.sp-keys.hidden { display: none; }
.sp-keys-bar { display: flex; align-items: center; gap: 6px; height: 38px; cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; }
.sp-keys-grip { width: 22px; height: 10px; flex-shrink: 0; margin-right: auto; background-image: radial-gradient(circle, var(--ink-soft) 1.2px, transparent 1.6px); background-size: 5.5px 5px; opacity: 0.7; }
.sp-keys-act { height: 28px; padding: 0 10px; border: 1px solid var(--border); border-radius: 999px; background: var(--canvas); color: var(--ink); font: inherit; font-size: 0.74rem; font-weight: 600; cursor: pointer; white-space: nowrap; }
.sp-keys-act:disabled { opacity: 0.4; cursor: default; }
.sp-keys-close { width: 28px; height: 28px; border: 0; border-radius: 8px; background: transparent; color: var(--ink-soft); font: inherit; font-size: 18px; line-height: 1; cursor: pointer; }
.sp-kits { display: flex; margin-bottom: 6px; border: 1px solid var(--border); border-radius: 999px; padding: 2px; background: var(--canvas); }
.sp-kits button { flex: 1; border: 0; background: transparent; color: var(--ink-soft); font: inherit; font-size: 0.76rem; padding: 5px 0; border-radius: 999px; cursor: pointer; }
.sp-kits button[aria-checked="true"] { background: var(--surface-raised); color: var(--ink); font-weight: 600; box-shadow: 0 0 0 1px var(--border); }
.sp-kit-tools { display: grid; grid-template-columns: repeat(6, 1fr); gap: 4px; margin-bottom: 6px; }
.sp-kit-tools.hidden, .sp-kit-hint.hidden, .sp-keys-grid.hidden { display: none; }
.sp-kit-tool { height: 36px; padding: 0; border: 1px solid var(--border); border-radius: 9px; background: var(--canvas); color: var(--ink); font: inherit; font-size: 0.78rem; font-weight: 700; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; touch-action: manipulation; }
.sp-kit-tool[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent-text); color: var(--accent-text); }
.sp-kit-hint { padding: 4px 2px 0; font-size: 0.8rem; color: var(--ink-soft); }
.sp-keys-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 4px; }
.sp-keys-extra { margin-top: 6px; padding-top: 6px; border-top: 1px dashed var(--border); }
.sp-keys-extra:empty { display: none; }
.sp-key { height: 38px; padding: 0; border: 1px solid var(--border); border-radius: 9px; background: var(--canvas); color: var(--ink); font: inherit; font-size: 1.05rem; cursor: pointer; touch-action: manipulation; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
.sp-key-digit { font-weight: 600; background: var(--surface); }
.sp-key-move { color: var(--ink-soft); font-size: 0.95rem; }
.sp-key[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent-text); color: var(--accent-text); }
.sp-key:active { background: var(--accent-soft); }
.sp-key-frac { display: inline-flex; flex-direction: column; align-items: center; font-size: 0.7rem; font-style: italic; line-height: 1.05; }
.sp-key-frac i:first-child { border-bottom: 1.5px solid currentColor; padding: 0 3px; }

/* The button on each Your turn card. */
.msg.coach .tb-scratch { display: inline-flex; align-items: center; gap: 4px; margin-top: 8px; padding: 6px 12px; border: 1px solid var(--star-gold-dark); border-radius: 999px; background: transparent; color: var(--star-gold-text); font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer; }

@media (min-width: 700px) {
  .scratchpad { left: auto; width: 50vw; border-left: 1px solid var(--border); z-index: 300; }
  .app.scratch-open { margin: 0; max-width: none; width: 50vw; }
  .app.scratch-open .context-panel { display: none !important; }
}
@media (min-width: 1200px) {
  .app.scratch-open { grid-template-columns: var(--rail-w) minmax(0, 1fr); }
}

/* ---------- Flagged pictures and exact diagrams ----------
   A flagged picture sits dimmed under the problem its check found until she
   chooses to look. An exact diagram's card waits under the reply, then holds
   the diagram; it opens in the side panel where the scratchpad goes. */
.visual-flagged { position: relative; margin: 4px 0 8px; }
.visual-flag-note { padding: 10px 12px; margin-bottom: 8px; border: 1px solid var(--star-gold-dark); border-radius: 12px; background: var(--try-fill); font-size: 0.9rem; line-height: 1.4; }
.visual-flag-note strong { display: block; margin-bottom: 4px; }
.visual-flag-note p { margin: 0 0 8px; color: var(--ink); }
.visual-flag-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.visual-flag-btn, .draw-exactly-btn { min-height: 36px; padding: 6px 14px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--ink); font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer; }
.visual-flag-primary, .draw-exactly-btn { border-color: var(--star-gold-dark); color: var(--star-gold-text); background: transparent; }
.draw-exactly-btn { display: inline-flex; margin-top: 8px; }
.visual-flag-btn:disabled, .draw-exactly-btn:disabled { opacity: 0.5; cursor: default; }
.visual-flagged .flagged-img { filter: blur(7px) grayscale(0.6); opacity: 0.45; cursor: pointer; }
.visual-flagged.revealed .flagged-img { filter: none; opacity: 1; cursor: zoom-in; outline: 3px dashed var(--star-gold-dark); outline-offset: 3px; }
.diagram-card { margin-top: 10px; }
.diagram-wait { margin: 0; padding: 10px 12px; border: 1px dashed var(--border); border-radius: 12px; background: var(--surface); color: var(--ink-soft); font-size: 0.88rem; }
.diagram-card .draw-exactly-btn { margin-top: 8px; }
.diagram-open { display: block; width: 100%; max-width: 520px; padding: 0; border: 1px solid var(--border); border-radius: 12px; overflow: hidden; background: #FFFFFF; cursor: zoom-in; }
.diagram-open img { display: block; width: 100%; height: auto; }
.diagram-tag { display: inline-block; margin-top: 6px; font-size: 0.8rem; font-weight: 600; color: var(--ink-soft); }
.diagram-tag.ok { color: var(--tb-check-text); }
.diagram-tag.warn { color: var(--star-gold-text); }
.diagram-ready-bar { position: fixed; left: 50%; transform: translateX(-50%); top: calc(12px + env(safe-area-inset-top)); white-space: nowrap; z-index: 460; display: flex; align-items: center; gap: 10px; max-width: calc(100vw - 32px); padding: 8px 8px 8px 14px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-raised); color: var(--ink); box-shadow: 0 6px 22px rgba(0, 0, 0, 0.16); font-size: 0.9rem; font-weight: 600; }
.diagram-ready-bar button { min-height: 32px; padding: 4px 14px; border: 0; border-radius: 999px; background: var(--star-gold-core); color: var(--star-gold-on); font: inherit; font-weight: 600; cursor: pointer; }
.diagram-ready-bar .diagram-ready-close { background: transparent; color: var(--ink-soft); padding: 4px 8px; font-size: 18px; }
.diagram-panel { position: fixed; inset: 0; height: var(--app-vh, 100dvh); z-index: 440; display: flex; flex-direction: column; background: var(--canvas); color: var(--ink); }
.diagram-panel.hidden { display: none; }
.dp-head { flex-shrink: 0; display: flex; align-items: center; gap: 10px; padding: 10px 12px; padding-top: max(10px, env(safe-area-inset-top)); border-bottom: 1px solid var(--border); background: var(--surface); }
.dp-title { font-weight: 600; white-space: nowrap; }
.dp-tag { margin: 0 0 0 auto; text-align: right; }
.dp-close { flex-shrink: 0; font-size: 20px; width: 36px; height: 36px; }
.dp-body { flex: 1; min-height: 0; overflow: auto; padding: 16px; padding-bottom: max(16px, env(safe-area-inset-bottom)); }
.dp-body img { display: block; width: 100%; height: auto; border-radius: 12px; background: #FFFFFF; border: 1px solid var(--border); }
@media (min-width: 700px) {
  .diagram-panel { left: auto; width: 50vw; border-left: 1px solid var(--border); z-index: 300; }
}

/* ---------- Timed test (mocktest.js) ----------
   A full-screen sheet on every size -- a test gets her whole attention.
   With the scratchpad open from 700px up, the test takes the left half and
   the pad the right. No animation anywhere, the clock included: the time
   changes, nothing pulses or flashes. */
.mock {
  position: fixed;
  inset: 0;
  height: var(--app-vh, 100dvh);
  z-index: 430;
  display: flex;
  justify-content: center;
  background: var(--canvas);
  color: var(--ink);
}
.mock.hidden { display: none; }
body.mock-open .scratchpad { z-index: 450; }
@media (min-width: 700px) {
  .mock.with-scratch { right: 50vw; }
}
.mock-sheet { display: flex; flex-direction: column; width: 100%; max-width: 760px; min-height: 0; }
.mock-head { flex-shrink: 0; display: flex; align-items: center; gap: 10px; padding: 10px 14px; padding-top: max(10px, env(safe-area-inset-top)); border-bottom: 1px solid var(--border); }
.mock-title { font-weight: 600; font-size: 0.95rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mock-x { font-size: 20px; width: 36px; height: 36px; }
.mock-body { flex: 1; min-height: 0; overflow-y: auto; padding: 16px; }
.mock-middle { display: flex; flex-direction: column; justify-content: center; max-width: 520px; margin: 0 auto; text-align: center; }
.mock-foot { flex-shrink: 0; display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; padding: 10px 14px; padding-bottom: max(10px, env(safe-area-inset-bottom)); border-top: 1px solid var(--border); }
.mock-run .mock-foot { justify-content: space-between; }
.mock-big { font-size: 1.35rem; font-weight: 600; margin: 0 0 8px; }
.mock-note { color: var(--ink-soft); font-size: 0.9rem; }
.mock-muted { color: var(--ink-soft); }
.mock-toast { width: 100%; margin: 0 0 4px; color: var(--status-error-text); font-size: 0.9rem; }
.mock-label { display: block; margin: 16px 0 6px; font-size: 0.8rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft); }
.mock-label:first-child { margin-top: 0; }
.mock-input { width: 100%; box-sizing: border-box; font: inherit; font-size: 16px; padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); color: var(--ink); }
.mock-input:focus { outline: 2px solid var(--border-focus); outline-offset: 1px; }
.mock-answer { font-size: 1.2rem; max-width: 320px; }
.mock-formats { display: grid; gap: 8px; }
.mock-format { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); color: var(--ink); font: inherit; text-align: left; cursor: pointer; }
.mock-format[aria-checked="true"] { border-color: var(--accent-text); box-shadow: 0 0 0 2px var(--accent-soft); }
.mock-format-title { font-weight: 600; }
.mock-format-sub { font-size: 0.85rem; color: var(--ink-soft); }
.mock-seg { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.mock-seg button { padding: 8px 12px; min-height: 40px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--ink); font: inherit; font-size: 0.88rem; cursor: pointer; }
.mock-seg button[aria-checked="true"] { border-color: var(--accent-text); background: var(--accent-soft); color: var(--accent-text); font-weight: 600; }
.mock-custom { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.mock-custom label { display: inline-flex; align-items: center; gap: 6px; font-size: 0.9rem; }
.mock-custom input { width: 72px; font: inherit; font-size: 16px; padding: 8px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); }
.mock-recent { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.mock-recent li { display: flex; justify-content: space-between; gap: 10px; font-size: 0.88rem; padding: 6px 0; border-bottom: 1px solid var(--border); }
.mock-recent-score { color: var(--ink-soft); white-space: nowrap; }
.mock-primary, .mock-secondary { min-height: 44px; padding: 0 18px; border-radius: 999px; font: inherit; font-weight: 600; cursor: pointer; }
.mock-primary { border: 0; background: var(--star-gold-core); color: var(--star-gold-on); }
.mock-secondary { border: 1px solid var(--border); background: var(--surface); color: var(--ink); }
.mock-primary:disabled, .mock-secondary:disabled { opacity: 0.45; cursor: default; }
.mock-small { min-height: 36px; padding: 0 12px; font-size: 0.85rem; }
.mock-clock { font-variant-numeric: tabular-nums; font-weight: 700; font-size: 1.1rem; padding: 4px 10px; border-radius: var(--radius-sm); background: var(--surface-raised); white-space: nowrap; }
.mock-clock.low { background: var(--status-error-soft); color: var(--status-error-text); }
.mock-warning { margin: 0; padding: 8px 14px; background: var(--status-developing-soft); color: var(--status-developing-text); font-weight: 600; font-size: 0.9rem; text-align: center; }
.mock-warning.hidden { display: none; }
.mock-strip { flex-shrink: 0; display: flex; gap: 6px; overflow-x: auto; padding: 10px 14px; border-bottom: 1px solid var(--border); scrollbar-width: thin; }
.mock-dot { flex-shrink: 0; width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--border); background: var(--surface); color: var(--ink-soft); font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer; position: relative; }
.mock-dot.answered { background: var(--surface-raised); color: var(--ink); }
.mock-dot.current { border: 2px solid var(--accent-text); color: var(--accent-text); }
.mock-dot.flagged::after { content: '⚑'; position: absolute; top: -6px; right: -4px; font-size: 11px; color: var(--status-error-text); }
.mock-qhead { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 10px; font-size: 0.85rem; font-weight: 600; color: var(--ink-soft); }
.mock-flag { padding: 6px 12px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--ink-soft); font: inherit; font-size: 0.85rem; cursor: pointer; }
.mock-flag[aria-pressed="true"] { border-color: var(--status-error-border); color: var(--status-error-text); background: var(--status-error-soft); }
.mock-question { font-size: 1.08rem; line-height: 1.55; margin-bottom: 16px; }
.mock-question p { margin: 0 0 0.6em; }
.mock-question .katex-display { overflow-x: auto; overflow-y: hidden; }
.mock-choices { display: grid; gap: 8px; }
.mock-choice { display: flex; align-items: center; gap: 12px; padding: 10px 14px; min-height: 48px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); color: var(--ink); font: inherit; text-align: left; cursor: pointer; }
.mock-choice[aria-checked="true"] { border-color: var(--accent-text); background: var(--accent-soft); }
.mock-choice-letter { flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--border); font-weight: 700; font-size: 0.85rem; }
.mock-choice[aria-checked="true"] .mock-choice-letter { background: var(--accent-text); color: var(--surface); border-color: var(--accent-text); }
.mock-score { font-size: 1rem; margin: 0 0 4px; }
.mock-score-big { font-size: 2rem; font-weight: 700; margin-right: 6px; font-variant-numeric: tabular-nums; }
.mock-res-list { list-style: none; margin: 12px 0 0; padding: 0; }
.mock-res { display: grid; grid-template-columns: 28px 44px 1fr auto; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 0.92rem; }
.mock-res-mark { font-weight: 700; text-align: center; }
.mock-res.correct .mock-res-mark { color: var(--status-strong-text); }
.mock-res.wrong .mock-res-mark { color: var(--status-error-text); }
.mock-res.blank .mock-res-mark { color: var(--ink-soft); }
.mock-res-q { font-weight: 600; }
.mock-res-given { overflow-wrap: anywhere; }
.mock-res-time { color: var(--ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* A parent's test family: the brand line reads "Test account" (app.js). */
body.test-account .brand-sub { color: var(--status-developing-text); font-weight: 700; }

/* ---------- US maps (usmap.js) ----------
   Drawn from real map data, in the theme's colours. Highlight groups use
   the graph palette (each with its own legend swatch); rivers are the
   scratchpad blue, parks the strong green. */
.msg.coach.has-map { max-width: 94%; }
.usmap { margin: 10px 0 0; }
.usmap-title { font-size: 0.9rem; font-weight: 600; color: var(--ink); margin-bottom: 6px; }
.usmap-frame { position: relative; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 6px; }
.usmap-svg { display: block; width: 100%; height: auto; }
.usmap-state { fill: var(--surface-raised); stroke: var(--surface); stroke-width: 0.9; cursor: pointer; }
.usmap-state.usmap-hl { fill-opacity: 0.6; }
.usmap-state.usmap-c0 { fill: var(--indigo-accent); }
.usmap-state.usmap-c1 { fill: var(--status-error); }
.usmap-state.usmap-c2 { fill: var(--status-strong); }
.usmap-state.usmap-c3 { fill: var(--star-gold-dark); }
.usmap-state.usmap-picked { stroke: var(--ink); stroke-width: 2; }
.usmap-nation { fill: none; stroke: var(--ink-soft); stroke-width: 0.9; pointer-events: none; }
.usmap-river { fill: none; stroke: var(--sp-blue); stroke-width: 1.6; stroke-linejoin: round; pointer-events: none; }
.usmap-river-name { font-family: var(--sans); font-size: 11px; font-weight: 700; fill: var(--sp-blue); paint-order: stroke; stroke: var(--surface); stroke-width: 3px; pointer-events: none; }
.usmap-label { font-family: var(--sans); font-size: 11px; font-weight: 700; fill: var(--ink); paint-order: stroke; stroke: var(--surface-raised); stroke-width: 2.5px; pointer-events: none; }
.usmap-label-name { font-size: 9px; }
.usmap-leader { stroke: var(--ink-soft); stroke-width: 0.7; vector-effect: non-scaling-stroke; }
/* Lines keep their width when the map is zoomed to a region. */
.usmap-state, .usmap-nation, .usmap-river { vector-effect: non-scaling-stroke; }
.usmap-park { fill: var(--status-strong); stroke: var(--surface); stroke-width: 0.8; pointer-events: none; }
.usmap-park.usmap-picked { fill: var(--ink); }
.usmap-park-hit { fill: transparent; cursor: pointer; }
.usmap-park-name { font-family: var(--sans); font-size: 9px; font-weight: 600; fill: var(--status-strong-text); paint-order: stroke; stroke: var(--surface); stroke-width: 2.5px; pointer-events: none; }
.usmap-tip { position: absolute; top: 8px; left: 8px; padding: 4px 10px; border-radius: 999px; background: var(--ink); color: var(--surface); font-size: 0.85rem; font-weight: 600; pointer-events: none; }
.usmap-tip.hidden { display: none; }
.usmap-legend { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 0.85rem; color: var(--ink); }
.usmap-legend li { display: flex; align-items: center; gap: 6px; }
.usmap-legend .usmap-note { color: var(--ink-soft); }
.usmap-swatch { width: 14px; height: 14px; border-radius: 3px; display: inline-block; }
.usmap-swatch.usmap-c0 { background: var(--indigo-accent); opacity: 0.6; }
.usmap-swatch.usmap-c1 { background: var(--status-error); opacity: 0.6; }
.usmap-swatch.usmap-c2 { background: var(--status-strong); opacity: 0.6; }
.usmap-swatch.usmap-c3 { background: var(--star-gold-dark); opacity: 0.6; }
.usmap-swatch-river { height: 3px; border-radius: 2px; background: var(--sp-blue); }
.usmap-swatch-park { width: auto; height: auto; color: var(--status-strong); font-size: 0.8rem; }
.usmap-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }
.usmap-hint { font-size: 0.8rem; color: var(--ink-soft); margin-right: auto; }
.usmap-btn { min-height: 36px; padding: 0 12px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--ink); font: inherit; font-size: 0.85rem; cursor: pointer; }
.usmap-slot { min-height: 120px; }
.usmap-zoom { position: fixed; inset: 0; z-index: 470; display: flex; flex-direction: column; background: var(--canvas); height: var(--app-vh, 100dvh); }
.usmap-zoom-head { flex-shrink: 0; display: flex; align-items: center; gap: 8px; padding: 10px 12px; padding-top: max(10px, env(safe-area-inset-top)); border-bottom: 1px solid var(--border); }
.usmap-zoom-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.usmap-zoom-tip { margin-right: auto; font-weight: 600; color: var(--accent-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.usmap-zoom-close { font-size: 20px; width: 36px; height: 36px; }
.usmap-zoom-body { flex: 1; min-height: 0; overflow: auto; padding: 8px; background: var(--surface); -webkit-overflow-scrolling: touch; }
.usmap-zoom-body .usmap-svg { max-width: none; }

/* "Nova has been updated -- Refresh" (app.js offerUpdate). Sits above
   everything, including a timed test, and appears without animation. */
.update-bar { position: fixed; z-index: 600; top: max(10px, env(safe-area-inset-top)); left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 10px; max-width: calc(100vw - 24px); padding: 8px 8px 8px 16px; border-radius: 999px; background: var(--ink); color: var(--surface); font-size: 0.9rem; font-weight: 600; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2); }
.update-bar span { white-space: nowrap; }
.update-bar button { min-height: 32px; padding: 0 14px; border: 0; border-radius: 999px; background: var(--star-gold-core); color: var(--star-gold-on); font: inherit; cursor: pointer; }
.update-bar .update-later { padding: 0 10px; background: transparent; color: var(--surface); font-size: 1.1rem; }

/* Lakes, physical regions, Canada / Mexico and the sea (usmap.js). Regions
   are see-through tints over the states -- each kind its own colour, with
   the kind also named in the legend -- so state borders stay readable
   underneath. */
.usmap-neighbour { fill: var(--canvas); stroke: var(--ink-soft); stroke-width: 0.6; vector-effect: non-scaling-stroke; }
.usmap-inset { fill: var(--surface); stroke: var(--border); stroke-width: 1; vector-effect: non-scaling-stroke; }
.usmap-lake { fill: var(--sp-blue); fill-opacity: 0.35; stroke: var(--sp-blue); stroke-width: 0.6; vector-effect: non-scaling-stroke; pointer-events: none; }
.usmap-lake-name { font-family: var(--sans); font-size: 9px; font-style: italic; font-weight: 600; fill: var(--sp-blue); paint-order: stroke; stroke: var(--surface); stroke-width: 2.5px; pointer-events: none; }
.usmap-region { fill-opacity: 0.32; stroke-width: 0.6; stroke-dasharray: 3 2; vector-effect: non-scaling-stroke; pointer-events: none; }
.usmap-region-mountains { fill: #8B5E3C; stroke: #8B5E3C; }
.usmap-region-plain { fill: #4E9A5B; stroke: #4E9A5B; }
.usmap-region-plateau { fill: #C9883A; stroke: #C9883A; }
.usmap-region-basin, .usmap-region-desert { fill: #C7A85E; stroke: #A88A40; }
.usmap-region-name { font-family: var(--sans); font-size: 10px; font-style: italic; font-weight: 700; letter-spacing: 0.04em; fill: var(--ink); paint-order: stroke; stroke: var(--surface); stroke-width: 3px; pointer-events: none; text-transform: uppercase; }
.usmap-country-name { font-family: var(--sans); font-size: 12px; font-weight: 700; letter-spacing: 0.25em; fill: var(--ink-soft); pointer-events: none; }
.usmap-ocean-name { font-family: var(--serif); font-size: 12px; font-style: italic; fill: var(--sp-blue); pointer-events: none; }
.usmap-swatch-lake { background: var(--sp-blue); opacity: 0.45; }
.usmap-region-swatch-mountains { background: #8B5E3C; opacity: 0.5; }
.usmap-region-swatch-plain { background: #4E9A5B; opacity: 0.5; }
.usmap-region-swatch-plateau { background: #C9883A; opacity: 0.5; }
.usmap-region-swatch-basin, .usmap-region-swatch-desert { background: #C7A85E; opacity: 0.6; }

/* Outline style (usmap.js "style": "outline"): a black-and-white
   worksheet map -- white states, black borders, black text -- the same in
   both themes, so what she sees is what prints. Highlights become light
   greys she can still tell apart; rivers and lakes stay a thin dark line. */
.usmap-frame-outline { background: #fff; border-color: #c9c9c9; }
.usmap-outline .usmap-state { fill: #fff; stroke: #111; stroke-width: 1; }
.usmap-outline .usmap-state.usmap-hl { fill-opacity: 1; }
.usmap-outline .usmap-state.usmap-c0 { fill: #d9d9d9; } .usmap-outline .usmap-state.usmap-c1 { fill: #bdbdbd; }
.usmap-outline .usmap-state.usmap-c2 { fill: #ececec; } .usmap-outline .usmap-state.usmap-c3 { fill: #a6a6a6; }
.usmap-outline .usmap-state.usmap-picked { stroke-width: 2.5; }
.usmap-outline .usmap-nation { stroke: #111; stroke-width: 1.3; }
.usmap-outline .usmap-river { stroke: #111; stroke-width: 1.4; }
.usmap-outline .usmap-lake { fill: #fff; fill-opacity: 1; stroke: #111; }
.usmap-outline .usmap-region { fill: none; stroke: #111; }
.usmap-outline .usmap-neighbour { fill: #f4f4f4; stroke: #555; }
.usmap-outline .usmap-inset { fill: #fff; stroke: #999; }
.usmap-outline .usmap-park { fill: #111; stroke: #fff; }
.usmap-outline .usmap-label, .usmap-outline .usmap-region-name, .usmap-outline .usmap-lake-name, .usmap-outline .usmap-river-name, .usmap-outline .usmap-park-name { fill: #111; stroke: #fff; }
.usmap-outline .usmap-country-name, .usmap-outline .usmap-ocean-name { fill: #444; }
.usmap-outline .usmap-leader { stroke: #444; }

/* Print / PDF on a phone: the map as a picture she can press and hold,
   with Share or print (app.js showMapPicture). */
.usmap-print-body { display: flex; align-items: center; justify-content: center; background: #fff; }
.usmap-print-body img { max-width: 100%; max-height: 100%; height: auto; -webkit-touch-callout: default; }
.usmap-print-foot { flex-shrink: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 12px; padding-bottom: max(10px, env(safe-area-inset-bottom)); border-top: 1px solid var(--border); background: var(--canvas); }
.usmap-btn-primary { margin-left: auto; border: 0; background: var(--star-gold-core); color: var(--star-gold-on); font-weight: 600; }
