/* ============ Chameleon — learning workspace ============
   Design system carried from the POC (IBM Carbon g10-ish tokens, spotlight
   tiles). The chat is new: one fused surface (composer + history) that
   yields to the workspace while the agent acts. */

:root {
  --bg: #0a0c10;
  --panel: #14171e;
  --line: rgba(255, 255, 255, 0.075);
  --line-2: rgba(255, 255, 255, 0.145);
  /* Matt: "the white text gets a little fatiguing after a while, I think we
     should make this a little more grey". Everything reading-surface hangs off
     this one token, so it moves as a set rather than a patchwork. */
  --text: #d3d9e3;
  --text-dim: #9aa4b4;
  --text-faint: #5e6774;
  --accent: #5d87ff;
  --accent-2: #8caaff;
  --good: #3ce2a5;
  --mid: #8b93a3;
  --bad: #ff5f8f;
  --warn: #f0a83c;
  --fill-1: rgba(255, 255, 255, 0.035);
  --fill-2: rgba(255, 255, 255, 0.065);
  --sh-md: inset 0 1px 0 rgba(255, 255, 255, 0.045), 0 10px 30px rgba(0, 0, 0, 0.35);
  --sh-lg: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 24px 60px rgba(0, 0, 0, 0.55);
  --ease-spring: cubic-bezier(0.24, 1.04, 0.34, 1);
  /* Motion vocabulary. Things ENTER with a little overshoot and LEAVE quickly
     and quietly — arrival deserves attention, departure does not. */
  --ease-out: cubic-bezier(0.16, 0.9, 0.28, 1);
  --ease-in: cubic-bezier(0.5, 0, 0.85, 0.3);
  --t-in: 380ms;
  --t-out: 200ms;
  --t-move: 300ms;
  --tile-t: 560ms;
  --r: 14px;
  --dock-w: min(720px, 92vw);
  --dock-side-w: 400px;
  /* the conversation's column: centred while there is nothing else on screen,
     narrow on the left once there is. One set of numbers, used by the pins,
     the transcript and the composer alike — they are one surface. */
  --talk-w: min(760px, 84vw);
  --talk-half: min(380px, 42vw);
  --talk-side: min(420px, 32vw);
  /* With nothing in the workspace the conversation IS the screen — roughly
     60% of it, centred, composer under it. It only moves aside when there is
     something to move aside for. (`--talk-mid-half` is exactly half of
     `--talk-mid`; both are written out so the centring margin stays a plain
     calc the browser can animate.) */
  --talk-mid: min(920px, 62vw);
  --talk-mid-half: min(460px, 31vw);
  --talk-move: 520ms;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
  font-family: 'IBM Plex Sans', -apple-system, sans-serif;
  background: var(--bg);
  color: var(--text);
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}

/* ---------------- sign-in + baseline ---------------- */
#gate {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  background: radial-gradient(900px 600px at 50% 20%, rgba(93, 135, 255, 0.07), transparent 60%), var(--bg);
  transition: opacity 400ms ease;
}
#gate.out { opacity: 0; pointer-events: none; }
body:not(.gated):not(.booting) #gate { display: none; }
body.booting #shell { opacity: 0; }
#shell { transition: opacity 500ms ease; }
#gate-card {
  width: min(520px, 92vw); text-align: center;
  padding: 34px 34px 30px;
  border-radius: 20px;
  background: linear-gradient(180deg, #181c25, #10141b);
  border: 1px solid var(--line);
  box-shadow: var(--sh-lg);
  animation: rise 500ms var(--ease-out);
}
.gate-glyph { width: 62px; height: 62px; object-fit: contain; margin-bottom: 12px; filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.55)); }
#gate-card h1 { font-size: 21px; font-weight: 600; letter-spacing: -0.3px; line-height: 1.3; }
#gate-card p { margin-top: 9px; font-size: 12.5px; line-height: 1.6; color: var(--text-dim); }
/* the question leads; everything under it is quieter */
#gate-card h1 { font-size: 21px; font-weight: 600; color: var(--text); line-height: 1.3; letter-spacing: -0.3px; }
.gate-sub { margin-top: 8px !important; font-size: 12.5px !important; font-weight: 400; color: var(--text-dim) !important; }
.gate-intro { font-size: 11.5px !important; color: var(--text-faint) !important; margin-top: 8px !important; }
#gate-form { display: flex; flex-direction: column; gap: 9px; margin-top: 20px; }
#gate-form input {
  height: 44px; border-radius: 11px;
  border: 1px solid var(--line-2); background: rgba(0, 0, 0, 0.3);
  color: var(--text); font: 400 13.5px 'IBM Plex Sans', sans-serif;
  padding: 0 14px; outline: none; text-align: center;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
#gate-form input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(93, 135, 255, 0.13); }
#gate-form button {
  height: 44px; border: 0; border-radius: 11px;
  background: var(--accent); color: #fff; cursor: pointer;
  font: 600 13px 'IBM Plex Sans', sans-serif;
  transition: filter 150ms ease, transform 150ms ease;
}
#gate-form button:hover:not(:disabled) { filter: brightness(1.1); transform: translateY(-1px); }
#gate-form button:disabled { opacity: 0.6; cursor: default; }
.gate-err { font-size: 12px; color: var(--bad); margin-top: 2px; animation: rise 240ms var(--ease-out); }
.gate-progress { display: flex; gap: 6px; justify-content: center; margin-bottom: 16px; }
.gate-progress i { width: 22px; height: 3px; border-radius: 2px; background: var(--line-2); transition: background 300ms ease, width 300ms var(--ease-out); }
.gate-progress i.done { background: var(--good); }
.gate-progress i.on { background: var(--accent); width: 34px; }
.gate-options { display: flex; flex-direction: column; gap: 8px; margin-top: 20px; }
.gate-opt {
  text-align: left; padding: 13px 15px;
  border: 1px solid var(--line-2); border-radius: 13px;
  background: var(--fill-1); color: var(--text); cursor: pointer;
  transition: all 160ms ease;
  animation: rise 380ms var(--ease-out) backwards;
}
.gate-options .gate-opt:nth-child(2) { animation-delay: 50ms; }
.gate-options .gate-opt:nth-child(3) { animation-delay: 100ms; }
.gate-options .gate-opt:nth-child(4) { animation-delay: 150ms; }
.gate-opt b { display: block; font-size: 13.5px; font-weight: 600; }
.gate-opt span { display: block; font-size: 11.5px; color: var(--text-faint); margin-top: 2px; line-height: 1.45; }
.gate-opt:hover { border-color: var(--accent); background: rgba(93, 135, 255, 0.06); transform: translateY(-1px); }
.gate-opt.on { border-color: var(--accent); background: rgba(93, 135, 255, 0.12); box-shadow: 0 0 0 3px rgba(93, 135, 255, 0.08); }
.gate-opt.on b::after { content: ' ✓'; color: var(--accent-2); }
.gate-actions { display: flex; gap: 8px; justify-content: center; margin-top: 18px; }
.gate-actions button {
  border: 1px solid var(--line-2); border-radius: 9px; background: transparent;
  color: var(--text-dim); font: 500 11.5px 'IBM Plex Sans', sans-serif;
  padding: 8px 16px; cursor: pointer; transition: all 150ms ease;
}
.gate-actions button:hover { color: var(--text); border-color: var(--accent); }
.gate-spin {
  width: 26px; height: 26px; margin: 22px auto 4px; border-radius: 50%;
  border: 2px solid var(--line-2); border-top-color: var(--accent);
  animation: spin 900ms linear infinite;
}

#aurora { position: fixed; inset: 0; z-index: 0; pointer-events: none; }
#aurora::before {
  content: ''; position: absolute; inset: 0;
  background:
    radial-gradient(1100px 500px at 15% -12%, rgba(93, 135, 255, 0.07), transparent 60%),
    radial-gradient(900px 600px at 108% 112%, rgba(60, 226, 165, 0.045), transparent 60%);
}
body::after {
  content: ''; position: fixed; inset: 0; z-index: 1; pointer-events: none; opacity: 0.05;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}
#shell { position: relative; z-index: 2; display: flex; flex-direction: column; height: 100vh; }

/* ---------------- top bar ---------------- */
#topbar {
  height: 56px; flex: 0 0 56px;
  display: flex; align-items: center; gap: 14px;
  padding: 0 18px;
  background: rgba(11, 13, 18, 0.72);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
  z-index: 30;
}
#brand { display: flex; align-items: center; flex: 0 0 auto; }
#brand-mark {
  width: 34px; height: 34px; padding: 0;
  border: 0; background: transparent; cursor: pointer;
  border-radius: 9px;
  transition: transform 200ms var(--ease-spring), filter 200ms ease;
}
#brand-mark img { width: 100%; height: 100%; object-fit: contain; display: block; filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.5)); }
#brand-mark:hover { transform: rotate(-8deg) scale(1.08); }
#brand-mark:active { transform: scale(0.94); }
#topspacer { flex: 1; }

/* session switcher */
#sessmenu { position: relative; }
#sess-btn {
  display: flex; align-items: center; gap: 8px;
  height: 34px; padding: 0 12px;
  border-radius: 10px; cursor: pointer;
  background: var(--panel); border: 1px solid var(--line); color: var(--text-dim);
  font: 600 11.5px 'IBM Plex Sans', sans-serif;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
  transition: border-color 160ms ease;
}
#sess-btn:hover { border-color: var(--line-2); color: var(--text); }
#sess-btn b { max-width: 220px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#sess-btn svg { width: 12px; height: 12px; color: var(--text-faint); transition: transform 200ms ease; }
#sessmenu.open #sess-btn svg { transform: rotate(180deg); }
#sess-pop {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 60;
  min-width: 240px; max-height: 320px; overflow-y: auto;
  border-radius: 13px; padding: 5px;
  background: var(--panel); border: 1px solid var(--line); box-shadow: var(--sh-lg);
  opacity: 0; transform: translateY(-6px) scale(0.98); pointer-events: none;
  transition: opacity 180ms ease, transform 240ms var(--ease-spring);
  scrollbar-width: none;
}
#sessmenu.open #sess-pop { opacity: 1; transform: none; pointer-events: auto; }
.sess-row {
  display: flex; align-items: center; gap: 8px; width: 100%;
  border: 0; background: transparent; cursor: pointer;
  border-radius: 9px; padding: 8px 10px; text-align: left;
  color: var(--text-dim); font: 400 11.5px 'IBM Plex Sans', sans-serif;
  transition: background 130ms ease, color 130ms ease;
}
.sess-row:hover { background: rgba(93, 135, 255, 0.07); color: var(--text); }
.sess-row b { font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sess-row span { margin-left: auto; font: 500 9.5px 'IBM Plex Mono', monospace; color: var(--text-faint); }
.sess-row.on { background: var(--fill-1); }
.sess-row.new b { color: var(--accent-2); }

/* apps popover */
#appsmenu { position: relative; }
#apps-btn {
  width: 32px; height: 32px; border-radius: 10px;
  border: 1px solid var(--line); background: var(--panel);
  color: var(--text-faint); cursor: pointer;
  display: grid; place-items: center;
  transition: all 150ms ease;
}
#apps-btn svg { width: 15px; height: 15px; }
#apps-btn:hover, #appsmenu.open #apps-btn { color: var(--text); border-color: var(--line-2); }
#apps-pop {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 80;
  width: 226px; padding: 5px;
  border-radius: 13px;
  background: var(--panel); border: 1px solid var(--line); box-shadow: var(--sh-lg);
  opacity: 0; transform: translateY(-6px); pointer-events: none;
  transition: opacity 180ms ease, transform 220ms var(--ease-spring);
}
#appsmenu.open #apps-pop { opacity: 1; transform: none; pointer-events: auto; }

/* how you like to work — the baseline stays changeable, and the system says
   out loud what it has since concluded from watching */
#usermenu { position: relative; }
#user-btn {
  width: 32px; height: 32px; border-radius: 50%;
  border: 1px solid var(--line-2);
  background: linear-gradient(150deg, var(--accent), #7b5cff);
  color: #fff; cursor: pointer;
  font: 600 12px 'IBM Plex Sans', sans-serif;
  display: grid; place-items: center;
  transition: all 150ms ease;
}
#user-btn:hover, #usermenu.open #user-btn { filter: brightness(1.12); transform: translateY(-1px); }
/* A settings sheet, not a popover. "Customise everything" only works if there
   is room for everything — this slides in from the right, is full height, and
   scrolls with its header and footer pinned. */
#user-pop {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 90;
  width: min(360px, 92vw); padding: 18px 18px 14px;
  display: flex; flex-direction: column;
  background: linear-gradient(180deg, #161a22, #10141b);
  border-left: 1px solid var(--line);
  box-shadow: -24px 0 60px rgba(0, 0, 0, 0.5);
  border-radius: 0;
  opacity: 0; transform: translateX(16px); pointer-events: none;
  transition: opacity 220ms ease, transform 340ms var(--ease-out);
}
body.settings-open #user-pop { opacity: 1; transform: none; pointer-events: auto; }
/* a scrim, so the sheet reads as a layer over the workspace rather than a
   panel competing with it */
#user-scrim {
  position: fixed; inset: 0; z-index: 89;
  background: rgba(0, 0, 0, 0.35);
  opacity: 0; pointer-events: none; transition: opacity 220ms ease;
}
body.settings-open #user-scrim { opacity: 1; pointer-events: auto; }
.up-head {
  flex: 0 0 auto; display: flex; align-items: baseline; gap: 8px;
  padding-bottom: 12px; margin-bottom: 2px; border-bottom: 1px solid var(--line);
}
.up-head b { font-size: 15px; }
.up-head span { font: 500 8.5px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1px; color: var(--text-faint); }
/* the fade says "there is more below" without needing a scrollbar */
.up-body { position: relative; flex: 1; min-height: 0; display: flex; }
.up-body::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 24px;
  background: linear-gradient(to bottom, rgba(16, 20, 27, 0), #10141b);
  pointer-events: none;
}
.up-scroll {
  flex: 1; min-height: 0; overflow-y: auto;
  margin: 0 -6px; padding: 0 6px 18px;
  scrollbar-width: thin; scrollbar-color: var(--line-2) transparent;
}
.up-group {
  font: 600 8.5px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1.2px;
  color: var(--accent-2); margin: 16px 0 4px; padding-top: 12px; border-top: 1px solid var(--line);
}
.up-scroll > .up-group:first-child { border-top: 0; padding-top: 0; margin-top: 12px; }
.up-row { padding: 9px 0 6px; }
.up-row em {
  display: block; font-style: normal;
  font: 500 10px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 0.9px;
  color: var(--text-faint); margin-bottom: 6px;
}
.up-opts { display: flex; gap: 4px; }
.up-opts button {
  flex: 1; padding: 7px 4px;
  border: 1px solid var(--line-2); border-radius: 7px;
  background: transparent; color: var(--text-dim); cursor: pointer;
  font: 500 11px 'IBM Plex Sans', sans-serif;
  transition: all 140ms ease;
}
.up-opts button:hover { color: var(--text); border-color: var(--accent); }
.up-opts button.on { background: rgba(93, 135, 255, 0.16); border-color: var(--accent); color: var(--text); }
.up-note { display: block; font-size: 10px; color: var(--text-faint); margin-top: 6px; line-height: 1.5; }
/* which windows the agent may build in at all */
.up-apps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-top: 8px; }
.up-app {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 10px 4px; border-radius: 10px;
  border: 1px solid var(--line-2); background: transparent;
  color: var(--text-faint); cursor: pointer;
  font: 500 10px 'IBM Plex Sans', sans-serif;
  transition: all 150ms ease;
}
.up-app svg { width: 15px; height: 15px; }
.up-app:hover { color: var(--text); border-color: var(--accent); }
.up-app.on { color: var(--text); border-color: var(--accent); background: rgba(93, 135, 255, 0.14); }
.up-app:not(.on) { opacity: 0.4; }
.up-learned {
  margin-top: 16px; padding: 10px 11px;
  border-radius: 9px; background: rgba(46, 204, 139, 0.07); border: 1px solid rgba(46, 204, 139, 0.18);
}
.up-learned em {
  display: block; font-style: normal;
  font: 500 9px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1px;
  color: var(--good); margin-bottom: 4px;
}
.up-learned span { font-size: 11px; color: var(--text-dim); line-height: 1.5; }
.up-foot { flex: 0 0 auto; display: flex; gap: 7px; padding-top: 12px; border-top: 1px solid var(--line); }
.up-foot button {
  flex: 1; padding: 9px; border-radius: 8px;
  border: 1px solid var(--line); background: transparent; color: var(--text-faint);
  font: 500 11px 'IBM Plex Sans', sans-serif; cursor: pointer;
  transition: all 140ms ease;
}
.up-retake:hover { color: var(--accent-2); border-color: var(--accent); background: rgba(93, 135, 255, 0.06); }
.up-out:hover { color: var(--bad); border-color: var(--bad); background: rgba(220, 47, 106, 0.07); }
.ap-row {
  display: flex; align-items: center; gap: 9px; width: 100%;
  border: 0; background: transparent; cursor: pointer;
  border-radius: 8px; padding: 7px 9px; text-align: left;
  color: var(--text-dim); font: 400 11.5px 'IBM Plex Sans', sans-serif;
  transition: background 130ms ease, color 130ms ease;
}
.ap-row svg { width: 13px; height: 13px; color: hsl(var(--h) 75% 68%); opacity: 0.9; }
.ap-row:hover { background: var(--fill-1); color: var(--text); }
.ap-row i { margin-left: auto; width: 5px; height: 5px; border-radius: 50%; background: transparent; }
.ap-row.on { color: var(--text); }
.ap-row.on i { background: var(--good); }

/* ---------------- stage / canvas ---------------- */
#stage { flex: 1; position: relative; min-height: 0; }
#workspace {
  position: absolute; left: 14px; right: 14px; top: 14px;
  bottom: 100px;
  transition: bottom 460ms var(--ease-spring), left 460ms var(--ease-spring), right 460ms var(--ease-spring);
}
body.chat-center #workspace { opacity: 0; pointer-events: none; }
/* a side dock takes its own column; minimised gives the canvas nearly everything */
body.place-left  #workspace { left: calc(var(--dock-side-w) + 26px); right: 14px; bottom: 14px; }
body.place-right #workspace { right: calc(var(--dock-side-w) + 26px); left: 14px; bottom: 14px; }
body.place-mini  #workspace { bottom: 110px; }  /* clears the real pill, which wraps to two lines */
/* The dots are their own LAYER, not the canvas's own background, because the
   tiles are children of #canvas — a mask on the element would take them with
   it. See the canvas-chat rule that clears them from behind the conversation. */
#canvas { position: relative; width: 100%; height: 100%; }
#canvas::before {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image: radial-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1.3px);
  background-size: 26px 26px;
  background-position: 3px 3px;
}
#canvas::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background-image: radial-gradient(rgba(150, 180, 255, 0.6) 1.2px, transparent 1.7px);
  background-size: 26px 26px; background-position: 3px 3px;
  -webkit-mask-image: radial-gradient(150px 150px at var(--mx, -300px) var(--my, -300px), #000 0%, rgba(0, 0, 0, 0.3) 45%, transparent 72%);
  mask-image: radial-gradient(150px 150px at var(--mx, -300px) var(--my, -300px), #000 0%, rgba(0, 0, 0, 0.3) 45%, transparent 72%);
  opacity: var(--mo, 0);
  transition: opacity 400ms ease;
}

#drop-ghost {
  position: absolute; z-index: 1;
  border: 1.5px dashed rgba(93, 135, 255, 0.5);
  border-radius: var(--r);
  background: rgba(93, 135, 255, 0.05);
  opacity: 0; pointer-events: none;
  transition: left var(--tile-t) var(--ease-spring), top var(--tile-t) var(--ease-spring), width var(--tile-t) var(--ease-spring), height var(--tile-t) var(--ease-spring), opacity 200ms ease;
}

