/*
  Jupyter-like layout. This is the recording surface: capture drives it
  via #stage's dimensions (see README.md for the target resolution
  convention), so keep layout stable/deterministic -- no content that
  reflows based on load timing or network images.
*/

#app {
  display: flex;
  flex-direction: column;
  /* Must be a fixed height, not min-height: a flex column child with
     overflow-y:auto (#ob-stage-viewport below) only actually clips and
     scrolls if ITS flex container has a definite height to constrain
     it to. With min-height, #app just grows to fit its content instead
     -- flex:1 then has nothing finite to fill, so the "viewport" grows
     to match the whole notebook and overflow-y:auto never has anything
     to do. This is what silently broke scrolling/auto-follow entirely
     (not a zoom or CSS `overflow` value issue, despite how it first
     looked -- confirmed by measuring clientHeight far exceeding the
     actual window.innerHeight). */
  /* Resolution scaling (see main.js's applyResolutionScale) used to be
     a CSS `zoom` on <html>. Confirmed via a real Xvfb/Docker capture at
     1920x1080 (the default capture resolution) that `zoom` combined
     with a `transform` on a descendant (engine/zoom.js's camera moves,
     applied to #ob-zoom-stage) paints in the WRONG position under
     Chromium's software rasterizer (--disable-gpu, no real GPU in this
     environment) even though every JS-observable measurement
     (getBoundingClientRect, offsetLeft/Width) agreed the math was
     correct -- a paint-time bug invisible to any DOM-level check, only
     caught by inspecting actual captured video frames. `zoom` at 1280px
     width (a no-op, scale=1) never showed it, which is why this went
     undetected until a wider capture was actually inspected frame by
     frame. `transform: scale()` is the standards-track, universally
     well-tested mechanism for the exact same "uniform page scale-up"
     effect and doesn't have this failure mode. Sized via calc() so the
     scaled-up box exactly fills the real viewport: at scale S, this
     element's own (pre-transform) box is 1/S the real viewport, then
     the transform inflates it back to 100%. */
  width: calc(100vw / var(--ob-scale, 1));
  height: calc(100vh / var(--ob-scale, 1));
  transform: scale(var(--ob-scale, 1));
  transform-origin: top left;
}

/* ---------- Header ---------- */
/* Decorative chrome (see index.html's header comment) -- logo/filename
   are real, everything else (Run All / save / settings / more) is a
   static, non-functional visual only. Fixed 40px height (reference's
   toolbar-height token) -- part of the design-baseline layout budget
   applied at 1280px and scaled uniformly with everything else via
   #app's --ob-scale (see main.js's applyResolutionScale), not something
   that needs its own per-resolution logic. */

#ob-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 40px;
  padding: 0 16px;
  border-bottom: 1px solid var(--ob-border);
  background: var(--ob-bg-elevated);
  flex-shrink: 0;
}

#ob-header-left {
  display: flex;
  align-items: center;
  gap: 16px;
  min-width: 0;
}

/* Hidden by default everywhere this header renders -- both the static
   preview (preview.html) and the real capture player (index.html), so
   no rendered video or in-app preview shows it unless a future setting
   explicitly turns it back on. Same change as editor.css's identical
   rule for the .py/Short headers. */
#ob-logo {
  display: none;
  height: 20px;
  width: auto;
  flex-shrink: 0;
}

#ob-filename {
  font-family: var(--ob-font-sans);
  font-size: 13px;
  line-height: 18px;
  color: var(--ob-text-muted);
  padding: 2px 8px;
  border-radius: var(--ob-radius-sm);
  background: var(--ob-badge-bg);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#ob-header-right {
  display: flex;
  align-items: center;
  gap: 16px;
}

.ob-header-btn,
.ob-header-icon-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: none;
  background: none;
  padding: 0;
  font-family: var(--ob-font-sans);
}

.ob-header-btn svg,
.ob-header-icon-btn svg {
  fill: currentColor;
  flex-shrink: 0;
}

#ob-run-all {
  background: var(--ob-accent);
  color: var(--ob-bg);
  padding: 4px 12px;
  border-radius: var(--ob-radius-sm);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.02em;
}
#ob-run-all svg {
  width: 14px;
  height: 14px;
}

.ob-header-icon-btn {
  color: var(--ob-text-muted);
}
.ob-header-icon-btn svg {
  width: 20px;
  height: 20px;
}

#ob-header-divider {
  width: 1px;
  height: 20px;
  background: var(--ob-border);
}

/* ---------- Footer ---------- */
/* Fully static/decorative (see index.html) -- fixed 16px height,
   same design-baseline scaling as the header above. Left-aligned only
   now (kernel status + venv) -- the cursor-position/encoding readout on
   the right was removed as pure noise for a captured video no one is
   actually editing in. */

#ob-footer {
  display: flex;
  align-items: center;
  height: 16px;
  padding: 0 12px;
  background: var(--ob-accent);
  color: var(--ob-bg);
  font-family: var(--ob-font-mono);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.02em;
  flex-shrink: 0;
}

#ob-footer-left {
  display: flex;
  align-items: center;
  gap: 12px;
}

#ob-kernel-status {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-weight: 700;
}
#ob-kernel-status svg {
  width: 9px;
  height: 9px;
  fill: currentColor;
}

/* ---------- Stage / zoom ---------- */

#ob-stage-viewport {
  flex: 1;
  overflow-x: hidden;
  overflow-y: auto;
  /* Deliberately NOT scroll-behavior: smooth. render.js's
     _autoFollowActiveCell already does its own frame-by-frame analytic
     interpolation (a plain `scrollTop =` write computed fresh from
     state.t every frame, same pattern as the zoom transform) precisely
     to avoid depending on the browser's own compositor-driven smooth
     scroll, which turned out not to reliably progress under real-time
     capture. Per the CSSOM View spec, `scroll-behavior: smooth` also
     applies to *direct* scrollTop assignment, not just scrollTo() --
     so leaving it set here would make the browser try to animate
     toward every one of those ~60-times-a-second writes as its own
     target, fighting the manual interpolation instead of just applying
     it. This is exactly what silently broke auto-scroll the first time.
  */
  position: relative;
  background: var(--ob-bg);
}

/* Scrolling needs to actually work (see overflow-y: auto above) but a
   visible scrollbar in the recorded video would look wrong -- this is
   Chromium-only (our only target, via Playwright), so the vendor-
   prefixed rule is enough; no need for a cross-browser fallback. */
#ob-stage-viewport::-webkit-scrollbar {
  display: none;
}

#ob-zoom-stage {
  /* engine/zoom.js's translate/scale math assumes a top-left origin --
     don't change this without updating that math to match. */
  transform-origin: 0 0;
  will-change: transform;
  /* Without this, #ob-notebook's `margin: 32px auto` (below) collapses
     straight through this element (a plain block with no border/padding
     doesn't stop margin collapsing -- only a new block formatting
     context does), so the 32px ends up rendered ABOVE this element's own
     box instead of inside it. zoom.js's localRect() measures cell
     position via offsetTop walked up to this element, which stops
     counting once it reaches this element's box -- so with the margin
     collapsed out, that measurement silently loses 32px, producing a
     constant ~32px vertical centering error on every zoom target
     regardless of the target cell's size (confirmed empirically: the
     error didn't move as the actively-typed cell grew, ruling out a
     stale-measurement explanation). `display: flow-root` establishes a
     BFC -- stops the collapse -- without clipping (unlike `overflow`,
     it doesn't affect paint, only layout containment), which matters
     because zoomed content is *meant* to visually extend beyond this
     element's untransformed box. */
  display: flow-root;
}

/* Fake in-page cursor (see index.html for why: Xvfb has no real cursor
   pixmap to render). A direct #app child, NOT inside #ob-zoom-stage --
   its target positions come from getBoundingClientRect() (real on-screen
   coordinates, already accounting for the current zoom/scroll), so the
   element itself must live in that same unscaled coordinate space rather
   than inheriting the stage's transform on top. render.js's _applyCursor
   writes `transform: translate()` here every frame, same analytic
   pattern as the zoom stage and the scroll position. */
#ob-fake-cursor {
  position: fixed;
  top: 0;
  left: 0;
  width: 26px;
  height: 26px;
  /* The SVG's own drawn tip is near its top-left (roughly 4,2 in its
     28x28 viewBox) -- shift left/up so the glide/target math (which
     treats the cursor's position as its tip, the way a real pointer's
     hotspot works) lines up with where render.js positions it. */
  margin-left: -3px;
  margin-top: -2px;
  pointer-events: none;
  z-index: 50;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35));
  transition: opacity 0.15s ease;
}

#ob-fake-cursor[data-hidden="true"] {
  opacity: 0;
}

#ob-notebook {
  max-width: 900px;
  margin: 32px auto;
  padding: 0 24px;
  /* render.js's _autoFollowActiveCell scrolls the LAST active cell to
     the same "18% down from the top of the viewport" position as every
     other cell (see its targetTop math) -- but a fixed, small trailing
     margin only leaves that much real scrollable room below the final
     cell. Once the requested scroll position exceeds the page's actual
     max scrollTop, the browser just clamps to the max: the final cell
     ends up barely peeking in at the very bottom edge of the viewport
     instead of properly positioned, which reads as "never scrolled
     there" even though scrollTop did reach its ceiling. A trailing
     spacer sized to the viewport itself guarantees enough room to
     scroll the last cell into the same position as any other,
     regardless of capture resolution or how short that last cell is. */
  padding-bottom: 100vh;
}