/* ---------------- tiles ---------------- */
.tile {
  position: absolute; z-index: 2;
  display: flex; flex-direction: column;
  border-radius: var(--r);
  background: linear-gradient(180deg, #181c25, #10141b);
  border: 1px solid var(--line);
  box-shadow: var(--sh-md);
  overflow: hidden;
  transition:
    transform var(--tile-t) var(--ease-spring),
    width var(--tile-t) var(--ease-spring),
    height var(--tile-t) var(--ease-spring),
    left var(--tile-t) var(--ease-spring),
    top var(--tile-t) var(--ease-spring),
    opacity 280ms ease,
    border-color 240ms ease,
    box-shadow 240ms ease;
  will-change: left, top, width, height;
}
.tile::before {
  content: ''; position: absolute; inset: -1px; border-radius: inherit; padding: 1.5px;
  background: radial-gradient(240px circle at var(--px, -300px) var(--py, -300px), hsl(var(--h) 85% 68% / 0.8), transparent 65%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0; transition: opacity 300ms ease;
  pointer-events: none;
}
.tile::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  background: radial-gradient(340px circle at var(--px, -300px) var(--py, -300px), hsl(var(--h) 85% 65% / 0.08), transparent 70%);
  opacity: 0; transition: opacity 300ms ease;
  pointer-events: none;
}
.tile:hover::before, .tile:hover::after { opacity: 1; }
/* Which window has focus, said quietly. A saturated ring around the thing you
   are reading is the loudest outline on the screen, and it is on screen the
   whole time — the app's own colour survives in the header icon and the glow,
   which is enough to tell two windows apart. */
.tile.focus { border-color: rgba(255, 255, 255, 0.2); box-shadow: var(--sh-md), 0 0 26px hsl(var(--h) 80% 60% / 0.07); }
.tile.enter { transform: translateY(14px) scale(0.97); opacity: 0; }
.tile.suppressed { opacity: 0.12; filter: saturate(0.3); pointer-events: none; }
.tile.exit {
  transform: scale(0.94) translateY(-4px); opacity: 0; pointer-events: none;
  transition: transform var(--t-out) var(--ease-in), opacity var(--t-out) var(--ease-in);
}
/* a tile the agent just touched gets a brief edge-light, so your eye follows */
.tile.pulse::after { animation: tilepulse 900ms var(--ease-out); }
@keyframes tilepulse {
  0% { opacity: 0; }
  35% { opacity: 1; }
  100% { opacity: 0; }
}

.tile-head { flex: 0 0 36px; display: flex; align-items: center; gap: 8px; padding: 0 8px 0 13px; user-select: none; cursor: grab; }
.tile-ico { width: 14px; height: 14px; color: hsl(var(--h) 75% 68%); }
.tile-ico svg { width: 14px; height: 14px; display: block; }
.tile-name { font: 500 11.5px 'IBM Plex Sans', sans-serif; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tile-size { display: none; }
.tile-size--unused { font: 500 8.5px 'IBM Plex Mono', monospace; color: var(--text-faint); border: 1px solid var(--line); border-radius: 5px; padding: 1.5px 5px; text-transform: uppercase; }
.tile-actions { margin-left: auto; display: flex; gap: 2px; opacity: 0; transform: translateX(4px); transition: all 200ms ease; }
.tile:hover .tile-actions { opacity: 1; transform: none; }
.tbtn {
  width: 25px; height: 25px; border: 0; border-radius: 8px;
  background: transparent; color: var(--text-dim);
  font-size: 13px; line-height: 1; cursor: pointer;
  display: grid; place-items: center;
  transition: background 140ms ease, color 140ms ease;
}
.tbtn:hover { background: var(--fill-2); color: var(--text); }
.tbtn svg { width: 14px; height: 14px; }
.tbtn.close:hover { background: rgba(220, 47, 106, 0.1); color: var(--bad); }
.tile-body { flex: 1; min-height: 0; overflow: hidden; position: relative; }
/* Content used to stop dead at the tile edge, mid-word, with nothing to say
   there was more below. A fade reads as "keep scrolling" instead of "broken". */
.tile-body::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 26px;
  background: linear-gradient(to bottom, rgba(16, 20, 27, 0), #10141b);
  opacity: 0.9; pointer-events: none; z-index: 1;
}
.tile-body:has(.app-empty)::after { content: none; }

/* Moving and resizing live on the EDGES and the title bar; everywhere else in
   a window is the app's, and a click there is a click into the app. */
.tile.rz, .tile.rz .tile-head { cursor: nwse-resize; }
.tile.rz-x, .tile.rz-x .tile-head { cursor: ew-resize; }
.tile.rz-y, .tile.rz-y .tile-head { cursor: ns-resize; }
.tile-body { cursor: auto; }
body.tiledrag { cursor: grabbing; user-select: none; }
body.tiledrag .tile:not(.drag) { pointer-events: none; }
.tile.drag {
  transition: width var(--tile-t) var(--ease-spring), height var(--tile-t) var(--ease-spring), box-shadow 200ms ease;
  z-index: 40; box-shadow: var(--sh-lg); opacity: 0.98;
}

/* ephemeral in-tile status: what's being made, right now (commandment 7) */
.tile-status {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 7px;
  margin: 0 10px 6px; padding: 5px 9px;
  border-radius: 8px;
  background: hsl(var(--h) 70% 60% / 0.09);
  border: 1px solid hsl(var(--h) 60% 60% / 0.28);
  font: 500 10.5px 'IBM Plex Sans', sans-serif;
  color: hsl(var(--h) 85% 80%);
  animation: msg-in 260ms var(--ease-spring);
}
.ts-dot {
  width: 5px; height: 5px; border-radius: 50%; flex: 0 0 auto;
  background: hsl(var(--h) 85% 70%);
  animation: pulse 1.2s ease-in-out infinite;
}

/* ============ shared motion ============
   One vocabulary for everything that appears or disappears, so the workspace
   feels like a single object rather than a collection of widgets. */
@keyframes rise { from { opacity: 0; transform: translateY(9px) scale(0.985); } }
@keyframes fall { to { opacity: 0; transform: translateY(-6px) scale(0.985); } }
@keyframes slideIn { from { opacity: 0; transform: translateX(-10px); } }
@keyframes slideOut { to { opacity: 0; transform: translateX(14px); } }
@keyframes popIn { from { opacity: 0; transform: scale(0.9); } }
@keyframes sheen { to { background-position: 200% 0; } }

.enter-rise { animation: rise var(--t-in) var(--ease-out) backwards; }
.leave-fall { animation: fall var(--t-out) var(--ease-in) forwards; pointer-events: none; }
.leave-slide { animation: slideOut var(--t-out) var(--ease-in) forwards; pointer-events: none; }

/* rows that arrive in a list stagger just enough to read as a sequence */
.stagger > * { animation: rise var(--t-in) var(--ease-out) backwards; }
.stagger > *:nth-child(1) { animation-delay: 0ms; }
.stagger > *:nth-child(2) { animation-delay: 40ms; }
.stagger > *:nth-child(3) { animation-delay: 80ms; }
.stagger > *:nth-child(4) { animation-delay: 120ms; }
.stagger > *:nth-child(5) { animation-delay: 160ms; }
.stagger > *:nth-child(n+6) { animation-delay: 200ms; }

/* content arriving live: each item lands as it is written */
.born { animation: rise var(--t-in) var(--ease-out) backwards; }
@keyframes born { from { opacity: 0; transform: translateY(7px); } }

/* Anyone who asks for less motion gets the state changes without the travel. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}
.draft-root { height: 100%; overflow-y: auto; padding: 12px 14px; scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
.draft-items { display: flex; flex-direction: column; gap: 4px; }
.draft-head { margin-bottom: 8px; }
/* the caret on the text being typed right now */
.caret {
  display: inline-block; width: 2px; height: 0.95em;
  vertical-align: text-bottom; margin-left: 2px;
  background: var(--accent-2); border-radius: 1px;
  animation: blink 1s steps(2, start) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
.writing {
  display: flex; align-items: center; gap: 5px;
  padding: 9px 4px 4px;
  font: 500 10.5px 'IBM Plex Sans', sans-serif; color: var(--text-faint);
}
.writing i {
  width: 4px; height: 4px; border-radius: 50%; background: var(--accent);
  animation: orb 1.1s ease-in-out infinite;
}
.writing i:nth-child(2) { animation-delay: 0.15s; }
.writing i:nth-child(3) { animation-delay: 0.3s; }
.writing span { margin-left: 4px; }
@keyframes orb { 40% { transform: translateY(-4px); opacity: 0.45; } }
.drafting { height: 100%; overflow-y: auto; padding: 12px 14px; scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
.drafting .qq-freebox {
  font: 500 9.5px 'IBM Plex Mono', monospace; color: var(--text-faint);
  border: 1px dashed var(--line-2); border-radius: 8px; padding: 8px 10px;
}
.fc-draftlist { display: flex; flex-direction: column; gap: 6px; }
.fc-draft { border: 1px solid var(--line); border-radius: 9px; padding: 7px 10px; }
.fc-draft b { display: block; font-size: 11.5px; font-weight: 500; }
.fc-draft span { display: block; font-size: 10.5px; color: var(--text-faint); margin-top: 2px; }

/* ---- narration: the agent talking inside the app window (commandment 3) ----
   A ROW OF THE WINDOW, not a bubble floating on top of one. It takes its own
   space in the tile's column along the bottom edge, so it never covers the
   thing it is pointing at, and it stays until the agent says something else
   there or the learner dismisses it. (It used to be an overlay that reserved
   padding and then folded itself into a corner avatar on a timer — three
   mechanisms to deliver one sentence, and it hid the content while it did.) */
.tile-narr {
  flex: 0 0 auto; position: relative; z-index: 3;
  display: flex; align-items: flex-start; gap: 9px;
  margin: 0 10px 10px; padding: 9px 11px 9px 10px;
  border-radius: 11px;
  background: hsl(var(--h) 60% 55% / 0.1);
  border: 1px solid hsl(var(--h) 60% 60% / 0.3);
  border-left: 2.5px solid hsl(var(--h) 75% 62% / 0.85);
  font-size: 12.5px; line-height: 1.55; color: var(--text);
  max-height: 36%; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--line-2) transparent;
  animation: narr-in 300ms var(--ease-out);
}
.tile-narr.instant { animation: none; }
.tile-narr img { width: 18px; height: 18px; flex: 0 0 auto; margin-top: 1px; filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.5)); }
.tile-narr span { flex: 1; min-width: 0; }
.narr-x {
  flex: 0 0 auto; width: 18px; height: 18px; margin: -1px -3px 0 0;
  border: 0; background: transparent; color: var(--text-faint);
  border-radius: 5px; cursor: pointer; font-size: 13px; line-height: 1;
  display: grid; place-items: center; opacity: 0;
  transition: opacity 150ms ease, background 140ms ease, color 140ms ease;
}
.tile-narr:hover .narr-x { opacity: 1; }
.narr-x:hover { background: var(--fill-2); color: var(--text); }
.tile-narr.out { opacity: 0; transform: translateY(4px); transition: all var(--t-out) var(--ease-in); }
@keyframes narr-in { from { opacity: 0; transform: translateY(6px); } }

/* resize guides + waves + shake */
.rz-guide {
  position: absolute; z-index: 1; pointer-events: none; opacity: 0;
  border: 1.5px dashed rgba(255, 255, 255, 0.22); border-radius: 12px;
  transition: opacity 200ms ease;
}
.rz-guide span { position: absolute; top: 5px; left: 8px; font: 600 8.5px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1px; color: var(--text-faint); }
#rz-dims {
  position: absolute; z-index: 45; pointer-events: none; opacity: 0;
  font: 600 11px 'IBM Plex Mono', monospace; color: var(--accent-2);
  background: rgba(10, 13, 20, 0.9); border: 1px solid rgba(93, 135, 255, 0.4);
  padding: 3px 8px; border-radius: 7px;
  transition: opacity 150ms ease;
}
.wave {
  position: absolute; z-index: 1; pointer-events: none;
  border-radius: 50%; transform: translate(-50%, -50%) scale(0.4);
  border: 1.5px solid var(--wc, rgba(140, 170, 255, 0.5));
  animation: wave-out var(--wd, 560ms) ease-out forwards;
}
@keyframes wave-out { to { transform: translate(-50%, -50%) scale(1); opacity: 0; } }
@keyframes shakeX { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-5px); } 40% { transform: translateX(5px); } 60% { transform: translateX(-3px); } 80% { transform: translateX(3px); } }
.shake { animation: shakeX 320ms ease !important; }

/* ---------------- the fused chat surface ---------------- */
#chatdock {
  position: absolute; z-index: 20;
  left: calc(50% - var(--dock-w) / 2);
  width: var(--dock-w);
  bottom: 16px;
  display: flex; flex-direction: column;
  transition: bottom 500ms var(--ease-spring), top 500ms var(--ease-spring),
    left 500ms var(--ease-spring), right 500ms var(--ease-spring), width 500ms var(--ease-spring);
}
/* The empty state belongs to the whole screen, not to a docked column. There
   is no workspace to sit beside yet, so the invitation is centred whatever
   placement they normally use; it takes its dock as soon as there is something
   to sit beside. */
body.chat-center #chatdock {
  top: 14% !important; bottom: auto !important;
  left: calc(50% - var(--dock-w) / 2) !important;
  right: auto !important;
  width: var(--dock-w) !important;
  justify-content: flex-start !important;
}

/* lifted: the panel tracks the cursor, so a drag feels like carrying it */
body.chat-lifted #chatdock {
  transform: translate(var(--drag-x, 0), var(--drag-y, 0)) scale(0.97);
  transition: none;
  z-index: 40;
  opacity: 0.96;
  pointer-events: none;
  filter: drop-shadow(0 22px 46px rgba(0, 0, 0, 0.6));
}

/* ---- placements ---- */
body.place-left #chatdock,
body.place-right #chatdock {
  top: 14px; bottom: 14px; width: var(--dock-side-w);
  justify-content: flex-end;
}
body.place-left #chatdock { left: 14px; right: auto; }
body.place-right #chatdock { right: 14px; left: auto; }
body.place-left #chat-log-wrap,
body.place-right #chat-log-wrap { flex: 1 1 auto; }

/* minimised: a sliver you can still type into */
/* Minimised sits below the canvas rather than over it, so it can't cover a
   tile's narration. */
body.place-mini #chatdock {
  left: auto; right: 16px; bottom: 8px;
  width: 300px;
  cursor: pointer;
}
body.place-mini #chat-log-wrap, body.place-mini #hero { display: none; }
body.place-mini #chat-input { height: 38px; min-height: 38px; font-size: 12.5px; padding: 8px 12px; }
body.place-mini #chat-send { width: 38px; height: 38px; }


#hero { display: none; text-align: center; padding: 0 20px 26px; }
body.chat-center #hero { display: block; animation: hero-in 700ms var(--ease-spring); }
@keyframes hero-in { from { opacity: 0; transform: translateY(12px); } }
.hero-glyph { width: 92px; height: 92px; object-fit: contain; margin: 0 auto 10px; display: block; filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.55)) drop-shadow(0 0 26px rgba(76, 224, 176, 0.16)); }
/* A short greeting, not a pitch. The old headline explained what the thing
   does to someone already using it. */
#hero h1 { font-size: 18px; font-weight: 500; letter-spacing: -0.2px; color: var(--text-dim); }
/* The four worked-example buttons that used to sit here are gone: a greeting
   and a box is the whole entry screen now. */

/* The fused card: history + composer are ONE surface (commandment 5).
   It reads as a single panel so it never looks like loose text over tiles. */
#chatdock.fused {
  background: rgba(13, 16, 22, 0.93);
  backdrop-filter: blur(18px) saturate(1.2);
  border: 1px solid var(--line);
  border-radius: 18px;
  box-shadow: var(--sh-lg);
  padding: 6px;
  transition: bottom 500ms var(--ease-spring), background 400ms ease, border-color 400ms ease, box-shadow 400ms ease, padding 400ms ease;
}
/* Docked to a side the agent is a LAYER, not a panel: no card, no border, no
   shadow — the conversation simply occupies its column and only the composer
   is drawn. Apps are the things in boxes; the agent is not an app. */
body.place-left #chatdock.fused,
body.place-right #chatdock.fused {
  background: transparent; backdrop-filter: none; border-color: transparent;
  box-shadow: none; border-radius: 0; padding: 0;
}

/* while the agent acts, the card collapses back to a bare composer */
body.acting #chatdock.fused {
  background: transparent; border-color: transparent; box-shadow: none; backdrop-filter: none; padding: 0;
}

/* History collapse uses a 1fr → 0fr grid row: it animates smoothly whether the
   log is height-capped (centre) or flex-filling (side docks), where animating
   max-height would snap. */
#chat-log-wrap {
  display: grid; grid-template-rows: 1fr; min-height: 0;
  transition: grid-template-rows 380ms var(--ease-spring), opacity 240ms ease;
}
body.acting #chat-log-wrap,
body.chat-collapsed #chat-log-wrap { grid-template-rows: 0fr; opacity: 0; pointer-events: none; }
/* the row collapses to 0fr but the log's own padding survives it, leaving the
   card ~26px taller than "closed" */
body.acting #chat-log,
body.chat-collapsed #chat-log { padding-top: 0; padding-bottom: 0; }
body.chat-center #chat-log-wrap,
body:not(.has-msgs) #chat-log-wrap { display: none; }

#chat-log {
  min-height: 0;
  max-height: 42vh;
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 10px;
  /* The bottom is generous on purpose: a reply that ends flush against the
     composer reads as though it has been cut off. `--tail` is the extra room
     the reply falls into while their message is held at the top — padding
     rather than a spacer element, so the last message is still the last
     child. */
  padding: 12px 12px calc(30px + var(--tail, 0px));
  scrollbar-width: thin; scrollbar-color: var(--line-2) transparent;
}
body.place-left #chat-log, body.place-right #chat-log { max-height: none; }

.msg { max-width: 92%; display: flex; flex-direction: column; align-self: flex-start; align-items: flex-start; animation: msg-in var(--t-in) var(--ease-out); }
.msg.agent { max-width: 100%; }
.msg.user { align-self: flex-end; align-items: flex-end; animation-name: msgInUser; }
@keyframes msg-in { from { opacity: 0; transform: translateY(10px) scale(0.97); } }
@keyframes msgInUser { from { opacity: 0; transform: translateY(10px) translateX(8px) scale(0.97); } }
/* The agent is not an app. Its words are text on the surface, not a bubble in
   a box — a bubble reads as another thing on the page, competing with the
   things on the page. What the LEARNER says keeps a bubble: their turn is an
   event, the agent's is running commentary. */
.bubble { padding: 0; border-radius: 0; font-size: 13.5px; line-height: 1.62; word-wrap: break-word; background: none; border: 0; }
.msg.user .bubble {
  padding: 8px 13px; border-radius: 14px; border-bottom-right-radius: 5px;
  background: rgba(93, 135, 255, 0.16); border: 1px solid rgba(93, 135, 255, 0.3);
  color: var(--text); font-size: 13px;
}
.msg.agent .bubble { color: var(--text-dim); }
.msg.agent .bubble strong { color: var(--text); }
.msg.error .bubble { padding: 8px 12px; border-radius: 10px; background: rgba(220, 47, 106, 0.07); color: var(--bad); border: 1px solid rgba(220, 47, 106, 0.3); }
.msg.chipline .bubble { display: none; }
.msg.chipline { flex-direction: row; }

.msg.agent .bubble p { margin: 0; }
.msg.agent .bubble p + p, .msg.agent .bubble p + ul, .msg.agent .bubble p + ol,
.msg.agent .bubble ul + p, .msg.agent .bubble ol + p, .msg.agent .bubble h4 + p { margin-top: 7px; }
.msg.agent .bubble ul, .msg.agent .bubble ol { margin: 7px 0 0; padding-left: 17px; display: flex; flex-direction: column; gap: 4px; }
.msg.agent .bubble ul li::marker { color: var(--accent); }
.msg.agent .bubble ol li::marker { color: var(--accent); font: 500 11px 'IBM Plex Mono', monospace; }
.msg.agent .bubble strong { color: var(--text); font-weight: 600; }
.msg.agent .bubble h4 { font: 600 10px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1.2px; color: var(--accent-2); margin: 10px 0 4px; }
.msg.agent .bubble h4:first-child { margin-top: 0; }
.msg.agent .bubble code { font: 500 11px 'IBM Plex Mono', monospace; background: rgba(255, 255, 255, 0.08); border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; }

.achip {
  display: inline-flex; align-items: center; gap: 5px;
  font: 500 10px 'IBM Plex Mono', monospace;
  color: hsl(var(--h, 220) 85% 78%);
  background: hsl(var(--h, 220) 75% 62% / 0.12);
  border: 1px solid hsl(var(--h, 220) 65% 60% / 0.32);
  border-radius: 7px; padding: 3.5px 8px;
  max-width: 100%;
  animation: msg-in 320ms var(--ease-spring) backwards;
}
.achip svg { width: 11px; height: 11px; flex: 0 0 auto; margin-top: 1px; }
.achip i { font-style: normal; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Narration is the agent's primary voice (commandment 3) — its record in the
   transcript has to be readable, not clipped to whatever fits one line of a
   narrow column. */
.msg.narr .achip { align-items: flex-start; }
.msg.narr .achip i { white-space: normal; overflow: visible; line-height: 1.5; }
.achip.evt { color: #a8bcff; background: rgba(93, 135, 255, 0.1); border-color: rgba(93, 135, 255, 0.3); }

/* composer — one container, holding whatever has been marked plus the box */
#chat-form { display: flex; flex-direction: column; }
/* Relative, because the stop control sits INSIDE the box rather than beside it
   — see #chat-send. */
#chat-row { display: flex; gap: 8px; align-items: flex-end; position: relative; }
#chat-input {
  flex: 1; height: 48px; min-height: 48px; max-height: 160px;
  resize: none; overflow-y: hidden;
  scrollbar-width: thin; scrollbar-color: var(--line-2) transparent;
  border-radius: 13px;
  border: 1px solid var(--line-2);
  background: rgba(8, 10, 15, 0.9);
  backdrop-filter: blur(10px);
  color: var(--text);
  font: 400 13.5px/1.6 'IBM Plex Sans', sans-serif;
  padding: 12.5px 16px; outline: none;
  box-shadow: var(--sh-md);
  transition: border-color 180ms ease, box-shadow 180ms ease;
}
#chat-input::placeholder { color: var(--text-faint); }
#chat-input:focus { border-color: rgba(255, 255, 255, 0.22); box-shadow: var(--sh-md); }
/* ---- there is no send button ----
   Matt: "Do we need a send button on the text input box? My feeling is no."
   Enter sends. But commandment 4 puts INTERRUPTION in the composer — the
   working indicator IS the stop control — so the button still exists and
   appears when there is something to stop, and not one moment before.

   Absolutely positioned inside the row rather than laid out beside the input,
   so the box does not change width when a turn starts: a composer that resizes
   the instant you press Enter is the jump this surface keeps being asked to
   stop making. */
#chat-send {
  position: absolute; right: 7px; bottom: 7px; z-index: 2;
  width: 34px; height: 34px;
  border: 0; border-radius: 10px;
  background: #232936; color: #fff; cursor: pointer;
  display: none; place-items: center;
  transition: transform 150ms ease, filter 150ms ease, background 250ms ease;
}
#chat-send svg { width: 15px; height: 15px; grid-area: 1 / 1; transition: opacity 200ms ease, transform 200ms ease; }
#chat-send .spin { grid-area: 1 / 1; width: 26px; height: 26px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, 0.22); border-top-color: #fff; opacity: 0; transition: opacity 200ms ease; }
#chat-send:hover { filter: brightness(1.25); }
#chat-send:active { transform: scale(0.94); }
#chat-send .ic-send { display: none; }        /* nothing left to send with */
#chat-send .ic-stop { opacity: 1; }
body.busy #chat-send { display: grid; }
body.busy #chat-send .spin { opacity: 1; animation: spin 900ms linear infinite; }
/* room for it, only while it is there */
body.busy #chat-input { padding-right: 50px; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------------- shared app bits ---------------- */
.app-empty {
  height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
  color: var(--text-faint); font-size: 12px; text-align: center; padding: 16px;
}
.app-empty svg { width: 26px; height: 26px; opacity: 0.5; }

.chipbtn {
  border: 1px solid var(--line-2); border-radius: 999px;
  background: var(--fill-1); color: var(--text-dim);
  font: 500 11px 'IBM Plex Sans', sans-serif;
  padding: 6px 13px; cursor: pointer;
  transition: all 150ms ease;
}
.chipbtn:hover { color: var(--text); border-color: var(--accent); background: rgba(93, 135, 255, 0.08); }
.chipbtn:disabled { opacity: 0.5; cursor: default; }
.chipbtn.dim { opacity: 0.7; }
.chipbtn.ok { border-color: rgba(60, 226, 165, 0.4); color: var(--good); }
.chipbtn.ok:hover { background: rgba(60, 226, 165, 0.08); }
.chipbtn.no { border-color: rgba(255, 95, 143, 0.35); color: var(--bad); }
.chipbtn.no:hover { background: rgba(255, 95, 143, 0.07); }
.chipbtn.deep { color: #c9b8ff; border-color: rgba(167, 139, 250, 0.4); }
.chipbtn.deep:hover { background: rgba(124, 58, 237, 0.08); }

/* ---------------- plan ---------------- */
.plan { height: 100%; display: flex; flex-direction: column; padding: 10px 12px 12px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
/* In a short tile the step card is the thing that must survive: the trail
   below it can scroll away, but "I'm ready" can never end up under the edge
   with no way to reach it. */
.plan-now { flex: 0 0 auto; }
.plan-head { flex: 0 0 auto; }
.plan-head { display: flex; align-items: center; gap: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
.plan-ring { width: 40px; height: 40px; flex: 0 0 auto; }
.plan-ring text { fill: var(--text); font: 600 9px 'IBM Plex Mono', monospace; text-anchor: middle; }
.plan-title b { display: block; font-size: 12.5px; }
.plan-title span { font: 500 9.5px 'IBM Plex Mono', monospace; color: var(--text-faint); }
/* the live goal leads — this is shared progress, not a to-do list */
.plan-live {
  margin: 9px 0 4px; padding: 10px 12px;
  border-radius: 11px;
  background: linear-gradient(180deg, rgba(93, 135, 255, 0.13), rgba(93, 135, 255, 0.05));
  border: 1px solid rgba(93, 135, 255, 0.42);
  box-shadow: 0 0 0 3px rgba(93, 135, 255, 0.06);
  animation: msg-in 340ms var(--ease-spring);
}
.plan-live em {
  display: flex; align-items: center; gap: 6px;
  font: 600 8.5px 'IBM Plex Mono', monospace; font-style: normal;
  text-transform: uppercase; letter-spacing: 1.3px; color: var(--accent-2);
  margin-bottom: 4px;
}
.plan-live em::before {
  content: ''; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent-2); animation: pulse 1.6s ease-in-out infinite;
}
.plan-live b { display: block; font-size: 12.5px; font-weight: 600; line-height: 1.4; }
.plan-live span { display: block; font-size: 10.5px; color: var(--text-dim); margin-top: 3px; line-height: 1.45; }

/* ---- the current step ----
   The one thing the learner is doing, what finished looks like, and the button
   that moves them on. Everything else in this window is context for it. */
.plan-now {
  margin: 10px 0 4px; padding: 11px 12px 10px;
  border-radius: 12px;
  background: linear-gradient(180deg, rgba(93, 135, 255, 0.14), rgba(93, 135, 255, 0.04));
  border: 1px solid rgba(93, 135, 255, 0.45);
  box-shadow: 0 0 0 3px rgba(93, 135, 255, 0.05), 0 6px 18px rgba(0, 0, 0, 0.3);
  animation: msg-in 380ms var(--ease-spring);
}
.plan-now.idle {
  background: var(--fill-1); border-color: var(--line-2); box-shadow: none;
}
.plan-now.advancing { animation: stepDone 460ms var(--ease-out) forwards; }
@keyframes stepDone {
  40% { transform: scale(1.015); border-color: var(--good); box-shadow: 0 0 0 4px rgba(46, 204, 139, 0.16); }
  100% { transform: translateY(-6px) scale(0.99); opacity: 0.35; }
}
.pn-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 5px; }
.pn-top em {
  display: flex; align-items: center; gap: 6px;
  font: 600 8.5px 'IBM Plex Mono', monospace; font-style: normal;
  text-transform: uppercase; letter-spacing: 1.3px; color: var(--accent-2);
}
.pn-top em::before {
  content: ''; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent-2); animation: pulse 1.6s ease-in-out infinite;
}
.pn-apps { display: flex; gap: 5px; opacity: 0.75; }
.pn-apps i { display: block; width: 12px; height: 12px; color: var(--text-dim); }
.pn-apps svg { width: 100%; height: 100%; }
.plan-now > b { display: block; font-size: 13px; font-weight: 600; line-height: 1.4; }
.pn-do { font-size: 11px; color: var(--text-dim); margin-top: 4px; line-height: 1.5; }
/* what "finished" means — the thing that stops the plan being guesswork */
.pn-done {
  display: flex; align-items: flex-start; gap: 6px;
  margin-top: 8px; padding: 6px 8px;
  border-radius: 8px; background: rgba(46, 204, 139, 0.08);
  border: 1px solid rgba(46, 204, 139, 0.2);
}
.pn-done i { flex: 0 0 auto; width: 11px; height: 11px; margin-top: 2px; color: var(--good); }
.pn-done svg { width: 100%; height: 100%; stroke-width: 2; }
.pn-done span { font-size: 10.5px; color: var(--text-dim); line-height: 1.45; }
.pn-actions { display: flex; gap: 6px; margin-top: 10px; }
.pn-go {
  flex: 1; height: 30px; border: 0; border-radius: 8px;
  background: var(--accent); color: #fff; cursor: pointer;
  font: 600 11px 'IBM Plex Sans', sans-serif;
  transition: filter 150ms ease, transform 150ms ease, opacity 150ms ease;
}
.pn-go:hover:not(:disabled) { filter: brightness(1.12); transform: translateY(-1px); }
.pn-go:active:not(:disabled) { transform: translateY(0) scale(0.99); }
.pn-stuck {
  height: 30px; padding: 0 11px; border-radius: 8px;
  border: 1px solid var(--line-2); background: transparent; color: var(--text-dim);
  font: 500 11px 'IBM Plex Sans', sans-serif; cursor: pointer;
  transition: all 150ms ease;
}
.pn-stuck:hover:not(:disabled) { color: var(--warn); border-color: var(--warn); background: rgba(232, 165, 71, 0.08); }
.pn-go:disabled, .pn-stuck:disabled { opacity: 0.45; cursor: default; }
.pt-start {
  flex: 0 0 auto; width: 19px; height: 19px; margin-top: 1px;
  border: 0; background: transparent; color: var(--text-faint);
  border-radius: 5px; cursor: pointer; opacity: 0;
  display: grid; place-items: center;
  transition: opacity 150ms ease, background 140ms ease, color 140ms ease;
}
.pt-start svg { width: 11px; height: 11px; fill: currentColor; stroke: none; }
.plan-task:hover .pt-start { opacity: 1; }
.pt-start:hover { background: rgba(93, 135, 255, 0.14); color: var(--accent-2); }
.plan-task.done .pt-start { display: none; }