/* ---------- Cells ---------- */

/* The whole notebook is laid out up front, like a real notebook the
   teacher already has open -- cells don't fade in as their turn comes
   up. Only the "currently being taught" highlight (.ob-cell--active)
   changes as playback moves through them. */
.ob-cell {
  display: grid;
  grid-template-columns: 64px 1fr;
  margin-bottom: 4px;
  /* Active-cell accent bar: border-left + padding-left always reserve
     the same 10px inset (2px border, always rendered even when
     transparent, + 8px padding), and margin-left cancels it out --
     content sits at the exact same position whether the cell is active
     or not, no layout shift when render.js toggles .ob-cell--active. */
  border-left: 2px solid transparent;
  padding-left: 8px;
  margin-left: -10px;
}

.ob-cell--active {
  border-left-color: var(--ob-accent);
}

.ob-cell--active .ob-cell-prompt {
  color: var(--ob-prompt);
}

.ob-cell-prompt {
  font-family: var(--ob-font-mono);
  font-size: 11px;
  line-height: 16px;
  letter-spacing: 0.02em;
  font-weight: 500;
  color: var(--ob-text-muted);
  padding: 12px 8px 12px 0;
  text-align: right;
  user-select: none;
  white-space: nowrap;
}

.ob-cell-body {
  min-width: 0;
}

/* Markdown cells */

.ob-markdown {
  padding: 8px 4px 8px 16px;
  font-size: 14px;
  line-height: 20px;
  /* Reference's paragraph text is the muted tone (on-surface-variant /
     gray-600), not the brighter heading color -- headings below
     explicitly override back to --ob-text. Without this, <p> silently
     inherited body's --ob-text (the heading-bright color) instead. */
  color: var(--ob-text-muted);
}

.ob-markdown h1,
.ob-markdown h2,
.ob-markdown h3,
.ob-markdown h4 {
  margin: 0.6em 0 0.4em;
  font-weight: 600;
  color: var(--ob-text);
}
.ob-markdown h1 { font-size: 24px; line-height: 32px; }
.ob-markdown h2 { font-size: 18px; line-height: 24px; }
.ob-markdown h3 { font-size: 16px; line-height: 22px; }
.ob-markdown h4 { font-size: 14px; line-height: 20px; }

.ob-markdown p {
  margin: 0.6em 0;
}

.ob-markdown code {
  font-family: var(--ob-font-mono);
  background: var(--ob-bg-inset);
  border-radius: 4px;
  padding: 0.15em 0.4em;
  font-size: 0.88em;
}

.ob-markdown pre {
  background: var(--ob-bg-inset);
  border-radius: var(--ob-radius);
  padding: 12px 14px;
  overflow-x: auto;
}
.ob-markdown pre code {
  background: none;
  padding: 0;
}

.ob-markdown blockquote {
  margin: 0.6em 0;
  padding: 2px 16px;
  border-left: 3px solid var(--ob-orange-end);
  color: var(--ob-text-muted);
}

.ob-markdown ul,
.ob-markdown ol {
  margin: 0.5em 0;
  padding-left: 1.4em;
}

.ob-markdown a {
  color: var(--ob-syntax-function);
}

.ob-markdown img {
  max-width: 100%;
  height: auto;
  vertical-align: middle;
}

.ob-markdown table {
  margin: 0.6em 0;
  border-collapse: collapse;
  width: 100%;
  font-size: 0.92em;
}

.ob-markdown th,
.ob-markdown td {
  padding: 6px 10px;
  border: 1px solid var(--ob-border);
  text-align: left;
}

.ob-markdown th {
  background: var(--ob-bg-inset);
  color: var(--ob-text);
  font-weight: 600;
}

.ob-markdown td {
  color: var(--ob-text-muted);
}

/* Code cells */

.ob-code-cell {
  border: 1px solid var(--ob-border);
  border-radius: var(--ob-radius);
  background: var(--ob-bg-elevated);
  overflow: hidden;
}

/* Active-cell glow (see .ob-cell--active above for the left accent bar)
   -- reuses the same timeline-driven .ob-cell--active toggle render.js
   already applies every frame from state.activeCellId; no new timing
   logic, this is styling only. */
.ob-cell--active .ob-code-cell {
  background: var(--ob-bg);
  border-color: var(--ob-accent-soft);
  /* Was a hardcoded rgba(255, 107, 0, 0.2) -- the orange brand accent --
     until the 8 VS Code-derived themes needed this glow to match each
     theme's own accent instead. --ob-accent-glow is the same accent
     color pre-mixed to ~20% opacity per theme (see theme.css), since a
     bare custom property can't have its alpha adjusted by a consumer
     rule. */
  box-shadow: 0 0 0 1px var(--ob-accent-glow);
}