.plan-body { flex: 1; overflow-y: auto; padding-top: 8px; display: flex; flex-direction: column; gap: 2px; scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
.plan-stage { font: 600 8.5px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1.2px; color: var(--text-faint); margin: 7px 0 3px; }
.plan-task {
  position: relative;
  display: flex; align-items: flex-start; gap: 9px;
  padding: 6px 8px; border-radius: 9px;
  transition: background 140ms ease, opacity 220ms ease, transform 220ms ease;
}
.plan-task:hover { background: var(--fill-1); }
.plan-task.removing { animation: slideOut var(--t-out) var(--ease-in) forwards; pointer-events: none; }
.plan-body .plan-task { animation: rise var(--t-in) var(--ease-out) backwards; }
.plan-live { animation: rise var(--t-in) var(--ease-out); }
.pt-tick { cursor: pointer; display: block; }
/* the checkbox is an override, not the mechanism: hidden until you reach for it */
.plan-task input { position: absolute; opacity: 0; width: 16px; height: 16px; left: 7px; top: 7px; margin: 0; cursor: pointer; }
.pt-text { flex: 1; min-width: 0; }
.pt-text b { cursor: text; border-radius: 4px; padding: 0 2px; margin: 0 -2px; outline: none; transition: background 140ms ease, box-shadow 140ms ease; }
.pt-text b:hover { background: rgba(255, 255, 255, 0.05); }
.pt-text b:focus { background: rgba(0, 0, 0, 0.35); box-shadow: 0 0 0 1px var(--accent); }
.pt-del {
  flex: 0 0 auto; width: 19px; height: 19px; margin-top: 1px;
  border: 0; background: transparent; color: var(--text-faint);
  border-radius: 5px; cursor: pointer; font-size: 14px; line-height: 1;
  display: grid; place-items: center; opacity: 0;
  transition: opacity 150ms ease, background 140ms ease, color 140ms ease;
}
.plan-task:hover .pt-del { opacity: 1; }
.pt-del:hover { background: rgba(220, 47, 106, 0.12); color: var(--bad); }
.plan-add {
  margin-top: 6px; align-self: flex-start;
  border: 1px dashed var(--line-2); border-radius: 8px;
  background: transparent; color: var(--text-faint);
  font: 500 10.5px 'IBM Plex Sans', sans-serif;
  padding: 5px 10px; cursor: pointer;
  transition: all 150ms ease;
}
.plan-add:hover { color: var(--accent-2); border-color: var(--accent); background: rgba(93, 135, 255, 0.06); }
.pt-mark {
  flex: 0 0 auto; width: 13px; height: 13px; margin-top: 2px;
  border-radius: 50%; border: 1.5px solid var(--line-2);
  display: grid; place-items: center;
  transition: all 180ms ease;
}
.plan-task:hover .pt-mark { border-color: var(--accent); }
.plan-task.doing .pt-mark { border-color: var(--accent-2); box-shadow: 0 0 0 3px rgba(93, 135, 255, 0.12); }
.plan-task.doing .pt-mark::after {
  content: ''; width: 5px; height: 5px; border-radius: 50%; background: var(--accent-2);
  animation: pulse 1.6s ease-in-out infinite;
}
.plan-task.done .pt-mark { border-color: var(--good); background: var(--good); }
.plan-task.done .pt-mark::after {
  content: '✓'; font-size: 9px; font-weight: 700; color: #0a0c10; line-height: 1;
}
.plan-task b { display: block; font-size: 12px; font-weight: 500; line-height: 1.4; }
.plan-task span { display: block; font-size: 10.5px; color: var(--text-faint); margin-top: 1px; }
.plan-task.done b { color: var(--text-faint); }
.plan-task.doing b { color: var(--text); font-weight: 600; }
@keyframes pulse { 50% { opacity: 0.4; } }

/* ---------------- sources (notebook) ---------------- */
.srcs { height: 100%; display: flex; flex-direction: column; padding: 10px 12px 12px; gap: 8px; }
.srcs-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
.srcs-none { color: var(--text-faint); font-size: 11.5px; line-height: 1.55; padding: 8px 4px; }
.src {
  display: flex; align-items: center; gap: 9px;
  border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px;
  transition: border-color 150ms ease, background 150ms ease;
  animation: rise var(--t-in) var(--ease-out) backwards;
}
.src:hover { border-color: var(--line-2); background: var(--fill-1); }
.src.removing { animation: slideOut var(--t-out) var(--ease-in) forwards; pointer-events: none; }
.src > i { font-style: normal; font-size: 12px; opacity: 0.7; }
.src-main { flex: 1; min-width: 0; cursor: pointer; }
.src-main b { display: block; font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.src-main span { display: block; font-size: 10px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.src-del { border: 0; background: transparent; color: var(--text-faint); cursor: pointer; font-size: 13px; border-radius: 6px; width: 22px; height: 22px; }
.src-del:hover { color: var(--bad); background: rgba(220, 47, 106, 0.1); }
.src-add { display: flex; flex-direction: column; gap: 6px; }
.src-add input, .src-add textarea {
  border: 1px solid var(--line-2); border-radius: 9px;
  background: rgba(0, 0, 0, 0.28); color: var(--text);
  font: 400 11.5px 'IBM Plex Sans', sans-serif;
  padding: 7px 10px; outline: none; resize: none;
}
.src-add textarea { min-height: 74px; }
.src-add input:focus, .src-add textarea:focus { border-color: var(--accent); }
.sa-row { display: flex; gap: 6px; }
.srcs-actions { flex: 0 0 auto; }

/* ---------------- lesson ---------------- */
.lesson { height: 100%; overflow-y: auto; padding: 14px 18px 18px; scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
.lesson-title { font-size: 16px; font-weight: 600; letter-spacing: -0.2px; padding-bottom: 10px; border-bottom: 1px solid var(--line); margin-bottom: 12px; }
.lesson-sec { margin-bottom: 16px; }
.lesson-sec h3 {
  font-size: 13px; font-weight: 600; color: var(--accent-2);
  display: flex; align-items: center; gap: 7px;
  cursor: pointer; border-radius: 7px; padding: 3px 6px; margin: 0 -6px 6px;
  transition: background 140ms ease;
}
.lesson-sec h3:hover { background: rgba(93, 135, 255, 0.07); }
.lesson-sec h3 i { font-style: normal; font-size: 11px; opacity: 0; color: #c9b8ff; transition: opacity 150ms ease; }
.lesson-sec h3:hover i { opacity: 1; }
/* A part the agent has just rewritten in place. Without this a paragraph
   silently becomes a different paragraph, which reads as a glitch rather than
   as an answer to what was asked. */
.lesson-sec.revised { animation: revised 2s var(--ease-out); border-radius: 8px; }
@keyframes revised {
  0% { background: rgba(93, 135, 255, 0.22); box-shadow: 0 0 0 8px rgba(93, 135, 255, 0.1); }
  35% { background: rgba(93, 135, 255, 0.16); box-shadow: 0 0 0 8px rgba(93, 135, 255, 0.07); }
  100% { background: transparent; box-shadow: 0 0 0 8px transparent; }
}
.lesson-body { font-size: 12.5px; line-height: 1.65; color: var(--text-dim); }
.lesson-body p + p { margin-top: 7px; }
.lesson-body strong { color: var(--text); }
.lesson-body code { font: 500 11px 'IBM Plex Mono', monospace; background: rgba(255, 255, 255, 0.07); border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; }
.lesson-body ul, .lesson-body ol { margin: 7px 0; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; }
.lesson-body > *:first-child { margin-top: 0; }
.lesson-body a { color: var(--accent-2); text-decoration: underline; text-underline-offset: 2px; }
.lesson-body a:hover { color: #fff; }
.lesson-body del { opacity: 0.55; }
.lesson-body mark { background: rgba(240, 168, 60, 0.22); color: #ffd79a; border-radius: 3px; padding: 0 3px; }
.lesson-body img { max-width: 100%; border-radius: 10px; margin: 8px 0; }
.lesson-body hr { border: 0; border-top: 1px solid var(--line); margin: 14px 0; }
.lesson-body .md-h { font: 600 12px 'IBM Plex Sans', sans-serif; color: var(--text); margin: 14px 0 6px; letter-spacing: -0.1px; }

/* generated illustration */
.genimg { margin: 12px 0; position: relative; border-radius: 12px; overflow: hidden; border: 1px solid var(--line); background: rgba(0, 0, 0, 0.3); }
.genimg img { display: block; width: 100%; height: auto; animation: imgIn 620ms var(--ease-out); }
/* the image is in the DOM from the moment it starts loading, so it must stay
   invisible until it actually has pixels — otherwise the placeholder jumps */
.genimg img.genimg-in { display: none; }
.genimg.ready img.genimg-in { display: block; }
@keyframes imgIn { from { opacity: 0; transform: scale(1.03); filter: blur(6px); } }
.genimg figcaption { padding: 7px 11px; font-size: 10.5px; color: var(--text-faint); border-top: 1px solid var(--line); }
.genimg-wait {
  aspect-ratio: 16 / 9; display: flex; align-items: center; justify-content: center; gap: 9px;
  font: 500 11px 'IBM Plex Sans', sans-serif; color: var(--text-faint);
  background: repeating-linear-gradient(115deg, rgba(255,255,255,0.028) 0 12px, transparent 12px 24px);
}
.genimg-wait i {
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
  animation: pulse 1.1s ease-in-out infinite;
}
.genimg-failed .genimg-wait { aspect-ratio: 6 / 1; color: var(--warn); }
.genimg-failed .genimg-wait i { background: var(--warn); animation: none; }

/* diagrams, charts and maths */
.viz {
  margin: 12px 0; padding: 10px;
  border: 1px solid var(--line); border-radius: 12px;
  background: rgba(0, 0, 0, 0.22);
  overflow-x: auto; text-align: center;
  scrollbar-width: thin; scrollbar-color: var(--line-2) transparent;
}
/* A diagram illustrates the prose — it must not swallow the tile. Capped by
   default; "bigger" opens it over everything rather than pushing the rest of
   the lesson down. The graphic itself is CONTENT: it is not a control, so a
   click on it selects, and a double-click marks the spot. */
.viz { position: relative; }
.viz svg { max-width: 100%; width: auto; height: auto; max-height: 420px; }

/* ---- what you can do with a picture ---- */
.viz-tools {
  position: absolute; right: 8px; bottom: 8px; z-index: 3;
  display: flex; gap: 5px;
  opacity: 0; transform: translateY(3px);
  transition: opacity 160ms ease, transform 200ms var(--ease-out);
}
.viz:hover > .viz-tools, .genimg:hover > .viz-tools, .viz-tools:focus-within { opacity: 1; transform: none; }
.viz-tool {
  height: 24px; padding: 0 9px;
  display: inline-flex; align-items: center; gap: 5px;
  border: 1px solid var(--line-2); border-radius: 7px;
  background: rgba(12, 15, 21, 0.92); backdrop-filter: blur(8px);
  color: var(--text-dim); cursor: pointer;
  font: 500 9px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 0.9px;
  transition: color 140ms ease, border-color 140ms ease, background 140ms ease;
}
.viz-tool svg { width: 11px; height: 11px; }
.viz-tool:hover { color: var(--text); border-color: var(--accent); background: rgba(93, 135, 255, 0.18); }

/* bigger: over everything, dismissed by clicking away or Escape */
#viz-zoom {
  position: fixed; inset: 0; z-index: 400;
  display: none; place-items: center; padding: 44px;
  background: rgba(6, 8, 12, 0.93); backdrop-filter: blur(6px);
}
#viz-zoom.on { display: grid; animation: rise 220ms var(--ease-out); }
/* A fixed box the picture scales INTO, rather than a box that inherits
   whatever size the picture happened to be drawn at where it came from. */
#viz-zoom .vz-body { display: grid; place-items: center; width: 92vw; height: 86vh; }
#viz-zoom svg { width: 100%; height: 100%; max-width: 92vw; max-height: 86vh; }
#viz-zoom img { max-width: 96vw; max-height: 88vh; object-fit: contain; border-radius: 12px; }
.vz-close {
  position: fixed; top: 16px; right: 20px;
  width: 34px; height: 34px; border-radius: 10px;
  border: 1px solid var(--line-2); background: var(--panel);
  color: var(--text-dim); font-size: 18px; line-height: 1; cursor: pointer;
  display: grid; place-items: center;
  transition: color 140ms ease, border-color 140ms ease;
}
.vz-close:hover { color: var(--text); border-color: var(--accent); }
.viz .viz-fallback {
  display: none;
  font: 400 10.5px/1.55 'IBM Plex Mono', monospace;
  color: var(--text-faint); text-align: left; white-space: pre-wrap;
}
/* not yet mounted, or the CDN never answered: show the source, it still reads */
.viz:not([data-done]) .viz-fallback,
.viz[data-done="offline"] .viz-fallback,
.viz.viz-error .viz-fallback { display: block; }
.viz.viz-error { border-color: rgba(240, 168, 60, 0.35); }
.viz.viz-error::before {
  content: 'source'; display: block; text-align: left;
  font: 600 8.5px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1px;
  color: var(--warn); margin-bottom: 5px;
}
.viz.mermaid-src.viz-error::before { content: 'diagram source'; }
.viz.chart-src.viz-error::before { content: 'chart source'; }
.katex { font-size: 1.02em; }
.katex-display { margin: 10px 0; overflow-x: auto; overflow-y: hidden; padding: 2px 0; }
pre.code.streaming { opacity: 0.75; }

/* tables */
.tablewrap { margin: 10px 0; overflow-x: auto; border: 1px solid var(--line); border-radius: 10px; scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
.tablewrap table { border-collapse: collapse; width: 100%; font-size: 12px; }
.tablewrap th, .tablewrap td { padding: 7px 11px; text-align: left; border-bottom: 1px solid var(--line); }
.tablewrap th {
  font: 600 9.5px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 0.9px;
  color: var(--text-faint); background: rgba(255, 255, 255, 0.035);
  position: sticky; top: 0;
}
.tablewrap tbody tr:last-child td { border-bottom: 0; }
.tablewrap tbody tr:hover td { background: rgba(255, 255, 255, 0.022); }
.tablewrap td strong { color: var(--text); }

/* callouts */
.callout {
  margin: 11px 0; border-radius: 11px; overflow: hidden;
  border: 1px solid var(--cl-line, var(--line-2));
  background: var(--cl-bg, var(--fill-1));
  border-left-width: 3px;
}
.callout-head {
  display: flex; align-items: center; gap: 7px;
  padding: 7px 12px 5px;
  font: 600 10px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1.1px;
  color: var(--cl-fg, var(--text-dim));
}
.callout-head i { font-style: normal; font-size: 11px; }
.callout-body { padding: 0 12px 9px; font-size: 12px; line-height: 1.6; color: var(--text-dim); }
.callout-body > p:first-child { margin-top: 0; }
.callout-body strong { color: var(--text); }
.callout.key   { --cl-bg: rgba(93, 135, 255, 0.09); --cl-line: rgba(93, 135, 255, 0.45); --cl-fg: var(--accent-2); }
.callout.tip   { --cl-bg: rgba(60, 226, 165, 0.08); --cl-line: rgba(60, 226, 165, 0.4);  --cl-fg: var(--good); }
.callout.note  { --cl-bg: rgba(255, 255, 255, 0.04); --cl-line: var(--line-2);            --cl-fg: var(--text-dim); }
.callout.warn  { --cl-bg: rgba(240, 168, 60, 0.09);  --cl-line: rgba(240, 168, 60, 0.45); --cl-fg: var(--warn); }
.callout.example { --cl-bg: rgba(167, 139, 250, 0.08); --cl-line: rgba(167, 139, 250, 0.42); --cl-fg: #c9b8ff; }
.callout.question { --cl-bg: rgba(255, 95, 143, 0.07); --cl-line: rgba(255, 95, 143, 0.38); --cl-fg: #ffa8c4; }

blockquote {
  margin: 10px 0; padding: 2px 0 2px 13px;
  border-left: 2px solid var(--line-2);
  color: var(--text-dim); font-style: italic;
}

/* task lists */
li.task { list-style: none; display: flex; align-items: flex-start; gap: 8px; margin-left: -18px; }
li.task i {
  flex: 0 0 auto; width: 12px; height: 12px; margin-top: 3px;
  border: 1.5px solid var(--line-2); border-radius: 3px;
  display: grid; place-items: center;
}
li.task.done i { background: var(--good); border-color: var(--good); }
li.task.done i::after { content: '✓'; font-size: 9px; font-weight: 700; color: #0a0c10; line-height: 1; }
li.task.done { color: var(--text-faint); }
pre.code {
  position: relative;
  margin: 9px 0; padding: 11px 13px;
  border: 1px solid var(--line); border-radius: 10px;
  background: rgba(0, 0, 0, 0.34);
  overflow-x: auto; scrollbar-width: thin; scrollbar-color: var(--line-2) transparent;
}
pre.code code {
  display: block; background: none; border: 0; padding: 0;
  font: 400 11.5px/1.6 'IBM Plex Mono', monospace;
  color: #cfe0ff; white-space: pre; tab-size: 2;
}
pre.code[data-lang]::after {
  content: attr(data-lang);
  position: absolute; top: 6px; right: 9px;
  font: 600 8.5px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1px;
  color: var(--text-faint);
}
.bubble pre.code { margin: 7px 0; }
.lesson-body li::marker { color: var(--accent); }
.lesson-body h4 { font: 600 10px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1.2px; color: var(--text-faint); margin: 10px 0 4px; }

/* ---------------- quiz ---------------- */
.quiz { height: 100%; overflow-y: auto; padding: 12px 14px 14px; scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
.quiz-title { font-size: 13.5px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 10px; }
.quiz-score { font: 600 10px 'IBM Plex Mono', monospace; font-style: normal; color: var(--good); border: 1px solid rgba(60, 226, 165, 0.35); border-radius: 999px; padding: 2px 8px; }
.qq { margin-bottom: 14px; }
.qq-p { font-size: 12.5px; font-weight: 500; margin-bottom: 7px; line-height: 1.5; }
.qq-free { font: 500 9px 'IBM Plex Mono', monospace; font-style: normal; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.8px; margin-left: 6px; }
.qq-choices { display: flex; flex-direction: column; gap: 5px; }
.qq-c {
  text-align: left; border: 1px solid var(--line); border-radius: 9px;
  background: var(--fill-1); color: var(--text-dim);
  font: 400 12px 'IBM Plex Sans', sans-serif;
  padding: 8px 11px; cursor: pointer;
  transition: all 140ms ease;
}
.qq-c:hover:not(:disabled) { color: var(--text); border-color: var(--line-2); }
.qq-c.sel { border-color: var(--accent); color: var(--text); background: rgba(93, 135, 255, 0.09); }
.qq-c.right { border-color: rgba(60, 226, 165, 0.6); color: var(--good); background: rgba(60, 226, 165, 0.07); }
.qq-c.wrong { border-color: rgba(255, 95, 143, 0.55); color: var(--bad); background: rgba(255, 95, 143, 0.06); }
.qq-c:disabled { cursor: default; }
.qq-t {
  width: 100%; min-height: 60px; resize: vertical;
  border: 1px solid var(--line-2); border-radius: 9px;
  background: rgba(0, 0, 0, 0.28); color: var(--text);
  font: 400 12px 'IBM Plex Sans', sans-serif;
  padding: 8px 10px; outline: none;
}
.qq-t:focus { border-color: var(--accent); }
.qq-ex { margin-top: 6px; font-size: 11px; line-height: 1.5; padding: 6px 9px; border-radius: 8px; animation: rise var(--t-in) var(--ease-out) backwards; }
.qq-c { animation: none; }
/* the graded state lands one question at a time */
.quiz .qq:nth-child(2) .qq-ex { animation-delay: 60ms; }
.quiz .qq:nth-child(3) .qq-ex { animation-delay: 120ms; }
.quiz .qq:nth-child(4) .qq-ex { animation-delay: 180ms; }
.quiz-score { animation: popIn var(--t-in) var(--ease-out) 140ms backwards; }
.qq-ex.ok { color: var(--good); background: rgba(60, 226, 165, 0.06); }
.qq-ex.no { color: #ffb4c9; background: rgba(255, 95, 143, 0.06); }
.quiz-go { margin-top: 4px; }
.quiz-after { font-size: 11px; color: var(--text-faint); padding: 6px 0; }

/* ---------------- flashcards ---------------- */
.fc { height: 100%; display: flex; flex-direction: column; padding: 10px 14px 12px; }
.fc-topic { font: 600 9px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1.2px; color: var(--text-faint); }
.fc-stage { flex: 1; min-height: 0; perspective: 900px; padding: 8px 0; }
.fc-card { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; transition: transform 480ms var(--ease-spring); cursor: pointer; }
.fc-card.flip { transform: rotateY(180deg); }
.fc-face {
  position: absolute; inset: 0; backface-visibility: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
  border: 1px solid var(--line-2); border-radius: 13px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.012));
  font-size: 13px; line-height: 1.5; text-align: center; padding: 14px 18px;
  overflow: hidden;
}
.fc-face span { font: 600 9px 'IBM Plex Mono', monospace; color: var(--text-faint); border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px; }
.fc-back { transform: rotateY(180deg); color: var(--accent-2); }
.fc-row { display: flex; gap: 7px; justify-content: center; }
.fc-done { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; text-align: center; }
.fc-done b { font-size: 14px; color: var(--good); }
.fc-done span { font: 500 10px 'IBM Plex Mono', monospace; color: var(--text-faint); }
.fc-done .fc-row { margin-top: 10px; }

/* ---------------- deck ---------------- */
.deck { height: 100%; display: flex; flex-direction: column; padding: 10px 12px 10px; outline: none; position: relative; }
.deck:fullscreen { padding: 32px 48px; background: var(--bg); }
/* Centre the slide in whatever box the grid gives us: a slide has a shape, and
   the drawn picture is 16:9, so the markdown form has to be 16:9 too — the two
   occupy the same box and the swap between them moves nothing. */
.deck-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; }
.deck-slide {
  position: relative;
  aspect-ratio: 16 / 9; max-width: 100%; max-height: 100%; width: 100%;
  border: 1px solid var(--line); border-radius: 13px; overflow: hidden;
  background: linear-gradient(160deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.012));
  animation: slideAdvance 340ms var(--ease-out);
}
@keyframes slideAdvance { from { opacity: 0; transform: translateX(14px); } }
/* The structured slide. Always present, so a picture that fails to load or was
   never drawn leaves a slide behind rather than an empty frame. */
.deck-md {
  position: absolute; inset: 0; overflow-y: auto; padding: 22px 26px;
  scrollbar-width: thin; scrollbar-color: var(--line-2) transparent;
  transition: opacity 320ms var(--ease-out);
}
.deck-slide.drawn .deck-md { opacity: 0; pointer-events: none; }
/* The drawn slide, over the top. `contain` because the render is exactly 16:9
   and the box is too — this only ever matters mid-resize. */
.deck-shot {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
  animation: shotIn 420ms var(--ease-out);
}
@keyframes shotIn { from { opacity: 0; } }
.deck-slide .sl > * { animation: rise 420ms var(--ease-out) 60ms backwards; }
.sl { height: 100%; display: flex; flex-direction: column; }
.sl h1 { font-size: clamp(20px, 3.4vw, 40px); font-weight: 600; letter-spacing: -0.6px; line-height: 1.15; }
.sl h2 {
  font-size: clamp(14px, 1.7vw, 22px); font-weight: 600; letter-spacing: -0.2px;
  color: var(--text); padding-bottom: 8px; margin-bottom: 12px;
  border-bottom: 1px solid var(--line);
}
.sl-title { align-items: flex-start; justify-content: center; gap: 10px; }
.sl-title p { font-size: clamp(12px, 1.4vw, 17px); color: var(--text-dim); max-width: 40ch; line-height: 1.5; }
.sl-quote { align-items: center; justify-content: center; text-align: center; gap: 14px; }
.sl-quote blockquote {
  border: 0; padding: 0; margin: 0; font-style: normal;
  font-size: clamp(15px, 2.2vw, 28px); font-weight: 500; line-height: 1.35; color: var(--text);
  max-width: 26ch;
}
.sl-quote blockquote::before { content: '“'; color: var(--accent); margin-right: 2px; }
.sl-quote blockquote::after { content: '”'; color: var(--accent); }
.sl-quote cite { font-style: normal; font: 500 11px 'IBM Plex Mono', monospace; color: var(--text-faint); letter-spacing: 0.6px; }
.sl-bullets ul { display: flex; flex-direction: column; gap: 12px; padding-left: 4px; margin-top: 4px; list-style: none; }
.sl-bullets li {
  position: relative; padding-left: 22px;
  font-size: clamp(12px, 1.35vw, 17px); line-height: 1.45; color: var(--text);
}
.sl-bullets li::before {
  content: ''; position: absolute; left: 2px; top: 0.55em;
  width: 7px; height: 7px; border-radius: 2px; background: var(--accent);
}
.sl-body, .sl-col { font-size: clamp(11.5px, 1.15vw, 14px); line-height: 1.6; color: var(--text-dim); min-width: 0; }
.sl-body > *:first-child, .sl-col > *:first-child { margin-top: 0; }
.sl-body strong, .sl-col strong { color: var(--text); }
.sl-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; flex: 1; min-height: 0; }
.sl-body .viz, .sl-col .viz { margin: 6px 0; }
/* a slide should not scroll — fit the visual to the slide, and give it the
   room it deserves when actually presenting */
.sl-body .viz svg, .sl-col .viz svg { max-height: 300px; }
.deck:fullscreen .sl-body .viz svg, .deck:fullscreen .sl-col .viz svg { max-height: 62vh; }
.deck:fullscreen .deck-md { padding: 40px 56px; }

/* an image slide is the picture, with the title floated over it */
.sl-image { position: relative; padding: 0; margin: -22px -26px; height: calc(100% + 44px); }
.sl-image h2 {
  position: absolute; left: 26px; right: 26px; top: 20px; z-index: 2;
  border: 0; padding: 0; margin: 0;
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.85);
}
.sl-image .sl-body { height: 100%; }
.sl-image .genimg { margin: 0; height: 100%; border: 0; border-radius: 0; }
.sl-image .genimg img { height: 100%; object-fit: cover; }
.sl-image .genimg-wait { height: 100%; aspect-ratio: auto; }
.sl-image figcaption {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  border: 0; padding: 26px 26px 16px; font-size: 12px; color: var(--text-dim);
  background: linear-gradient(180deg, transparent, rgba(10, 12, 16, 0.92));
}

.deck-notes {
  flex: 0 0 auto; margin-top: 8px; padding: 8px 11px;
  border-radius: 9px; border: 1px solid rgba(240, 168, 60, 0.3);
  background: rgba(240, 168, 60, 0.06);
  font-size: 11.5px; line-height: 1.55; color: var(--text-dim);
  max-height: 26%; overflow-y: auto; scrollbar-width: thin;
}
.deck-notes b {
  display: block; margin-bottom: 3px;
  font: 600 9px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1.1px; color: var(--warn);
}
.deck-bar { flex: 0 0 auto; display: flex; align-items: center; gap: 7px; padding-top: 9px; }
.dbtn {
  min-width: 26px; height: 24px; padding: 0 7px;
  border: 1px solid var(--line); border-radius: 7px;
  background: var(--fill-1); color: var(--text-dim);
  font: 500 12px 'IBM Plex Sans', sans-serif; cursor: pointer;
  display: grid; place-items: center;
  transition: all 140ms ease;
}
.dbtn.wide { font: 500 9.5px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 0.9px; }
.dbtn:hover:not(:disabled) { color: var(--text); border-color: var(--accent); background: rgba(93, 135, 255, 0.08); }
.dbtn:disabled { opacity: 0.3; cursor: default; }
.dbtn.on { color: var(--warn); border-color: rgba(240, 168, 60, 0.45); background: rgba(240, 168, 60, 0.08); }
.deck-dots { display: flex; gap: 5px; align-items: center; flex: 1; flex-wrap: wrap; }
.deck-dots i {
  width: 7px; height: 7px; border-radius: 50%; cursor: pointer;
  background: var(--line-2); transition: all 160ms ease;
}
.deck-dots i:hover { background: var(--text-faint); transform: scale(1.25); }
.deck-dots i.on { background: var(--accent); width: 16px; border-radius: 4px; }
.dbtn.count { font: 500 9.5px 'IBM Plex Mono', monospace; color: var(--text-faint); letter-spacing: 0.5px; }
/* Every slide at once — the one control a deck viewer is missing without it,
   and the fastest way back to "the one with the chart on it". */
.deck-grid {
  /* Stops above the bar rather than covering it: the control that opened the
     overview has to still be there to close it. */
  position: absolute; inset: 0 0 42px 0; z-index: 3;
  background: rgba(10, 12, 16, 0.94); backdrop-filter: blur(8px);
  padding: 16px; overflow-y: auto;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px;
  align-content: start;
  animation: gridIn 200ms var(--ease-out);
  scrollbar-width: thin; scrollbar-color: var(--line-2) transparent;
}
@keyframes gridIn { from { opacity: 0; } }
.deck-thumb {
  position: relative; aspect-ratio: 16 / 9; padding: 0; cursor: pointer;
  border: 1px solid var(--line); border-radius: 8px; overflow: hidden;
  background: var(--panel); display: grid; place-items: center;
  transition: border-color 140ms ease, transform 140ms var(--ease-out);
}
.deck-thumb:hover { border-color: var(--accent); transform: translateY(-2px); }
.deck-thumb.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.deck-thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
.deck-thumb-txt { padding: 10px; font-size: 11px; color: var(--text-dim); text-align: center; line-height: 1.35; }
.deck-thumb em {
  position: absolute; left: 5px; bottom: 4px; font-style: normal;
  font: 600 8.5px 'IBM Plex Mono', monospace; color: var(--text-faint);
  background: rgba(10, 12, 16, 0.75); border-radius: 3px; padding: 1px 4px;
}
.deck-drafthead { font-size: 12.5px; font-weight: 600; margin-bottom: 6px; }
.deck-thumb-row { display: flex; align-items: baseline; gap: 8px; padding: 5px 7px; border-radius: 7px; }
.deck-thumb-row em { font: 600 9px 'IBM Plex Mono', monospace; font-style: normal; color: var(--text-faint); min-width: 14px; }
.deck-thumb-row b { flex: 1; font-size: 11.5px; font-weight: 500; }
.deck-thumb-row i {
  font-style: normal; font: 500 8.5px 'IBM Plex Mono', monospace;
  color: var(--text-faint); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px;
}

/* ---------------- podcast ---------------- */
.podw { height: 100%; display: flex; flex-direction: column; padding: 10px 12px 12px; gap: 8px; }
.pod { display: flex; gap: 12px; align-items: center; }
.pod-art {
  width: 52px; height: 52px; flex: 0 0 auto; border-radius: 12px;
  display: grid; place-items: center;
  background: linear-gradient(135deg, hsl(330 60% 45% / 0.5), hsl(280 60% 40% / 0.45));
  border: 1px solid rgba(255, 255, 255, 0.12);
}
.pod-art svg { width: 22px; height: 22px; color: #fff; }
.pod-info { flex: 1; min-width: 0; }
.pod-title { font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pod-sub { font-size: 10px; color: var(--text-faint); margin: 1px 0 7px; }
.pod-row { display: flex; align-items: center; gap: 9px; }
.pod-play {
  width: 30px; height: 30px; flex: 0 0 auto; border-radius: 50%;
  border: 0; background: #fff; color: #111; cursor: pointer;
  font-size: 11px; display: grid; place-items: center;
  transition: transform 140ms ease, opacity 140ms ease;
}
.pod-play:hover:not(:disabled) { transform: scale(1.08); }
.pod-play:disabled { opacity: 0.4; cursor: default; }
.pod-track { flex: 1; height: 4px; border-radius: 3px; background: rgba(255, 255, 255, 0.1); overflow: hidden; }
.pod-track i { display: block; height: 100%; background: hsl(330 80% 70%); border-radius: 3px; transition: width 300ms ease; }
.pod-time { font: 500 9.5px 'IBM Plex Mono', monospace; color: var(--text-faint); }
.pod-nott { font-size: 10px; color: var(--warn); margin-top: 5px; }
.pod-lines { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 3px; scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
.pod-ln {
  display: flex; gap: 8px; align-items: flex-start; text-align: left;
  border: 0; border-radius: 8px; background: transparent;
  color: var(--text-dim); font: 400 11px 'IBM Plex Sans', sans-serif;
  line-height: 1.45; padding: 5px 8px; cursor: pointer;
  transition: background 130ms ease, color 130ms ease;
}
.pod-ln:hover { background: var(--fill-1); color: var(--text); }
.pod-ln.on { background: hsl(var(--h, 330) 70% 60% / 0.1); color: var(--text); box-shadow: inset 2px 0 0 hsl(var(--h, 330) 80% 65% / 0.7); }
.pod-ln em {
  flex: 0 0 auto; font-style: normal; font: 600 8.5px 'IBM Plex Mono', monospace;
  border-radius: 5px; padding: 1.5px 5px; margin-top: 1px;
}
.pod-ln.hA em { color: #ffa8d0; border: 1px solid rgba(255, 105, 180, 0.35); }
.pod-ln.hB em { color: #8caaff; border: 1px solid rgba(93, 135, 255, 0.4); }

/* ---------------- toast ----------------
   The app's own way of saying something went wrong. Never a native alert():
   that steals focus and reads as a browser error rather than as the product. */
#toast {
  position: fixed; z-index: 300;
  left: 50%; bottom: 26px;
  transform: translate(-50%, 14px);
  max-width: min(460px, 88vw);
  padding: 10px 16px;
  border-radius: 11px;
  background: var(--panel); border: 1px solid var(--line-2);
  box-shadow: var(--sh-lg);
  font: 500 12px 'IBM Plex Sans', sans-serif; color: var(--text);
  opacity: 0; pointer-events: none;
  transition: opacity 220ms ease, transform 320ms var(--ease-spring);
}
#toast.on { opacity: 1; transform: translate(-50%, 0); }
#toast.bad { border-color: rgba(220, 47, 106, 0.5); }
#toast.bad::before { content: '⚠ '; color: var(--bad); }
#toast.good { border-color: rgba(46, 204, 139, 0.5); }
#toast.good::before { content: '✓ '; color: var(--good); }

/* adding a step, in the list rather than in a dialog */
.plan-new { display: flex; gap: 6px; margin-top: 6px; animation: rise var(--t-in) var(--ease-out); }
.plan-new input {
  flex: 1; min-width: 0; height: 30px;
  border: 1px solid var(--accent); border-radius: 8px;
  background: rgba(0, 0, 0, 0.3); color: var(--text);
  font: 400 11.5px 'IBM Plex Sans', sans-serif; padding: 0 10px; outline: none;
}
.plan-new button {
  flex: 0 0 auto; height: 30px; padding: 0 12px;
  border: 0; border-radius: 8px; background: var(--accent); color: #fff;
  font: 600 11px 'IBM Plex Sans', sans-serif; cursor: pointer;
}
.plan-new button:hover { filter: brightness(1.1); }

/* submitting a quiz with blanks asks in place */
.quiz-go.warn { border-color: var(--warn); color: var(--warn); background: rgba(232, 165, 71, 0.1); }

/* The minimised pill wrapped onto two lines, which is why it was taller than
   the space the canvas reserved for it. */

/* Flashcard faces render markdown now, so they inherit the link/code colours
   from the lesson. The card is not a document — its text is the content. */
.fc-face code { color: inherit; background: rgba(255, 255, 255, 0.07); }
.fc-face a { color: inherit; text-decoration: underline; }
.fc-face strong { color: inherit; }

/* ---------------- the spine, in the conversation ----------------
   Matt's version of the plan: no window, no second thing to look at. It sits
   at the top of the chat card, above the transcript, and stays put while the
   history comes and goes — it is structure, not conversation. */
.msg-plan { max-width: 100%; align-self: stretch; }
.msg-plan .bubble { width: 100%; }
/* The pin is absolutely placed at the message's top-right, and the plan's own
   collapse chevron lives at the top-right of its card — so the head has to
   reserve that gutter, or the pin lands on top of the chevron and the step
   list cannot be opened at all. */
.msg-plan .cp-head { padding-right: 34px; }
/* A plan that has been replaced stays in the conversation where it was made —
   as a record, not as a control surface. */
.msg-plan.plan-past .cp { opacity: 0.55; filter: saturate(0.65); }
.msg-plan.plan-past .cp-actions, .msg-plan.plan-past .cp-chev { display: none; }
.msg-plan.plan-past .cp::before {
  content: 'replaced by a newer plan'; display: block;
  padding: 6px 11px 0;
  font: 600 8.5px 'IBM Plex Mono', monospace; text-transform: uppercase;
  letter-spacing: 1.1px; color: var(--text-faint);
}
.cp {
  border-radius: 12px;
  background: linear-gradient(180deg, rgba(93, 135, 255, 0.13), rgba(93, 135, 255, 0.04));
  border: 1px solid rgba(93, 135, 255, 0.4);
  overflow: hidden;
  animation: msg-in 340ms var(--ease-spring);
}
.cp-head {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 9px 11px; border: 0; background: transparent; cursor: pointer;
  color: var(--text); text-align: left;
  transition: background 140ms ease;
}
.cp-head:hover { background: rgba(255, 255, 255, 0.04); }
.cp-head b { flex: 1; min-width: 0; font-size: 12px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cp-head em { font: 500 9.5px 'IBM Plex Mono', monospace; font-style: normal; color: var(--text-faint); }
.cp-chev { width: 13px; height: 13px; color: var(--text-faint); transition: transform 240ms var(--ease-out); }
.cp-head[aria-expanded="true"] .cp-chev { transform: rotate(180deg); }
/* a progress dial small enough to be punctuation rather than a chart */
/* The card used to open on a progress ring, which said how far along it was to
   someone who already knew what it was. "x of y" sits right beside it and says
   the same thing in words, so the space goes to naming the thing instead. */
/* Kyle: "the word plan is too aggressive in green when plan is in expanded
   view". It is a label saying what the card is, not a status — so it reads as
   quiet type on a quiet fill, with no outline at all. */
.cp-tag {
  flex: 0 0 auto;
  font: 600 9.5px/1 'IBM Plex Sans', sans-serif;
  letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--text-dim);
  background: rgba(255, 255, 255, 0.06);
  border: 0;
  border-radius: 5px; padding: 4px 6px;
}
.cp-list { padding: 2px 8px 8px; display: flex; flex-direction: column; gap: 1px; }
.cp-list[hidden] { display: none; }   /* [hidden] loses to any author display rule */
.cp-step {
  display: flex; align-items: flex-start; gap: 8px; width: 100%;
  padding: 6px 8px; border: 0; border-radius: 7px;
  background: transparent; color: var(--text-dim); cursor: pointer;
  font: 400 11.5px 'IBM Plex Sans', sans-serif; text-align: left; line-height: 1.45;
  transition: background 140ms ease, color 140ms ease;
  animation: rise 300ms var(--ease-out) backwards;
}
.cp-step:hover { background: rgba(255, 255, 255, 0.05); color: var(--text); }
.cp-step i {
  flex: 0 0 auto; width: 11px; height: 11px; margin-top: 3px;
  border-radius: 50%; border: 1.5px solid var(--line-2);
  display: grid; place-items: center;
}
.cp-step.doing { color: var(--text); cursor: default; }
.cp-step.doing i { border-color: var(--accent-2); box-shadow: 0 0 0 3px rgba(93, 135, 255, 0.14); }
.cp-step.doing i::after {
  content: ''; width: 4px; height: 4px; border-radius: 50%; background: var(--accent-2);
  animation: pulse 1.6s ease-in-out infinite;
}
.cp-step.done { color: var(--text-faint); }
.cp-step.done span { text-decoration: line-through; text-decoration-color: var(--line-2); }
.cp-step.done i { border-color: var(--good); background: var(--good); }
.cp-step.done i::after { content: '✓'; font-size: 8px; font-weight: 700; color: #0a0c10; line-height: 1; }

.cp-now { padding: 10px 12px 11px; border-top: 1px solid rgba(93, 135, 255, 0.22); }
.cp-now.advancing { animation: stepDone 460ms var(--ease-out) forwards; }
.cp-now > em {
  display: flex; align-items: center; gap: 6px;
  font: 600 8.5px 'IBM Plex Mono', monospace; font-style: normal;
  text-transform: uppercase; letter-spacing: 1.3px; color: var(--accent-2);
  margin-bottom: 4px;
}
.cp-now > em::before {
  content: ''; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent-2); animation: pulse 1.6s ease-in-out infinite;
}
.cp-now > b { display: block; font-size: 13px; font-weight: 600; line-height: 1.4; }
.cp-now > p { font-size: 11px; color: var(--text-dim); margin-top: 4px; line-height: 1.5; }
.cp-empty { border-top: 0; }
.cp-empty > b { color: var(--text-dim); font-weight: 500; }
.cp-done {
  display: flex; align-items: flex-start; gap: 6px;
  margin-top: 8px; padding: 6px 8px;
  border-radius: 8px; background: rgba(46, 204, 139, 0.08);
  border: 1px solid rgba(46, 204, 139, 0.2);
}
.cp-done i { flex: 0 0 auto; width: 11px; height: 11px; margin-top: 2px; color: var(--good); }
.cp-done svg { width: 100%; height: 100%; stroke-width: 2; }
.cp-done span { font-size: 10.5px; color: var(--text-dim); line-height: 1.45; }
.cp-actions { display: flex; gap: 6px; margin-top: 10px; }
.cp-go {
  flex: 1; height: 31px; border: 0; border-radius: 8px;
  background: var(--accent); color: #fff; cursor: pointer;
  font: 600 11.5px 'IBM Plex Sans', sans-serif;
  transition: filter 150ms ease, transform 150ms ease, opacity 150ms ease;
}
.cp-go:hover:not(:disabled) { filter: brightness(1.12); transform: translateY(-1px); }
.cp-go:active:not(:disabled) { transform: translateY(0) scale(0.99); }
.cp-stuck {
  height: 31px; padding: 0 12px; border-radius: 8px;
  border: 1px solid var(--line-2); background: transparent; color: var(--text-dim);
  font: 500 11.5px 'IBM Plex Sans', sans-serif; cursor: pointer;
  transition: all 150ms ease;
}
.cp-stuck:hover:not(:disabled) { color: var(--warn); border-color: var(--warn); background: rgba(232, 165, 71, 0.08); }
.cp-go:disabled, .cp-stuck:disabled { opacity: 0.45; cursor: default; }

/* the spine writing itself, in the conversation */
/* Kyle: "the title text while it was being built was a bit too close to it."
   The tag and the title it labels need air between them while the title is
   still arriving — a name growing a character at a time hard against a chip
   reads as one run-on word. */
.cp-drafting .cp-head.as-text { cursor: default; padding-bottom: 4px; gap: 13px; }
.cp-drafting .cp-head.as-text:hover { background: transparent; }
.cp-drafting .cp-step { cursor: default; }
.cp-drafting .cp-step:hover { background: transparent; color: var(--text-dim); }
.cp-writing { padding: 2px 12px 10px; }

/* ---------------- the one control ----------------
   Matt: "there should be just a slider — how crazy do you want to go… and all
   of the parameters hang off just one." Everything else is still here, behind
   Advanced, and still tracked either way. */
.up-modes { display: flex; flex-direction: column; gap: 7px; margin: 14px 0 4px; }
.up-mode {
  text-align: left; padding: 11px 13px;
  border: 1px solid var(--line-2); border-radius: 12px;
  background: var(--fill-1); color: var(--text); cursor: pointer;
  transition: all 160ms ease;
}
.up-mode b { display: block; font-size: 13px; font-weight: 600; }
.up-mode span { display: block; font-size: 11px; color: var(--text-faint); margin-top: 3px; line-height: 1.45; }
.up-mode:hover { border-color: var(--accent); background: rgba(93, 135, 255, 0.06); }
.up-mode.on { border-color: var(--accent); background: rgba(93, 135, 255, 0.13); box-shadow: 0 0 0 3px rgba(93, 135, 255, 0.08); }
.up-mode.on b::after { content: ' ✓'; color: var(--accent-2); }
.up-adv {
  display: flex; align-items: center; justify-content: space-between; width: 100%;
  margin-top: 16px; padding: 9px 0;
  border: 0; border-top: 1px solid var(--line); background: transparent;
  color: var(--text-faint); cursor: pointer;
  font: 500 10px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 1.1px;
  transition: color 140ms ease;
}
.up-adv:hover { color: var(--text-dim); }
.up-adv svg { width: 13px; height: 13px; transition: transform 240ms var(--ease-out); }
.up-adv[aria-expanded="true"] svg { transform: rotate(180deg); }
.up-advbody[hidden] { display: none; }
.up-advbody .up-group:first-child { border-top: 0; padding-top: 0; margin-top: 4px; }

/* ---------------- the pace dial ----------------
   One control, and every parameter hangs off it. Matt: "there should be just a
   slider — how crazy do you want to go. Like, there's slow walk, walk, run,
   sprint." A range input rather than a row of buttons, because the difference
   between the settings is one of degree, not of kind. */
.dial { margin: 18px 0 4px; }
.dial input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 4px; border-radius: 3px; outline: none;
  background: linear-gradient(90deg, var(--accent) 0%, #7b5cff 100%);
  cursor: pointer;
}
.dial-custom input[type="range"] { background: var(--line-2); }
.dial input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 18px; height: 18px; border-radius: 50%;
  background: #fff; border: 0;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5), 0 0 0 3px rgba(93, 135, 255, 0.25);
  cursor: grab; transition: transform 140ms ease, box-shadow 140ms ease;
}
.dial input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.12); }
.dial input[type="range"]:active::-webkit-slider-thumb { cursor: grabbing; transform: scale(1.05); }
.dial input[type="range"]::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%; background: #fff; border: 0;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5), 0 0 0 3px rgba(93, 135, 255, 0.25);
  cursor: grab;
}
.dial-ticks { display: flex; justify-content: space-between; margin-top: 10px; }
.dial-ticks span {
  flex: 1; text-align: center;
  font: 500 10px 'IBM Plex Mono', monospace; letter-spacing: 0.4px;
  color: var(--text-faint); transition: color 180ms ease;
}
.dial-ticks span:first-child { text-align: left; }
.dial-ticks span:last-child { text-align: right; }
.dial-ticks span.on { color: var(--accent-2); font-weight: 600; }
.dial-sub {
  margin-top: 10px !important; min-height: 32px;
  font-size: 11.5px !important; line-height: 1.5; color: var(--text-dim) !important;
}
#gate-card .dial-sub { text-align: center; }
.gate-go {
  min-width: 130px; height: 40px; border: 0; border-radius: 11px;
  background: var(--accent); color: #fff; cursor: pointer;
  font: 600 13px 'IBM Plex Sans', sans-serif;
  transition: filter 150ms ease, transform 150ms ease;
}
.gate-go:hover { filter: brightness(1.1); transform: translateY(-1px); }

/* ================= the agent surface =================
   Matt's spec, and the whole point of the separation:

     "The chat is really two parts, fused together and never appearing in
      separate places… the chat input box should always be underneath the chat
      history. However the chat history should always be on the background —
      the canvas, the dark layer underneath the app windows. The chat input box
      is like an app container. It needs to be a different colour, it needs to
      be differentiated from the canvas. But the text itself just lays on top
      of the canvas."

   So: the transcript is text on the canvas — no card, no panel, nothing to
   mistake for a window. The composer is the only container, and it is the one
   thing that never moves. The agent is the surface; the apps sit on top of it. */
body.agent-surface #chatdock,
body.agent-surface #chatdock.fused {
  background: transparent !important;
  backdrop-filter: none !important;
  border-color: transparent !important;
  box-shadow: none !important;
  border-radius: 0 !important;
  padding: 0 !important;
}
/* the transcript, laid straight on the canvas */
body.agent-surface #chat-log {
  /* Must carry `--tail` too, and its own generous bottom. This is the mode you
     are in whenever no windows are open — so a flat 16px here was both the
     reply sitting on top of the composer AND the reason a sent message could
     not be held at the top: there was no room below it to scroll into. */
  padding: 0 2px calc(34px + var(--tail, 0px));
  gap: 16px;
  scrollbar-width: none;
}
body.agent-surface #chat-log::-webkit-scrollbar { width: 0; }
body.agent-surface .msg.agent .bubble {
  color: var(--text-dim);
  font-size: 14px; line-height: 1.68;
}
/* The learner's turn is where a conversation is divided. Scrolling back for
   "what did I ask that produced this?" was the thing the surface was worst at:
   a 22px dash above plain text reads as a paragraph break, not as a turn. So
   their words get a real container — restrained enough to stay text on a
   canvas, solid enough to find at a glance from three screens away. */
/* Matt: "for the user messages, I think we should remove the outlines… I'm
   finding the bright outlines a little loud & distracting to my eyes." A solid
   fill does the same job — the turn is still findable at a glance from three
   screens away — without drawing a blue box around every thing you said. */
body.agent-surface .msg.user .bubble {
  padding: 8px 14px 9px 13px;
  border-radius: 13px; border-top-left-radius: 5px;
  border: 0;
  background: rgba(93, 135, 255, 0.13);
  color: var(--text); font-weight: 500;
}
body.agent-surface .msg.user { align-self: flex-start; align-items: flex-start; max-width: 100%; }
body.agent-surface .msg.user .bubble::before { content: none; }
/* the marks under a learner's turn belong to it, inside its container */
body.agent-surface .msg.user .msg-marks { margin-top: 7px; }

/* the composer: the one container on the surface */
body.agent-surface #chat-form {
  background: linear-gradient(180deg, #1b202a, #141922);
  border: 1px solid var(--line-2);
  border-radius: 15px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  padding: 5px;
}
/* Focused is the state the composer is in for most of a session, so it is the
   last place that should be shouting. A slightly brighter grey edge, no ring. */
body.agent-surface #chat-form:focus-within {
  border-color: rgba(255, 255, 255, 0.22);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}
body.agent-surface #chat-input { background: transparent; border: 0; }
body.agent-surface #chat-input:focus { box-shadow: none; }

/* Apps take the foreground; the transcript recedes into the canvas it was
   already lying on rather than being clipped away. The composer never moves.
   (Pre-canvas-chat behaviour only: on the canvas the history stays readable
   in its column — hiding it here fought the canvas-chat rules below.) */
body.agent-surface:not(.canvas-chat).has-apps #chat-log-wrap { opacity: 0; pointer-events: none; }
body.agent-surface:not(.canvas-chat).has-apps #chat-log { transform: translateY(14px); }
body.agent-surface #chat-log-wrap {
  transition: opacity 320ms ease, grid-template-rows 380ms var(--ease-spring);
}
body.agent-surface #chat-log { transition: transform 380ms var(--ease-out); }

/* ---------------- pinning ----------------
   A conversation that runs long buries the thing you keep coming back to. So:
   ONE mechanism. Every message carries the same pin. Pinning MOVES it to the
   rail above the transcript and leaves a marker where it was said; unpinning
   drops it back exactly there. The plan is pinned by this and nothing else —
   which is what makes it unpinnable like anything else, and what lets a new
   plan simply take the pin from the old one. */
.msg { position: relative; }
/* What you can do to a message — pin it, copy it — in one cluster in one place,
   appearing on hover so the transcript stays text until you reach for it. */
.msg-tools { position: absolute; top: -3px; right: -2px; z-index: 2; display: flex; gap: 1px; }
.msg-pin, .msg-copy {
  width: 22px; height: 22px; padding: 0;
  border: 0; border-radius: 7px; background: transparent;
  color: var(--text-faint); cursor: pointer; opacity: 0;
  display: grid; place-items: center;
  transition: opacity 150ms ease, color 140ms ease, background 140ms ease;
}
.msg-copy svg { width: 12px; height: 12px; }
.msg:hover .msg-copy, .msg-copy:focus-visible { opacity: 1; }
.msg-copy:hover { color: var(--text); background: rgba(255, 255, 255, 0.08); }
.msg-copy.copied, .tbtn.copied { color: var(--good); opacity: 1; }
/* The plan is a card of controls, not text worth copying — and a second button
   here would sit on top of its own collapse chevron. */
.msg-plan .msg-copy { display: none; }
.msg-pin svg { width: 12px; height: 12px; }
.msg:hover .msg-pin, .msg-pin:focus-visible { opacity: 1; }
.msg-pin:hover { color: var(--accent-2); background: rgba(93, 135, 255, 0.14); }

/* Where a pinned message was said. The transcript is a record in order, and
   lifting a message out of it silently rewrote that order — scrolling back to
   find where something came up turned up a hole. */
.msg-slot { align-self: flex-start; }
/* The number reads first and the pin says what it means, so the number goes on
   the LEFT — in white, because it is the thing being matched between the two
   places and a tinted number is one more thing to decode. */
.slot-pin {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 8px 2px 7px; border-radius: 999px;
  border: 1px dashed rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.03);
  color: var(--text-dim); cursor: pointer;
  font: 600 10px 'IBM Plex Mono', monospace;
  opacity: 0.8;
  transition: opacity 140ms ease, background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.slot-pin:hover { opacity: 1; background: rgba(93, 135, 255, 0.14); border-color: rgba(255, 255, 255, 0.3); color: var(--accent-2); }
.slot-pin svg { width: 11px; height: 11px; order: 2; }
.slot-pin i { order: 1; font-style: normal; color: #fff; font-weight: 700; }
/* the same number, in the same white, on the pinned message itself — so either
   end answers "which one is this?" without a second look */
.pin-no {
  position: absolute; top: 1px; left: -16px; z-index: 3;
  min-width: 12px; text-align: right;
  color: #fff; font: 700 10px/1.5 'IBM Plex Mono', monospace;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}
#chat-pins .msg.pinned { padding-left: 4px; }
@keyframes pinFlash {
  0%, 100% { box-shadow: none; }
  35% { box-shadow: 0 0 0 3px rgba(93, 135, 255, 0.55); }
}
.msg.pin-flash { border-radius: 10px; animation: pinFlash 1.1s var(--ease-out); }

/* the rail: what is pinned, above the conversation, always in view */
#chat-pins {
  flex: 0 0 auto; min-height: 0; max-height: 52%;
  display: flex; flex-direction: column; gap: 9px;
  overflow-y: auto; scrollbar-width: none;
  padding: 2px 4px 11px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 9px;
  pointer-events: auto;
}
#chat-pins::-webkit-scrollbar { width: 0; }
#chat-pins:empty { display: none; }
#chat-pins .msg-pin { opacity: 0.5; color: var(--accent-2); }
#chat-pins .msg:hover .msg-pin { opacity: 1; }
#chat-pins .msg-pin:hover { color: var(--text); background: rgba(93, 135, 255, 0.2); }
/* a pinned line of talk gets a quiet marker and a ceiling; a pinned PLAN
   brings its own card and needs neither */
#chat-pins .msg:not(.msg-plan) .bubble {
  border-left: 2px solid rgba(255, 255, 255, 0.16);
  padding: 2px 22px 2px 11px;
  font-size: 12.5px; line-height: 1.55;
  scrollbar-width: none;
  position: relative;   /* the fade below anchors to the bubble, not the row */
}

/* ---------------- pins are small until you want them ----------------
   The rail is meant to be the things you are keeping in view, so each one has
   to cost almost nothing to keep there. Four pins at their full height are not
   a rail above the conversation, they ARE the conversation. So every pin rests
   at a couple of lines and opens to full size on a click anywhere in it —
   scrolling inside itself once open, rather than pushing the transcript down.
   The fade is the only affordance that survives being this small: a hard crop
   reads as a short message, a fading one reads as more. */
#chat-pins .msg { max-width: 100%; align-self: stretch; align-items: stretch; position: relative; cursor: zoom-in; }
#chat-pins .msg:not(.msg-plan) .bubble { max-height: 58px; overflow: hidden; }
#chat-pins .msg:not(.msg-plan):not(.pin-open) .bubble::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 22px;
  background: linear-gradient(to top, var(--panel) 20%, transparent);
  pointer-events: none;
}
#chat-pins .msg.pin-open { cursor: zoom-out; }
#chat-pins .msg.pin-open:not(.msg-plan) .bubble { max-height: 190px; overflow-y: auto; }

/* ---- what a pin says at rest ----
   Named on the first pin (see nameThePin). With a title, the resting pin is the
   title alone — the clipped two lines it used to show were usually two lines of
   preamble. Open it and the real words are there, which is what keeps the title
   a label rather than a substitute. */
.pin-title {
  font: 600 11.5px/1.45 'IBM Plex Sans', sans-serif;
  color: var(--text); padding: 1px 22px 0 11px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#chat-pins .msg.has-title:not(.pin-open) .bubble { display: none; }
#chat-pins .msg.has-title.pin-open .pin-title { color: var(--text-dim); margin-bottom: 3px; }

/* The way back to where it was said. Only when open — at rest a pin is meant to
   cost nothing, and a second control on two lines of text is not nothing. */
.pin-goto {
  display: none; margin: 7px 0 1px 11px;
  padding: 2px 9px; border-radius: 999px;
  border: 1px solid var(--line-2); background: transparent;
  color: var(--text-dim); cursor: pointer;
  font: 500 10px 'IBM Plex Mono', monospace; letter-spacing: 0.3px;
  transition: color 140ms ease, border-color 140ms ease, background 140ms ease;
}
#chat-pins .msg.pin-open .pin-goto { display: inline-block; }
.pin-goto:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.05); }
/* and the marker it lands on says "here" for a moment */
@keyframes slotFlash {
  0%, 100% { box-shadow: none; }
  30% { box-shadow: 0 0 0 3px rgba(93, 135, 255, 0.4); }
}
.msg-slot.slot-flash .slot-pin { animation: slotFlash 1.3s var(--ease-out); border-radius: 999px; }

/* The plan is not clipped by height — half a step is no use. Shut, it is the
   step you are on and the two buttons that move it; open, it is the whole
   thing. */
#chat-pins .msg-plan:not(.pin-open) .cp-head,
#chat-pins .msg-plan:not(.pin-open) .cp-done,
#chat-pins .msg-plan:not(.pin-open) .cp-list { display: none; }
/* Matt: "I also think we should show the plan building as it's created."
   A resting pin hides the list because a finished plan only needs to show the
   step you are on. While it is being WRITTEN there is no step you are on, and
   the list is the whole of what there is to see — so for that one state it
   comes back, collapsed or not. */
#chat-pins .msg-plan:not(.pin-open) .cp-drafting .cp-head,
#chat-pins .msg-plan:not(.pin-open) .cp-drafting .cp-list { display: block; }
#chat-pins .msg-plan.pin-open .bubble { max-height: 46vh; overflow-y: auto; scrollbar-width: none; }
#chat-pins .msg-plan .msg-tools { top: 7px; right: 7px; }

/* The leading edge of a streaming reply: each word arrives faint and settles.
   Only the newest one carries this — everything behind it is already solid, so
   the effect is a soft edge moving along the text rather than the whole reply
   pulsing. Short and cheap on purpose; it runs a few times a second.

   The duration here is only the ceiling: `fadeTail` writes a shorter one when
   the words are coming quickly, because the bubble is rebuilt on every release
   and a word still fading when the next lands would snap to solid. */