.ob-code-block {
  font-family: var(--ob-font-mono);
  font-size: 14px;
  line-height: 22px;
  padding: 10px 16px;
  white-space: pre-wrap;
  word-break: break-word;
  tab-size: 4;
}

/* No visual divider between consecutive blocks within a code cell --
   removed on request; each block was previously separated by a
   1px dashed border-top, which read as visual clutter rather than
   useful structure once the blocks are typed out in sequence. */

.ob-cursor {
  display: inline-block;
  width: 2px;
  height: 1.1em;
  background: var(--ob-prompt);
  vertical-align: text-bottom;
  /* Opacity (blink) is driven from render.js as a function of t, not a
     CSS animation -- a CSS animation is wall-clock-driven, which would
     make the cursor blink randomly/inconsistently under frame-by-frame
     capture instead of on a predictable, reproducible cadence. */
}

.ob-mistake {
  background: var(--ob-mistake-bg);
  color: var(--ob-mistake-text);
  border-radius: 2px;
}

/* Syntax highlighting */
.tok-keyword { color: var(--ob-syntax-keyword); }
.tok-string { color: var(--ob-syntax-string); }
.tok-comment { color: var(--ob-syntax-comment); font-style: italic; }
.tok-function { color: var(--ob-syntax-function); }
.tok-number { color: var(--ob-syntax-number); }
.tok-builtin { color: var(--ob-syntax-builtin); }
.tok-operator { color: var(--ob-syntax-operator); }
.tok-variable { color: var(--ob-syntax-variable); }
.tok-type { color: var(--ob-syntax-type); }

/* Output */

.ob-output {
  border-top: 1px solid var(--ob-border);
  background: var(--ob-bg-inset);
  padding: 10px 16px;
  font-family: var(--ob-font-mono);
  font-size: 12px;
  line-height: 18px;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease, padding 0.3s ease;
}

/* render.js wraps stream/error/text-plain output in a real <pre>
   element (out.text/out.traceback), never inline text -- a browser's
   own default stylesheet sets white-space:pre directly on <pre>, which
   wins over the white-space:pre-wrap inherited from .ob-output above
   (inheritance only applies where nothing else targets the element
   itself, and this is exactly such a case). That's what let long,
   space-free output lines (a long path, a stack trace, a JSON blob)
   overflow .ob-output's own width instead of wrapping -- confirmed live.
   Setting the same wrap behavior directly on the <pre> closes that gap;
   margin/font are reset since a bare <pre> otherwise carries its own
   default vertical margin and (in some UAs) its own monospace stack,
   both of which .ob-output already accounts for itself. */
.ob-output pre {
  margin: 0;
  font: inherit;
  white-space: pre-wrap;
  word-break: break-word;
}

.ob-output--visible {
  max-height: 600px;
  padding: 10px 16px;
}

.ob-output img {
  max-width: 100%;
  border-radius: 4px;
}

.ob-output-error {
  color: var(--ob-mistake-text);
}

/* DataFrame / rich text-html output tables -- mirrors Jupyter's own
   default pandas.DataFrame._repr_html_ look. sanitizeHtml.js strips
   pandas' own inline <style scoped> block on purpose (untrusted CSS
   text, not worth the injection surface), so these rules are what
   actually make the table look right instead of like a bare HTML
   table. Scoped under .ob-output-table-wrap so a notebook's own <table>
   markup (unrelated to DataFrames) sanitized elsewhere never picks
   these up by accident.

   Values/column names are never truncated, shrunk illegibly, or split
   across separate tables. render.js's initTableFit() measures the
   table's real natural (unscaled) width once, then applies a plain
   `transform: scale()` -- never a smaller font-size -- to bring it
   down to whatever width .ob-output actually has; a ResizeObserver
   keeps that correct if that width changes. Nothing more happens while
   the teacher narrates this output -- the camera zoom (engine/zoom.js's
   "output:<cellId>" target) already brings it in close enough to read,
   so the table doesn't also scale itself up on top of that. */
.ob-output-table-wrap table {
  border-collapse: collapse;
  table-layout: auto;
  font-size: 14px;
  margin: 2px 0;
  white-space: nowrap;
  transform-origin: top left;
}

.ob-output-table-wrap th,
.ob-output-table-wrap td {
  padding: 0.35em 0.75em;
  border: 1px solid var(--ob-border);
  text-align: right;
}

.ob-output-table-wrap th {
  background: var(--ob-bg-elevated);
  font-weight: 600;
}

.ob-output-table-wrap tbody tr:nth-child(even) {
  background: var(--ob-bg-elevated);
}

/* Responsive */

@media (max-width: 720px) {
  #ob-notebook {
    padding: 0 12px;
  }
  .ob-cell {
    grid-template-columns: 44px 1fr;
  }
  .ob-cell-prompt {
    font-size: 0.68rem;
  }
  .ob-markdown {
    font-size: 0.92rem;
  }
}