@keyframes wordIn { from { opacity: 0; } to { opacity: 1; } }
.w-in { animation: wordIn 260ms var(--ease-out) both; }
@media (prefers-reduced-motion: reduce) { .w-in { animation: none; } }

/* ---------------- thinking, in the chat ----------------
   The agent reasons where it talks. The block streams as the reasoning forms,
   carries the headline of what it is doing RIGHT NOW, and folds itself into
   one quiet line when the answer starts. Nothing has to be read at speed, and
   everything can be read back — which a pill that flickered past could not
   offer at any hold duration. */
.think-msg { max-width: 100%; }
.think { width: 100%; border-left: 2px solid var(--line-2); padding-left: 11px; }
.think-head {
  display: flex; align-items: center; gap: 7px; width: 100%;
  padding: 1px 0; border: 0; background: transparent;
  color: var(--text-faint); cursor: pointer; text-align: left;
  font: 500 11px 'IBM Plex Sans', sans-serif;
}
.think-head:hover { color: var(--text-dim); }
.think-dot {
  width: 5px; height: 5px; border-radius: 50%; flex: 0 0 auto;
  background: var(--accent); animation: pulse 1.2s ease-in-out infinite;
}
.think-msg.done .think-dot { background: var(--text-faint); animation: none; }
.think-what { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.think-chev { width: 12px; height: 12px; opacity: 0; transition: transform 220ms var(--ease-out), opacity 160ms ease; }
.think-msg.done .think-chev, .think-head:hover .think-chev { opacity: 0.8; }
.think-msg.shut .think-chev { transform: rotate(-90deg); }
.think-body {
  max-height: 168px; overflow-y: auto; scrollbar-width: none;
  transition: max-height 320ms var(--ease-out), opacity 200ms ease, margin 200ms ease;
}
.think-body::-webkit-scrollbar { width: 0; }
.think-body p { margin-top: 5px; font-size: 12px; line-height: 1.6; color: var(--text-faint); font-style: italic; }
.think-msg.shut .think-body { max-height: 0; opacity: 0; overflow: hidden; }
/* The cap belongs to the LIVE block, where it keeps a fast stream to a
   readable window that scrolls itself. Once the thought is finished and you
   have opened it deliberately, you get all of it — a cap with a hidden
   scrollbar reads as the end of the thinking rather than the middle of it. */
.think-msg.done:not(.shut) .think-body { max-height: none; overflow: visible; }

/* ---------------- annotations ----------------
   Right-click anything in an app to mark it. The mark does not send: it becomes
   an element in the chat input, so any number of them go to the agent together
   with whatever the learner wants to say around them. */
.annot-mark {
  outline: 2px solid rgba(93, 135, 255, 0.85) !important;
  outline-offset: 2px; border-radius: 4px;
  background: rgba(93, 135, 255, 0.1);
}
.annot-flash {
  outline: 2px solid var(--accent-2) !important;
  outline-offset: 3px; background: rgba(93, 135, 255, 0.2);
}
/* Marked TEXT, painted without touching the DOM — a tile repaints constantly,
   so a <mark> wrapped round the words would be gone by the next frame, and the
   browser's own selection is cleared the moment the note box takes focus. */
::highlight(annot) { background: rgba(93, 135, 255, 0.34); color: #fff; }
::selection { background: rgba(93, 135, 255, 0.35); }
.annot {
  position: fixed; z-index: 220;
  padding: 10px; border-radius: 13px;
  background: linear-gradient(180deg, #1b202a, #12161e);
  border: 1px solid var(--accent);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
  animation: rise 200ms var(--ease-out);
}
.annot-head { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.annot-app {
  font: 600 8.5px 'IBM Plex Mono', monospace; text-transform: uppercase;
  letter-spacing: 1.1px; color: var(--accent-2);
}
.annot-quote {
  font-size: 10.5px; line-height: 1.45; color: var(--text-faint);
  padding: 0 3px 7px 8px; margin-bottom: 7px;
  border-bottom: 1px solid var(--line);
  border-left: 2px solid var(--accent);
  max-height: 46px; overflow: hidden;
}
.annot-input {
  width: 100%; resize: none; min-height: 34px;
  border: 0; background: transparent; color: var(--text);
  font: 400 12.5px 'IBM Plex Sans', sans-serif; line-height: 1.5;
  padding: 2px 3px; outline: none;
}
.annot-actions { display: flex; align-items: center; gap: 8px; margin-top: 7px; }
.annot-hint { flex: 1; font: 500 9px 'IBM Plex Mono', monospace; letter-spacing: 0.5px; color: var(--text-faint); }
.annot-add, .annot-send {
  flex: 0 0 auto; height: 28px; padding: 0 13px;
  border: 0; border-radius: 8px; background: var(--accent); color: #fff;
  font: 600 11px 'IBM Plex Sans', sans-serif; cursor: pointer;
  transition: filter 140ms ease, background 140ms ease;
}
.annot-add:hover, .annot-send:hover { filter: brightness(1.12); }
/* Asking about it is the primary action, so it keeps the filled button.
   Saving — gathering several up before asking — recedes without hiding. */
.annot-add {
  background: transparent; color: var(--text-dim);
  border: 1px solid var(--line-2); padding: 0 11px;
}
.annot-add:hover { background: rgba(255, 255, 255, 0.05); color: var(--text); filter: none; }

/* ---- the pin: where they actually pointed ----
   A quote says WHAT was marked; only this says WHERE. It is a child of the
   tile rather than of its scrolling body, because a tile repaints constantly
   and replaces every node inside it — a pin in there would not survive the
   next frame. The layer is click-through so the app underneath stays usable. */
.marks-layer { position: absolute; inset: 0; z-index: 4; pointer-events: none; overflow: hidden; }
/* Moved with `translate`, not with left/top: a scroll then costs the compositor
   a transform instead of costing the browser a layout, and the `transform`
   below is left free to do the centring and the hover growth. */
.mark-pin {
  position: absolute; left: 0; top: 0;
  transform: translate(-50%, -50%);
  width: 20px; height: 20px; padding: 0;
  border: 2px solid #fff; border-radius: 50%;
  background: var(--bad); color: #fff; cursor: default;
  font: 700 10px 'IBM Plex Mono', monospace; line-height: 1;
  display: grid; place-items: center;
  /* A tight ring, not a halo. The wide coloured glow this used to wear sat on
     top of whatever the pin was pointing at — usually the very thing being
     asked about — and tinted it. */
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.55);
  pointer-events: auto;
  animation: popIn 260ms var(--ease-out);
  transition: box-shadow 160ms ease, transform 160ms var(--ease-out), background 160ms ease;
}
.mark-pin[hidden] { display: none; }
.mark-pin i { font-style: normal; }
/* a pin that can put the whole screen back says so before it is clicked */
.mark-pin.replayable { cursor: zoom-in; }
/* Sent, and standing there until it has been answered — a question about a
   spot is not finished with the moment you press send. */
/* A tight white breath on the pin, not a coloured ripple over the page. The
   ring used to expand to nearly twice the pin and wash pink across whatever it
   was pinned to — which is always the thing being asked about. */
.mark-pin.awaiting {
  border-color: #fff;
  animation: pinWait 1.9s ease-in-out infinite;
}
@keyframes pinWait {
  0%, 100% { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.55), 0 0 0 1.5px rgba(255, 255, 255, 0.55); }
  50% { box-shadow: 0 2px 9px rgba(0, 0, 0, 0.55), 0 0 0 3px rgba(255, 255, 255, 0.95), 0 0 10px rgba(255, 255, 255, 0.45); }
}
/* Brought back for a moment because they hovered its line in the transcript.
   White, because it is answering "where was that?" — it has to be findable
   against a lesson, a chart or a diagram alike, and a tinted pin disappears
   into whichever of those it landed on. */
.mark-pin.recalled {
  background: #fff; color: #0d1016; border-color: #fff;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
}
/* Hover: the PIN glows, not the page behind it. */
.mark-pin.on, .mark-pin:hover {
  transform: translate(-50%, -50%) scale(1.22);
  border-color: #fff;
  animation: pinGlow 1.15s ease-in-out infinite;
}
@keyframes pinGlow {
  0%, 100% { box-shadow: 0 2px 9px rgba(0, 0, 0, 0.6), 0 0 0 2px rgba(255, 255, 255, 0.85), 0 0 9px rgba(255, 255, 255, 0.5); }
  50% { box-shadow: 0 2px 11px rgba(0, 0, 0, 0.6), 0 0 0 3.5px #fff, 0 0 15px rgba(255, 255, 255, 0.85); }
}
/* what they said about it, without having to go and look in the chat */
.mark-bubble {
  position: absolute; left: 50%; bottom: calc(100% + 9px);
  transform: translateX(-50%);
  width: max-content; max-width: 230px;
  padding: 6px 9px; border-radius: 9px;
  background: rgba(12, 15, 21, 0.97); border: 1px solid var(--line-2);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.6);
  font: 400 11px/1.45 'IBM Plex Sans', sans-serif; color: var(--text);
  opacity: 0; pointer-events: none;
  transition: opacity 150ms ease, transform 200ms var(--ease-out);
}
.mark-pin:hover .mark-bubble, .mark-pin.on .mark-bubble { opacity: 1; transform: translateX(-50%) translateY(-2px); }

/* marks attached to the message being written */
#chat-marks { display: none; flex-wrap: wrap; gap: 6px; padding: 6px 6px 1px; }
body.has-marks #chat-marks { display: flex; }
.mark-chip {
  display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
  padding: 4px 4px 4px 10px; border-radius: 999px;
  border: 1px solid rgba(93, 135, 255, 0.4);
  background: rgba(93, 135, 255, 0.12);
  font: 400 10.5px 'IBM Plex Sans', sans-serif; color: var(--text-dim);
  animation: popIn 220ms var(--ease-out);
}
.mark-chip b { font-weight: 600; color: var(--text); max-width: 150px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mark-chip i { font-style: normal; color: var(--accent-2); max-width: 170px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mark-chip i:not(:empty)::before { content: '— '; color: var(--text-faint); }
.mark-chip.on { border-color: var(--accent-2); background: rgba(93, 135, 255, 0.24); }
/* a mark that is carrying a picture of the spot says so */
.mark-chip.has-shot::before, .mark-tag.has-shot > b::before {
  content: '▣'; margin-right: 5px; color: var(--accent-2); font-size: 10px;
}
.mark-x {
  flex: 0 0 auto; width: 17px; height: 17px;
  border: 0; border-radius: 50%; background: transparent;
  color: var(--text-faint); cursor: pointer; font-size: 12px; line-height: 1;
  display: grid; place-items: center;
  transition: background 140ms ease, color 140ms ease;
}
.mark-x:hover { background: rgba(255, 95, 143, 0.18); color: var(--bad); }

/* ---- the glyph a note wears ----
   The same shape on the chip in the composer, on the line in the transcript
   and on the window that opens from it, so a note is recognisable as a note
   before a word of it is read. */
.mark-glyph {
  flex: 0 0 auto; width: 12px; height: 12px;
  color: var(--accent-2); opacity: 0.85;
}
.mark-chip > .mark-glyph { margin-right: -2px; }

/* and how the same marks read back, in the transcript: what was pointed at on
   one line, what was said about it under it */
.msg-marks { display: flex; flex-direction: column; gap: 5px; margin-top: 8px; }
.mark-tag {
  display: grid; grid-template-columns: 12px 1fr; align-items: baseline;
  column-gap: 7px; row-gap: 1px;
  font-size: 11px; line-height: 1.45;
  border-left: 2px solid rgba(93, 135, 255, 0.4); padding: 1px 4px 1px 8px;
}
.mark-tag > .mark-glyph { grid-row: 1 / span 2; align-self: start; margin-top: 2px; }
.mark-tag b {
  grid-column: 2;
  font-weight: 400; color: var(--text-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* a mark that knows where it was: hovering it puts its pin back on the page */
.mark-tag.locatable { cursor: help; border-radius: 0 5px 5px 0; transition: background 140ms ease, border-color 140ms ease; }
.mark-tag.locatable:hover { background: rgba(93, 135, 255, 0.1); border-left-color: var(--accent-2); }
.mark-tag i { grid-column: 2; font-style: normal; color: var(--accent-2); }
.mark-tag i:empty { display: none; }
.u-nowords { font-style: italic; color: var(--text-faint); }

/* ================= WHAT THE AGENT MADE =================
   Everything built leaves a card in the conversation. A lesson scrolled past
   used to be a lesson gone — the window gets closed, the next one takes its
   place, and there is no way back to the good one from three topics ago. This
   is the way back, and it opens THAT artifact by id, not the newest one. */
.msg-asset .bubble { max-width: none; }
.asset {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px 10px 9px 11px; border-radius: 12px;
  border: 1px solid hsl(var(--h, 220) 60% 58% / 0.35);
  background: linear-gradient(180deg, hsl(var(--h, 220) 70% 60% / 0.13), rgba(20, 23, 30, 0.7));
  color: var(--text); cursor: pointer; text-align: left;
  transition: border-color 150ms ease, background 150ms ease, transform 150ms var(--ease-out);
}
.asset:hover { border-color: hsl(var(--h, 220) 75% 65% / 0.7); background: linear-gradient(180deg, hsl(var(--h, 220) 70% 60% / 0.22), rgba(20, 23, 30, 0.7)); }
.asset:active { transform: translateY(1px); }
.asset-ico {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 28px; height: 28px; border-radius: 9px;
  background: hsl(var(--h, 220) 70% 60% / 0.2);
  color: hsl(var(--h, 220) 85% 76%);
}
.asset-ico svg { width: 15px; height: 15px; }
.asset-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.asset-txt b {
  font: 600 12.5px 'IBM Plex Sans', sans-serif; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.asset-txt em {
  font: 500 9px 'IBM Plex Mono', monospace; font-style: normal;
  text-transform: uppercase; letter-spacing: 1.1px;
  color: hsl(var(--h, 220) 60% 70%);
}
.asset-go {
  flex: 0 0 auto; padding: 4px 11px; border-radius: 8px;
  border: 1px solid hsl(var(--h, 220) 60% 58% / 0.4);
  font: 600 10.5px 'IBM Plex Sans', sans-serif;
  color: hsl(var(--h, 220) 85% 80%);
}
.asset:hover .asset-go { background: hsl(var(--h, 220) 70% 60% / 0.2); }
/* an edit changes the card that is already there — it is the same thing */
@keyframes assetEdited {
  0%, 100% { box-shadow: none; }
  30% { box-shadow: 0 0 0 3px hsl(var(--h, 220) 75% 62% / 0.4); }
}
.msg-asset.asset-edited .asset { animation: assetEdited 1.3s var(--ease-out); }

/* ---- the card while the thing is still being written ----
   Same card, same place, so nothing jumps when it lands: it fills in rather
   than being replaced. The pulse is on the whole card because there is no
   progress to report — only that it is happening. */
@keyframes assetWriting {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 0.95; }
}
.msg-asset.asset-loading .asset {
  cursor: default; animation: assetWriting 1.5s ease-in-out infinite;
}
.msg-asset.asset-loading .asset:hover {
  border-color: hsl(var(--h, 220) 60% 58% / 0.4);
  background: linear-gradient(180deg, hsl(var(--h, 220) 70% 60% / 0.12), rgba(20, 23, 30, 0.7));
}
.msg-asset.asset-loading .asset-go {
  width: 13px; height: 13px; padding: 0; border-radius: 50%;
  border: 1.5px solid hsl(var(--h, 220) 60% 58% / 0.35);
  border-top-color: hsl(var(--h, 220) 85% 78%);
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ================= THE AGENT ASKING SOMETHING =================
   The only message on this surface that stops and waits, so it is the only one
   that gets a container, a border and a glow. Everything else the agent says is
   text laid on the canvas; this is a thing you have to answer. */
.msg-ask .bubble { max-width: none; }
.ask {
  border: 1px solid var(--accent);
  border-radius: 14px;
  background: linear-gradient(180deg, rgba(93, 135, 255, 0.14), rgba(20, 23, 30, 0.92));
  box-shadow: 0 0 0 4px rgba(93, 135, 255, 0.09), 0 14px 34px rgba(0, 0, 0, 0.45);
  padding: 12px 13px 11px;
  animation: rise var(--t-in) var(--ease-out);
}
/* it pulses, once, so it is not missed in a moving transcript */
body.awaiting-answer .ask:not(.ask-done) { animation: rise var(--t-in) var(--ease-out), askWait 2.6s var(--ease-out) 2; }
@keyframes askWait {
  0%, 100% { box-shadow: 0 0 0 4px rgba(93, 135, 255, 0.09), 0 14px 34px rgba(0, 0, 0, 0.45); }
  50% { box-shadow: 0 0 0 8px rgba(93, 135, 255, 0.16), 0 14px 34px rgba(0, 0, 0, 0.45); }
}
.ask-head { display: flex; align-items: center; gap: 8px; margin-bottom: 11px; }
.ask-badge {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 22px; height: 22px; border-radius: 7px;
  background: rgba(93, 135, 255, 0.2); color: var(--accent-2);
}
.ask-badge svg { width: 13px; height: 13px; }
.ask-head b { font: 600 13px 'IBM Plex Sans', sans-serif; color: var(--text); line-height: 1.4; }
.ask-qs { display: flex; flex-direction: column; gap: 12px; }
.ask-q { display: flex; flex-direction: column; gap: 5px; }
.ask-label { font: 500 11.5px 'IBM Plex Sans', sans-serif; font-style: normal; color: var(--text-dim); }
.ask-hint { font-size: 10.5px; color: var(--text-faint); line-height: 1.45; margin-top: -2px; }

/* choices: real answers, not a form. The likeliest is already picked, so Send
   works the moment the card lands. */
.ask-opts { display: flex; flex-wrap: wrap; gap: 6px; }
.ask-opt {
  display: flex; flex-direction: column; gap: 1px; align-items: flex-start;
  padding: 6px 11px; border-radius: 9px;
  border: 1px solid var(--line-2); background: var(--fill-1);
  color: var(--text-dim); cursor: pointer; text-align: left;
  font: 400 11.5px 'IBM Plex Sans', sans-serif;
  transition: border-color 140ms ease, background 140ms ease, color 140ms ease;
}
.ask-opt b { font-weight: 500; color: inherit; }
.ask-opt i { font-style: normal; font-size: 10px; color: var(--text-faint); }
.ask-opt:hover { border-color: var(--accent-2); color: var(--text); background: rgba(93, 135, 255, 0.1); }
.ask-opt.on {
  border-color: var(--accent); background: rgba(93, 135, 255, 0.22); color: var(--text);
  box-shadow: inset 0 0 0 1px rgba(93, 135, 255, 0.4);
}
.ask-opt.on i { color: var(--accent-2); }
/* "Not sure" is a real answer and looks like one — it is how the learner says
   which part to teach them, so it must never read as the failure option */
.ask-opt.unsure { border-style: dashed; color: var(--text-faint); }
.ask-opt.unsure.on { border-style: solid; border-color: var(--warn); background: rgba(240, 168, 60, 0.16); color: var(--text); box-shadow: none; }

/* a slider, for an amount or a level */
.ask-scale { display: flex; flex-direction: column; gap: 4px; padding-top: 2px; }
.sc-val { display: flex; align-items: baseline; gap: 4px; }
.sc-val b { font: 600 15px 'IBM Plex Mono', monospace; color: var(--accent-2); }
.sc-val span { font-size: 10.5px; color: var(--text-faint); }
.ask-scale input[type=range] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 20px; background: transparent; cursor: pointer;
}
.ask-scale input[type=range]::-webkit-slider-runnable-track {
  height: 5px; border-radius: 3px;
  background: linear-gradient(90deg, var(--accent) var(--fill, 50%), rgba(255, 255, 255, 0.1) var(--fill, 50%));
}
.ask-scale input[type=range]::-moz-range-track { height: 5px; border-radius: 3px; background: rgba(255, 255, 255, 0.1); }
.ask-scale input[type=range]::-moz-range-progress { height: 5px; border-radius: 3px; background: var(--accent); }
.ask-scale input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 16px; height: 16px; margin-top: -5.5px;
  border-radius: 50%; background: #fff; border: 3px solid var(--accent);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
  transition: transform 140ms var(--ease-out);
}
.ask-scale input[type=range]::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%; background: #fff; border: 3px solid var(--accent);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.ask-scale input[type=range]:hover::-webkit-slider-thumb { transform: scale(1.15); }
.ask-scale input[type=range]:focus-visible { outline: none; }
.ask-scale input[type=range]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 5px rgba(93, 135, 255, 0.3); }
.sc-ends { display: flex; justify-content: space-between; font-size: 9.5px; color: var(--text-faint); }

.ask-text input {
  width: 100%; padding: 7px 10px; border-radius: 9px;
  border: 1px solid var(--line-2); background: var(--fill-1); color: var(--text);
  font: 400 12px 'IBM Plex Sans', sans-serif; outline: none;
  transition: border-color 140ms ease;
}
.ask-text input:focus { border-color: var(--accent); }

.ask-foot { display: flex; align-items: center; gap: 10px; margin-top: 13px; }
.ask-note { flex: 1; font-size: 10px; line-height: 1.45; color: var(--text-faint); }
.ask-note em { font-style: normal; color: var(--warn); }
.ask-go {
  flex: 0 0 auto; height: 30px; padding: 0 16px;
  border: 0; border-radius: 9px; background: var(--accent); color: #fff;
  font: 600 12px 'IBM Plex Sans', sans-serif; cursor: pointer;
  transition: filter 140ms ease, transform 140ms var(--ease-out);
}
.ask-go:hover { filter: brightness(1.12); }
.ask-go:active { transform: translateY(1px); }
.ask-sent { opacity: 0.6; }

/* answered: the controls go, what they said stays. The card is the record of
   the exchange, so a reload shows the question and the answer, not a form. */
.ask-done {
  border-color: var(--line-2); background: var(--fill-1);
  box-shadow: none; animation: none;
}
.ask-done .ask-badge { background: rgba(255, 255, 255, 0.06); color: var(--text-faint); }
.ask-said { font: 500 12px 'IBM Plex Sans', sans-serif; color: var(--text); }
.ask-said.unsure { color: var(--warn); }
.ask-said.unsure::after { content: ' — so I explained it'; font-weight: 400; font-size: 10.5px; color: var(--text-faint); }

/* ================= THE AGENT POINTING AT SOMETHING =================
   The mirror of a learner's mark: a numbered marker on the exact phrase, with
   what was said about it in a bubble beside — never over — the thing itself. */
.point-layer { position: absolute; inset: 0; z-index: 4; pointer-events: none; overflow: hidden; }
::highlight(point) { background: rgba(60, 226, 165, 0.26); color: #fff; }
.point-dot {
  position: absolute; left: 0; top: 0;
  transform: translate(-2px, -50%);
  width: 17px; height: 17px; padding: 0;
  border: 2px solid var(--bg); border-radius: 50%;
  background: var(--good); color: #06231a;
  font: 700 9px 'IBM Plex Mono', monospace; line-height: 1;
  display: grid; place-items: center; cursor: pointer;
  pointer-events: auto;
  box-shadow: 0 2px 7px rgba(0, 0, 0, 0.5);
  animation: popIn 260ms var(--ease-out);
  transition: transform 160ms var(--ease-out), box-shadow 160ms ease;
}
.point-dot[hidden] { display: none; }
.point-dot i { font-style: normal; }
.point-dot:hover, .point-dot.on { transform: translate(-2px, -50%) scale(1.2); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6), 0 0 0 7px rgba(60, 226, 165, 0.18); }

.point-bubble {
  position: absolute; left: 0; top: 0;
  display: flex; align-items: flex-start; gap: 7px;
  padding: 8px 9px 8px 8px; border-radius: 11px;
  background: linear-gradient(180deg, #17231f, #101a17);
  border: 1px solid rgba(60, 226, 165, 0.45);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
  pointer-events: auto;
  animation: rise 220ms var(--ease-out);
}
.point-bubble .pb-face { flex: 0 0 auto; width: 17px; height: 17px; object-fit: contain; margin-top: 1px; }
/* Matt: "we should change the colour of the text… to be consistent with the
   colour of the text in the tool (where it's highlighting the area)". The
   highlight, the dot and the line the agent is saying about them are one
   gesture, so they are one colour — the line reads as belonging to the thing it
   is pointing at rather than as a note that happens to be nearby. */
.pb-text { flex: 1; font: 400 11.5px/1.5 'IBM Plex Sans', sans-serif; color: #b6f0d8; }
.pb-text strong, .pb-text b { color: var(--good); }
.pb-x {
  flex: 0 0 auto; width: 16px; height: 16px; margin: -1px -2px 0 0;
  border: 0; border-radius: 50%; background: transparent;
  color: var(--text-faint); cursor: pointer; font-size: 13px; line-height: 1;
  display: grid; place-items: center;
  transition: background 140ms ease, color 140ms ease;
}
.pb-x:hover { background: rgba(255, 255, 255, 0.09); color: var(--text); }
/* the arrow tracks the phrase, even when the bubble has been clamped to the
   edge of the window — it must point at the thing, not at its own middle */
.point-bubble::after {
  content: ''; position: absolute; left: var(--arrow, 50%);
  width: 9px; height: 9px; transform: translateX(-50%) rotate(45deg);
  background: #101a17; border: 1px solid rgba(60, 226, 165, 0.45);
}
.point-bubble:not(.below)::after { bottom: -5.5px; border-top: 0; border-left: 0; }
.point-bubble.below::after { top: -5.5px; border-bottom: 0; border-right: 0; background: #17231f; }

/* ---- the mark, drawn onto the record ----
   These only ever render inside a snapshot: the words that were marked, the
   spot that was clicked, and what was said about it, drawn into the stored copy
   at save time. Without them the record is the page a moment BEFORE the note,
   which is the one thing it must not be — the note is why it was kept. */
.snap-quote {
  background: rgba(93, 135, 255, 0.3);
  box-shadow: 0 0 0 2px rgba(93, 135, 255, 0.3);
  border-radius: 3px; color: #fff;
}
.snap-pin { position: absolute; z-index: 40; transform: translate(-50%, -50%); }
.snap-pin-dot {
  position: absolute; left: 0; top: 0; transform: translate(-50%, -50%);
  width: 21px; height: 21px;
  border: 2px solid #fff; border-radius: 50%;
  background: var(--bad, #ff5f8f); color: #fff;
  font: 700 10px/17px 'IBM Plex Mono', monospace; font-style: normal; text-align: center;
  box-shadow: 0 2px 9px rgba(0, 0, 0, 0.65);
}
.snap-note {
  position: absolute; left: 16px; top: -10px;
  display: block; width: max-content; max-width: 260px;
  padding: 7px 10px; border-radius: 10px;
  background: rgba(12, 15, 21, 0.97);
  border: 1px solid rgba(255, 95, 143, 0.5);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.6);
}
.snap-note b { display: block; font: 500 11.5px/1.45 'IBM Plex Sans', sans-serif; color: #edf0f6; }
.snap-note em {
  display: block; margin-top: 3px; font-style: normal;
  font: 500 8.5px 'IBM Plex Mono', monospace; text-transform: uppercase;
  letter-spacing: 0.9px; color: #5e6774;
}
.snap-note em:empty { display: none; }

/* ================= THE SCREEN, REPLAYED =================
   What the workspace looked like when a note was taken, in a window on top of
   the conversation it was taken from. Deliberately NOT full screen: a record
   of the screen that fills the screen reads as the app having navigated
   somewhere, and the point is that you have not gone anywhere — the note you
   clicked is still there behind it, and one Escape puts it away. */
#replay {
  position: fixed; inset: 0; z-index: 420;
  display: none; place-items: center;
}
#replay.on { display: grid; }
.rp-scrim {
  position: absolute; inset: 0;
  background: rgba(5, 7, 11, 0.72); backdrop-filter: blur(7px);
  animation: scrimIn 200ms ease;
}
@keyframes scrimIn { from { opacity: 0; } }
.rp-win {
  position: relative;
  width: min(1500px, 92vw); height: min(950px, 88vh);
  display: flex; flex-direction: column;
  border: 1px solid var(--line-2); border-radius: 16px;
  background: linear-gradient(180deg, #12161e, #0d1015);
  box-shadow: var(--sh-lg);
  overflow: hidden;
  animation: rise var(--t-in) var(--ease-out);
}
.rp-head {
  flex: 0 0 auto; display: flex; align-items: center; gap: 10px;
  padding: 10px 12px 10px 14px;
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, rgba(93, 135, 255, 0.08), transparent);
}

/* ---- what happened next ----
   The screen is the question; this is the answer. Under the record rather than
   beside it, so the screen keeps its full width and this reads as the thing
   that FOLLOWED — which is what it was. */
.rp-answer {
  flex: 0 0 auto; max-height: 30%; overflow-y: auto;
  padding: 12px 16px 14px;
  border-top: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.02);
  display: flex; gap: 26px; align-items: flex-start;
}
.rp-answer[hidden] { display: none; }
.rp-answer b {
  display: block; margin-bottom: 4px;
  font: 500 9.5px 'IBM Plex Mono', monospace; letter-spacing: 0.9px;
  text-transform: uppercase; color: var(--text-faint);
}
.rp-said { flex: 1 1 auto; min-width: 0; }
.rp-said p { font-size: 12.5px; line-height: 1.6; color: var(--text-dim); }
.rp-did { flex: 0 1 320px; min-width: 0; }
.rp-did ul { list-style: none; display: flex; flex-direction: column; gap: 4px; }
.rp-did li {
  font-size: 11.5px; line-height: 1.45; color: var(--good);
  padding-left: 13px; position: relative; overflow-wrap: anywhere;
}
.rp-did li::before { content: '›'; position: absolute; left: 2px; color: var(--good); opacity: 0.7; }
.rp-badge { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 8px;
  background: rgba(93, 135, 255, 0.16); color: var(--accent-2); }
.rp-badge .mark-glyph { width: 13px; height: 13px; opacity: 1; }
.rp-titles { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.rp-what {
  font: 600 12.5px 'IBM Plex Sans', sans-serif; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rp-when {
  font: 500 9px 'IBM Plex Mono', monospace; text-transform: uppercase;
  letter-spacing: 1px; color: var(--text-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rp-tools { flex: 0 0 auto; display: flex; align-items: center; gap: 6px; }
.rp-zoom, .rp-out, .rp-x {
  height: 26px; border-radius: 8px;
  border: 1px solid var(--line-2); background: var(--fill-1);
  color: var(--text-dim); cursor: pointer;
  display: grid; place-items: center;
  transition: color 140ms ease, border-color 140ms ease, background 140ms ease;
}
.rp-zoom { padding: 0 10px; min-width: 52px; font: 600 10px 'IBM Plex Mono', monospace; }
.rp-out, .rp-x { width: 26px; text-decoration: none; }
.rp-out svg, .rp-x svg { width: 13px; height: 13px; }
.rp-zoom:hover, .rp-out:hover, .rp-x:hover { color: var(--text); border-color: var(--accent); background: rgba(93, 135, 255, 0.14); }
.rp-x:hover { border-color: var(--bad); color: var(--bad); background: rgba(255, 95, 143, 0.12); }

.rp-body {
  flex: 1; min-height: 0; position: relative;
  display: grid; place-items: center;
  padding: 12px; overflow: hidden;
  background:
    radial-gradient(700px 400px at 50% 0%, rgba(93, 135, 255, 0.05), transparent 70%),
    repeating-linear-gradient(45deg, rgba(255,255,255,0.012) 0 8px, transparent 8px 16px);
}
/* at 100% the record is bigger than the window, so the window scrolls */
#replay.rp-scrolls .rp-body { place-items: start; overflow: auto; }
.rp-stage {
  position: relative; flex: 0 0 auto;
  border-radius: 10px; overflow: hidden;
  border: 1px solid var(--line-2);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
  opacity: 0; transition: opacity 260ms ease;
}
#replay.loaded .rp-stage { opacity: 1; }
.rp-stage iframe {
  display: block; border: 0; background: var(--bg);
  transform-origin: top left;
}
/* it is a photograph you can scroll, not a page you can leave */
.rp-body::after {
  content: 'a record — nothing here is live';
  position: absolute; right: 14px; bottom: 10px;
  font: 500 8.5px 'IBM Plex Mono', monospace; text-transform: uppercase;
  letter-spacing: 1px; color: var(--text-faint); pointer-events: none;
}
@media (max-width: 720px) {
  .rp-win { width: 96vw; height: 92vh; }
}

/* A note that kept the screen it was made on. Clicking it opens that screen
   again — so it reads as something you can open, not something you can hover. */
.mark-tag.replayable { cursor: zoom-in; }
.mark-tag.replayable:hover, .mark-tag.replayable:focus-visible {
  background: rgba(93, 135, 255, 0.14); border-left-color: var(--accent-2); outline: none;
}
.mark-tag.replayable:focus-visible { box-shadow: 0 0 0 2px rgba(93, 135, 255, 0.55); }
.mark-tag.replayable:hover > .mark-glyph, .mark-tag.replayable:focus-visible > .mark-glyph { opacity: 1; }
.mark-tag.replayable b::after {
  content: '↗'; margin-left: 6px; font-size: 9px; color: var(--accent-2);
  opacity: 0; transition: opacity 140ms ease;
}
.mark-tag.replayable:hover b::after, .mark-tag.replayable:focus-visible b::after { opacity: 0.9; }

/* With nothing in front of it the transcript IS the canvas, so it gets a
   comfortable reading column rather than a 400px gutter. When apps open it
   yields the middle and retreats to its dock — the composer never moves
   between the two, it just narrows. */
/* superseded by canvas-chat: the dock spans the stage and the columns inside
   it do the positioning */
body.agent-surface:not(.canvas-chat):not(.has-apps) #chatdock {
  left: 50% !important; right: auto !important;
  width: min(720px, 92vw) !important;
  transform: translateX(-50%);
  top: 64px !important; bottom: 18px !important;
  justify-content: flex-end !important;
}
body.agent-surface:not(.canvas-chat):not(.has-apps) #chat-log { font-size: 14.5px; }
body.agent-surface #chatdock { transition: width 480ms var(--ease-out), left 480ms var(--ease-out), transform 480ms var(--ease-out); }

/* ================= THE CANVAS IS THE CHAT =================
   Matt, precisely:

     "The chat history should always be on the background. There is a canvas on
      the back of the thing, the dark layer that is underneath the app windows.
      The canvas is where the text of the chat history is displayed. However
      the chat input box is like an app container — a different colour,
      differentiated from the canvas. But the text itself just lays on top of
      the canvas… Only when the agent starts to invoke applications does the
      chat history potentially disappear. It dissolves to the background."

   So the transcript is not in a panel at all. It is painted across the whole
   canvas, UNDER the app windows, and the composer is a container fixed beneath
   it. Apps are opaque and float on top, so as they fill the screen they cover
   the conversation — that IS the dissolve. Nothing is hidden by a rule; the
   history stays exactly where it was and comes back as windows close. */
body.canvas-chat #chatdock {
  position: absolute;
  left: 0 !important; right: 0 !important; top: 0 !important; bottom: 0 !important;
  width: auto !important; transform: none !important;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 0 0 16px;
  background: none !important; border: 0 !important; box-shadow: none !important;
  backdrop-filter: none !important; border-radius: 0 !important;
  pointer-events: none;              /* the canvas beneath stays reachable */
  z-index: 1;                        /* .tile is 2 — apps cover the talk */
}
/* A flex column, so the log inside it is a real, bounded scroller in EVERY
   state. display:block here made the log's height meaningless the moment the
   wrap went auto-height, and the history was clipped with no way to scroll.
   Centred is written as an explicit margin (not auto) so the move to the left
   column animates. */
body.canvas-chat #chat-log-wrap {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
  opacity: 1;
  overflow: hidden;
  margin: 0; margin-left: calc(50% - min(380px, 42vw));
  width: min(760px, 84vw);
  pointer-events: auto;
}
/* the fused-card fades (body.acting) do not apply on the canvas: windows
   covering the transcript IS the dissolve, nothing else hides it */
body.canvas-chat.acting #chat-log-wrap { opacity: 1; pointer-events: auto; }
body.canvas-chat #chat-log {
  height: 100%; max-height: none;
  padding: 18px 4px 20px; gap: 18px;
  overflow-y: auto; scrollbar-width: none;
}
/* The newest sits just above the box. NOT justify-content: flex-end — on an
   overflowing flex column that puts the history above the start edge, outside
   the scrollable area, and the conversation cannot be scrolled back. A spacer
   with an auto margin pushes short content down and collapses once it fills. */
body.canvas-chat #chat-log::before { content: ''; margin-top: auto; }
body.canvas-chat #chat-log::-webkit-scrollbar { width: 0; }

/* ---- the column ----
   ONE column holds the whole conversation: what is pinned, then the
   transcript, then the composer. They are one surface (commandment 5), so they
   share one width and one margin and move together — the pins and the plan are
   not separately-positioned furniture floating over the composer any more.
   Centred is written as an explicit margin (not auto) so the move animates. */
body.canvas-chat #chat-log-wrap,
body.canvas-chat #chat-form,
body.canvas-chat #hero {
  margin-left: calc(50% - var(--talk-mid-half)); margin-right: 0;
  width: var(--talk-mid);
  transition: margin-left var(--talk-move) var(--ease-out), width var(--talk-move) var(--ease-out), opacity 320ms ease;
}
/* An invisible twin of the column with NO transition on it, so the grid can
   ask where the conversation is ABOUT to be rather than where it currently is
   mid-slide. Measuring the real thing during the move parked tiles half off
   the canvas until the transition ended and a second relayout fixed them. */
#talk-probe { height: 0; visibility: hidden; pointer-events: none; }
body.canvas-chat #talk-probe { margin-left: calc(50% - var(--talk-mid-half)); width: var(--talk-mid); }
body.canvas-chat.side-talk #talk-probe { margin-left: 18px; width: var(--talk-side); }
body.canvas-chat #chat-form {
  pointer-events: auto;
  position: relative; z-index: 5;    /* above the app windows: never covered */
  /* The agent-surface rules zero the dock's padding with !important, so the
     gap under the composer has to be its own margin — without it the box sits
     flush against the bottom edge of the window. */
  margin-bottom: 16px;
}
/* nothing in the column hugs the top bar */
body.canvas-chat #chat-log-wrap { position: relative; z-index: 1; padding-top: 14px; }

/* ---- no dots behind the conversation ----
   Matt: "the isometric dots in the background feel a bit distracting. I do like
   these in the whitespace, but could we try removing these from the chat panel
   so they don't show under text & tables etc?"

   So the dots are masked out of the column the conversation occupies, feathered
   at the edges so it reads as the text having room rather than as a rectangle
   cut out of the pattern. The band follows the same `--talk` numbers the column
   itself uses; the 14px is `#workspace`'s inset, since these coordinates are
   the canvas's and the column's are the dock's. */
body.canvas-chat #canvas::before {
  --talk-l: calc(50% - var(--talk-mid-half) - 26px);
  --talk-r: calc(50% + var(--talk-mid-half) + 26px);
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 var(--talk-l),
    transparent calc(var(--talk-l) + 48px), transparent calc(var(--talk-r) - 48px), #000 var(--talk-r), #000 100%);
  mask-image: linear-gradient(to right, #000 0, #000 var(--talk-l),
    transparent calc(var(--talk-l) + 48px), transparent calc(var(--talk-r) - 48px), #000 var(--talk-r), #000 100%);
}
/* Against the left wall there is nothing to feather INTO — the column runs to
   the edge of the screen. Kyle: "there is a column of dots just barely visible
   on the left, lets make it so that chat window where the dots hide extends to
   the edge of the screen." The shared gradient above fades in from x=0, which
   is exactly that sliver, so this mode gets its own gradient: clear from the
   wall, feathering only on the inside edge. */
body.canvas-chat.side-talk #canvas::before {
  --talk-r: calc(var(--talk-side) + 30px);
  -webkit-mask-image: linear-gradient(to right, transparent 0,
    transparent calc(var(--talk-r) - 48px), #000 var(--talk-r), #000 100%);
  mask-image: linear-gradient(to right, transparent 0,
    transparent calc(var(--talk-r) - 48px), #000 var(--talk-r), #000 100%);
}
/* folded away, the column is not there to clear */
body.canvas-chat.talk-folded #canvas::before,
body.canvas-chat:not(.has-msgs):not(.chat-center) #canvas::before {
  -webkit-mask-image: none; mask-image: none;
}

/* the workspace must not paint over the conversation */
body.canvas-chat #canvas::after { z-index: 0; }
body.canvas-chat #workspace { background: transparent; }
/* the empty state is already the canvas — no separate centred mode needed */
body.canvas-chat.chat-center #chatdock { top: 0 !important; bottom: 0 !important; }
body.canvas-chat #hero { padding-bottom: 20px; pointer-events: auto; }
/* The conversation is the canvas, so there is no dock column to make room for:
   the workspace spans the whole stage, and metrics() measures the same thing.
   A mismatch here draws tiles wider than the canvas holding them. */
body.canvas-chat #workspace {
  left: 14px !important; right: 14px !important; top: 14px !important;
  bottom: 14px !important;
  opacity: 1 !important; pointer-events: auto !important;
}
/* #chatdock must NOT create a stacking context, or its children cannot rise
   above the app windows however high their z-index: they would only be ordered
   within the dock. The transcript sits below the windows, the composer above
   them, and the dock itself takes no part. */
body.canvas-chat #chatdock { z-index: auto !important; }
/* The pinned plan can grow; cap the STEP LIST rather than the card, so the
   head stays clickable and the current step stays whole. */
body.canvas-chat .msg-plan .cp-list { max-height: 24vh; overflow-y: auto; scrollbar-width: thin; }

/* ================= where the conversation lives =================
   Not a preference — it follows the work, and the work is what is in the
   WORKSPACE. With nothing open, the conversation is the screen: centred, wide,
   composer beneath it. The moment a window exists there is something to move
   aside for, so the whole column slides to the left; close the last window and
   it slides back. Nothing else moves it — talking is not a reason to shrink
   the thing you are talking in. */

/* nothing said yet: the conversation owns the middle */
body.canvas-chat:not(.side-talk).chat-center #chatdock { justify-content: center; }
body.canvas-chat:not(.side-talk).chat-center #chat-log-wrap { flex: 0 0 auto; }
/* said something, still nothing open: reads at full size, in the middle */
/* `--tail` has to be carried by EVERY mode that scrolls, not just the base
   rule: each one sets the padding shorthand at a higher specificity, so a tail
   that only works in one of them is a feature that works on one screen. */
body.canvas-chat:not(.side-talk) #chat-log { padding: 20px 6px calc(30px + var(--tail, 0px)); gap: 20px; }
body.canvas-chat:not(.side-talk) .msg.agent .bubble { font-size: 15px; line-height: 1.72; }
body.canvas-chat:not(.side-talk) .msg.user .bubble { font-size: 14px; }

/* a window opens: the WHOLE surface — pins, history and composer — takes the
   left column, and stays there until the workspace is empty again. */
body.canvas-chat.side-talk #chat-log-wrap,
body.canvas-chat.side-talk #chat-form,
body.canvas-chat.side-talk #hero {
  margin-left: 18px;
  width: var(--talk-side);
}
body.canvas-chat.side-talk #chat-log-wrap { margin-bottom: 10px; }
body.canvas-chat.side-talk #chat-log { padding: 0 4px calc(24px + var(--tail, 0px)); font-size: 13px; }

/* folded away — for a full-screen app, or because they asked. It becomes a tab
   on the left wall; clicking it brings the conversation back. */
body.canvas-chat.talk-folded #chat-log-wrap { opacity: 0; pointer-events: none; }
#talk-tab {
  position: fixed; left: 0; top: 50%; transform: translateY(-50%);
  z-index: 6;
  width: 34px; padding: 12px 0 14px;
  border: 1px solid var(--line-2); border-left: 0;
  border-radius: 0 12px 12px 0;
  background: linear-gradient(180deg, #1b202a, #141922);
  box-shadow: 6px 0 20px rgba(0, 0, 0, 0.45);
  cursor: pointer;
  display: none; flex-direction: column; align-items: center; gap: 8px;
  transition: width 180ms ease, background 180ms ease;
}
body.canvas-chat.talk-folded #talk-tab { display: flex; }
#talk-tab img { width: 19px; height: 19px; object-fit: contain; }
#talk-tab span {
  writing-mode: vertical-rl; font: 500 9px 'IBM Plex Mono', monospace;
  letter-spacing: 1.2px; text-transform: uppercase; color: var(--text-faint);
}
#talk-tab:hover { width: 40px; background: linear-gradient(180deg, #222836, #171c26); }
#talk-tab:hover span { color: var(--text-dim); }

/* the transcript holds the pins and the scrolling talk, in one column */
body.canvas-chat #chat-log { flex: 1 1 auto; min-height: 0; }

/* the agent's words are the content — they read white, not grey */
body.canvas-chat .msg.agent .bubble { color: var(--text); }
body.canvas-chat .msg.user .bubble { color: var(--text); opacity: 0.72; }
body.canvas-chat #chat-log { color: var(--text); }

/* No opacity games on live text — fading a scrolling transcript over a dotted
   canvas is what made it look glitchy. Windows are opaque and simply cover it,
   and that IS the dissolve. */
body.canvas-chat #chat-log { opacity: 1; transition: none; }
