/**
 * style.css — Layout and component styles
 * Visual tokens imported from tokens.css
 */

@import url('tokens.css');

/* ---- Reset ---- */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* ---- Base ---- */
html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--size-body);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
}

/* ---- App Shell ---- */
#app {
  position: relative;
  width: var(--viewport-width);
  height: var(--viewport-height);
  overflow: hidden;
}

/* ---- Progress Bar ---- */
#progress-bar {
  position: fixed;
  top: var(--progress-top-margin);
  left: var(--slide-padding);
  right: var(--slide-padding);
  display: flex;
  gap: var(--progress-section-gap);
  z-index: 100;
  height: auto;
}

.progress-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  flex: 1;
  transition: flex var(--transition-normal);
  overflow: hidden;
}

.progress-section.collapsed {
  flex: 0.15;
}

.progress-section.active {
  flex: 1;
}

.progress-section-bars {
  display: flex;
  gap: var(--progress-gap);
  width: 100%;
}

.progress-section-title {
  font-family: var(--font-xxcond);
  font-size: var(--size-caption);
  color: var(--color-text-muted);
  opacity: 0;
  max-height: 0;
  overflow: hidden;
  transition: opacity var(--transition-normal), max-height var(--transition-normal);
  white-space: nowrap;
  text-overflow: ellipsis;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: var(--weight-medium);
  user-select: none;
  margin-top: 2px;
}

.progress-section.active .progress-section-title {
  opacity: 1;
  max-height: 2em;
  color: var(--color-text);
}

.progress-segment {
  flex: 1;
  height: var(--progress-height);
  background: var(--color-progress-bg);
  border-radius: 2px;
  overflow: hidden;
  transition: opacity var(--transition-fast);
}

.progress-section.collapsed .progress-segment {
  opacity: 0.5;
}

.progress-segment-fill {
  height: 100%;
  width: 0%;
  background: var(--color-progress-fill);
  border-radius: 2px;
  transition: width var(--transition-fast);
}

.progress-segment.complete .progress-segment-fill {
  width: 100%;
}

.progress-segment.current .progress-segment-fill {
  width: var(--segment-progress, 0%);
}

/* ---- Slide Area ---- */
#slide-area {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  /* Synthesis direction: allow vertical scroll when a slide's content is
     taller than the viewport so images can render at full scale without
     being clipped. Hide the scrollbar chrome — users scroll naturally. */
  display: block;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--slide-padding);
  padding-top: calc(var(--progress-top-margin) + var(--progress-height) + 1.5rem + var(--space-lg));
  /* Leave room at the bottom of every slide so the floating subtitle bar
     doesn't obstruct the final line of content. Value = subtitle height +
     its bottom offset + breathing room. */
  padding-bottom: calc(var(--subtitle-bottom, 2rem) + 12rem);
  scrollbar-width: none;
}

#slide-area::-webkit-scrollbar {
  display: none;
}

.slide {
  width: 100%;
  min-height: 100%;
  display: none;
  flex-direction: column;
}

.slide.active {
  display: flex;
}

/* ---- Layout: Title Cover (opening slide) ----
   Three stacked rows: header (tagline left, author right), image
   vertically centered, title below image. The header uses
   justify-content: space-between so tagline hugs the upper-left and
   author hugs the upper-right. */
.slide.layout-title-cover {
  justify-content: flex-start;
  align-items: stretch;
  gap: var(--space-lg);
  padding-top: var(--space-xl);
  padding-bottom: var(--space-xl);
  margin-bottom: calc(-1 * (var(--subtitle-bottom, 2rem) + 12rem));
}

.cover-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-xl);
  width: 100%;
  padding: 0 var(--space-md);
  position: relative;
  z-index: 10;
}

.cover-image {
  flex: 1;
  min-height: 0;
  max-height: 60vh;
  max-width: 100%;
  object-fit: contain;
  align-self: center;
  margin-top: -14vh;
  margin-bottom: 0;
  position: relative;
  z-index: 1;
}

.slide-title.cover-title {
  font-family: var(--font-wide);
  font-size: clamp(2rem, 3.4vw, 3.8rem);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
  text-align: center;
  width: 100%;
  padding: 0 var(--space-md);
}

.cover-author {
  font-family: var(--font-wide);
  font-size: clamp(0.75rem, 1.5vw, var(--size-body));
  color: var(--color-text);
  line-height: 1.5;
  text-align: left;
  max-width: 40%;
}

.cover-description {
  font-family: var(--font-wide);
  font-size: clamp(0.75rem, 1.5vw, var(--size-body));
  color: var(--color-text-muted);
  text-align: right;
  max-width: 32%;
  line-height: 1.5;
}

/* ---- Layout: Title ---- */
.slide.layout-title {
  justify-content: flex-end;
  gap: var(--space-md);
  padding-bottom: var(--space-xl);
}

.slide.layout-title .slide-title {
  font-family: var(--font-wide);
  font-size: var(--size-title);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}

.slide.layout-title .slide-heading {
  font-family: var(--font-wide);
  font-size: var(--size-body);
  font-weight: var(--weight-regular);
  color: var(--color-text-muted);
  line-height: var(--leading-normal);
}

.slide.layout-title .slide-bullets {
  gap: var(--space-lg);
}

.slide.layout-title .slide-bullet {
  font-family: var(--font-condensed);
  font-size: clamp(1.25rem, 2vw, 2.5rem);
  line-height: var(--leading-tight);
  border-left: none;
  padding-left: 0;
}

/* ---- Layout: Bullets + Images ---- */
.slide.layout-bullets-left-images-right {
  flex-direction: row;
  gap: var(--space-xl);
}

.slide-content-area {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: var(--space-lg);
}

.slide-image-area {
  flex: 1;
  position: relative;
}

/* Right-column video slot. Used for videos annotated `pos:right` in the
   per-video metadata — e.g. a product demo that lives where images would
   normally render on a bullets-left-images-right slide. */
.video-right-col {
  width: 100%;
  max-height: 45vh;
  object-fit: contain;
  border-radius: var(--image-radius);
  box-shadow: var(--image-shadow);
  display: block;
}

.right-video-wrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  width: 100%;
}

/* When a right-column video AND images both live in the right column, the
   image reveal area takes a fixed share below the video. When there's no
   right video, images render directly into .slide-image-area so their
   percent-based positioning has the full column as a reference frame. */
.slide-image-area:has(.right-video-wrap) .image-reveal-area {
  position: relative;
  margin-top: var(--space-md);
  flex: 1;
  min-height: 40vh;
}

/* Behind video: absolute-positioned in the slide, sitting under the text
   layer of the left column. Starts hidden; fades in when `.revealed` is
   added (by the TTS end-listener in applyClick). */
.video-behind {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 48%;
  max-height: 55%;
  object-fit: cover;
  border-radius: var(--image-radius);
  box-shadow: var(--image-shadow);
  z-index: 0;
  opacity: 0;
  transition: opacity 600ms ease;
  pointer-events: none;
}

.video-behind.revealed {
  opacity: 0.85;
  pointer-events: auto;
}

/* Ensure bullets / title in the left column float above the behind video. */
.slide.layout-bullets-left-images-right .slide-content-area {
  position: relative;
  z-index: 1;
}

/* ---- Layout: Two Column Compare ---- */
.slide.layout-two-column-compare {
  flex-direction: column;
  gap: var(--space-md);
}

.slide.layout-two-column-compare .slide-title {
  font-size: clamp(1.4rem, 2.2vw, 2.4rem);
  flex-shrink: 0;
}

.slide.layout-two-column-compare>div {
  flex: 1;
  display: flex;
  gap: var(--space-lg);
  align-items: stretch;
  min-height: 0;
}

.compare-column {
  flex: 1;
  padding: var(--space-lg) var(--space-xl);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 0.5rem;
  text-align: left;
  font-family: var(--font-condensed);
  font-size: var(--size-bullet);
  line-height: 1.7;
  overflow-wrap: break-word;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.compare-row {
  flex: 1;
  display: flex;
  gap: var(--space-lg);
  align-items: stretch;
  min-height: 0;
}

.compare-header {
  font-family: var(--font-condensed);
  font-size: clamp(1.6rem, 2.5vw, 3rem);
  font-weight: var(--weight-semibold);
  margin-bottom: var(--space-lg);
  color: var(--color-accent);
  line-height: 1.2;
}

.compare-items {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.compare-item {
  font-family: var(--font-condensed);
  font-size: var(--size-bullet);
  line-height: 1.5;
  padding-left: var(--space-md);
  border-left: 2px solid rgba(255, 255, 255, 0.2);
  color: var(--color-text);
}

.compare-column strong {
  display: block;
  font-family: var(--font-condensed);
  font-size: clamp(1.4rem, 2.2vw, 2.8rem);
  margin-bottom: var(--space-md);
  color: var(--color-accent);
}

/* Cascade reveal for compare columns */
.compare-reveal {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--transition-reveal), transform var(--transition-reveal);
}

.compare-reveal.revealed {
  opacity: 1;
  transform: translateY(0);
}

/* ---- Layout: Diagram ---- */
.slide.layout-diagram {
  flex-direction: column;
  gap: var(--space-lg);
}

.slide.layout-diagram .slide-title {
  font-family: var(--font-condensed);
  font-size: var(--size-heading);
  font-weight: var(--weight-regular);
}

.slide.layout-diagram .slide-heading {
  font-family: var(--font-condensed);
  font-size: var(--size-subtitle);
  color: var(--color-text-muted);
}

/* ---- Layout: Workshop ---- */
.slide.layout-workshop {
  justify-content: center;
  align-items: center;
  gap: var(--space-xl);
  text-align: center;
  background: var(--color-workshop-bg);
}

.workshop-link {
  display: inline-block;
  padding: var(--space-md) var(--space-xl);
  background: var(--color-workshop-accent);
  color: var(--color-bg);
  text-decoration: none;
  border-radius: var(--image-radius);
  font-family: var(--font-condensed);
  font-weight: var(--weight-semibold);
  font-size: var(--size-bullet);
  transition: opacity var(--transition-fast);
}

.workshop-link:hover {
  opacity: 0.85;
}

/* ---- Layout: Video ---- */
.slide.layout-video {
  justify-content: center;
  align-items: center;
  gap: var(--space-lg);
}

.video-player {
  max-width: 90%;
  max-height: 70vh;
  border-radius: 20px;
  object-fit: contain;
  /* Subtle off-white outer glow so the video pops off the dark bg. */
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.04),
    0 8px 40px rgba(255, 255, 255, 0.08),
    0 0 80px rgba(235, 227, 196, 0.06);
}

/* Synthesis direction: corner-radius + glow applies to *every* video
   element, not just .video-player (which is only set on a subset of
   render paths). Controls are hidden by default and only surface when
   the element carries .show-controls (added by app.js if autoplay
   fails or the user toggles to view them). */
#slide-area video {
  border-radius: 20px;
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.04),
    0 8px 40px rgba(255, 255, 255, 0.08),
    0 0 80px rgba(235, 227, 196, 0.06);
}

/* Default-placement videos (below bullets in bullets-left-images-right).
   Size to the video's intrinsic aspect ratio so a portrait / square
   clip doesn't render as a wide letterbox with black bars. max-width
   keeps it from overflowing the content column; max-height keeps it
   from pushing past the viewport. No hard width, no bg fill. */
#slide-area video.video-default {
  width: auto;
  max-width: 100%;
  max-height: 60vh;
  object-fit: contain;
  align-self: center;
}

/* The old explicit black fill is dropped — the frame now hugs the video's
   natural aspect. Restore it on clips where the encoder's matte is
   meant to be invisible by adding class="video-letterbox" via markdown
   metadata (not wired yet; easy add when needed). */

/* Hide the browser's default controls shelf unless we've explicitly
   opted this element into showing them. Uses ::-webkit-media-controls
   to bypass Safari/Chrome showing them when controls=true. */
#slide-area video::-webkit-media-controls {
  display: none !important;
  opacity: 0;
}

#slide-area video.show-controls::-webkit-media-controls {
  display: flex !important;
  opacity: 1;
}

/* ---- Layout: Bibliography ---- */
.slide.layout-bibliography {
  overflow-y: auto;
  gap: var(--space-md);
}

/* ---- Layout: Bibliography Two-Column ---- */
.slide.layout-bibliography-two-column {
  overflow-y: auto;
  gap: var(--space-md);
}

.bibliography-columns {
  display: flex;
  gap: var(--space-xl);
  flex: 1;
  overflow-y: auto;
}

.bibliography-column {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.bibliography-column-title {
  font-size: var(--size-body);
  font-weight: var(--weight-bold);
  color: var(--color-accent);
  margin-bottom: var(--space-sm);
  padding-bottom: var(--space-xs);
  border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}

/* Bibliography bullets use the standard slide-bullet `+` marker style
   but smaller (citation density is high) and in Graphik Condensed
   Medium so they read consistently with the rest of the deck. */
.bibliography-bullet-list {
  gap: 0.3rem;
  list-style: none;
  padding: 0;
  margin: 0;
}

.bibliography-item.slide-bullet {
  font-family: var(--font-condensed);
  font-weight: 500;
  font-size: clamp(0.8rem, 0.95vw, 1rem);
  line-height: 1.45;
  color: var(--color-text);
  letter-spacing: 0;
  /* Already revealed — skip the translateY opacity animation. */
  opacity: 1;
  transform: none;
}

.bibliography-item.slide-bullet::before {
  font-size: 1em;
}

.bibliography-item em,
.bibliography-item strong {
  color: var(--color-accent);
}

/* ---- Diagram System ---- */
.diagram-container {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-sm) 0;
  min-height: 0;
}

.diagram-side-by-side {
  gap: var(--space-lg);
}

.diagram-wrapper {
  flex: 1;
  max-height: 70vh;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
}

.diagram-wrapper svg {
  width: 125%;
  height: auto;
  max-height: 75vh;
}

/* ---- Layout: Iframe ---- */
.slide.layout-iframe {
  flex-direction: column;
  gap: var(--space-sm);
}

.slide.layout-iframe .slide-title {
  flex-shrink: 0;
  font-size: clamp(1.4rem, 2.2vw, 2.4rem);
}

.slide.layout-iframe .slide-bullets {
  flex-shrink: 0;
}

.iframe-container {
  flex: 1;
  width: 100%;
  min-height: 0;
  border-radius: var(--image-radius);
  overflow: hidden;
}

.iframe-container iframe {
  width: 100%;
  height: 100%;
  border: none;
}

/* ---- Audio Player ---- */
.slide-audio-player {
  position: absolute;
  bottom: calc(var(--subtitle-bottom) + 80px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 85;
}

.slide-audio-player audio {
  height: 32px;
  border-radius: 16px;
  opacity: 0.7;
  transition: opacity var(--transition-fast);
}

.slide-audio-player audio:hover {
  opacity: 1;
}

/* ---- Layout: Fullscreen ---- */
.slide.layout-fullscreen {
  justify-content: center;
  align-items: center;
  padding: var(--space-md);
  gap: var(--space-sm);
}

.slide.layout-fullscreen .slide-title {
  flex-shrink: 0;
  font-size: clamp(1.2rem, 1.8vw, 2rem);
  color: var(--color-text-muted);
  text-align: center;
}

.slide.layout-fullscreen .slide-image-area {
  flex: 1;
  min-height: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.slide.layout-fullscreen .image-container {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-lg);
}

.slide.layout-fullscreen .image-frame {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
}

.slide.layout-fullscreen .image-frame img {
  max-height: 75vh;
  max-width: 100%;
  object-fit: contain;
}

/* ---- Bullets ---- */
.slide-bullets {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.slide-bullet {
  font-family: var(--font-condensed);
  font-size: var(--size-bullet);
  line-height: 1.6;
  position: relative;
  border-left: none;
  /* Synthesis: `+` marker is a flex item so it can sit vertically
     centred on the x-height of the adjacent bullet text rather than
     rising above or dropping below the visual centre. */
  padding-left: 0;
  display: flex;
  align-items: center;
  gap: 0.55em;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--transition-reveal), transform var(--transition-reveal);
}

/* Oversized `+` marker. Colour set per bullet via --plus-color (app.js
   picks a palette slot from a deterministic hash of slide id + bullet
   index). Smaller ascent height + line-height 1 keeps the plus optically
   centred on the bullet text's x-height instead of looming above the caps. */
.slide-bullet::before {
  content: '+';
  flex: 0 0 auto;
  font-family: var(--font-wide);
  font-weight: 600;
  font-size: 1.25em;
  line-height: 1;
  /* Nudge down slightly: the `+` glyph's geometric centre sits above
     the x-height of neighbouring lowercase letters in most fonts. */
  transform: translateY(-0.02em);
  color: var(--plus-color, var(--palette-7, rgba(255, 255, 255, 0.55)));
}

/* Centered-list slides (e.g. 7_35 Hollywood's 8 Rules) use their own
   numbered text — no leading `+` marker. */
.slide.layout-centered-list .slide-bullet {
  display: block;
  padding-left: 0;
}

.slide.layout-centered-list .slide-bullet::before {
  content: none;
}

.slide-bullet.revealed {
  opacity: 1;
  transform: translateY(0);
}

/* ---- Slide Title / Headings ---- */
.slide-title {
  font-family: var(--font-condensed);
  font-size: var(--size-heading);
  font-weight: var(--weight-regular);
  line-height: var(--leading-tight);
}

.slide-heading {
  font-family: var(--font-condensed);
  font-size: var(--size-subtitle);
  color: var(--color-text-muted);
  line-height: 1.6;
}

/* ---- Image System ---- */
.image-container {
  position: relative;
  width: 100%;
  height: 100%;
}

/* Grid layouts (default / columns-N) — flow-positioned, scale-only transform. */
.image-frame {
  opacity: 0;
  transform: scale(var(--reveal-from, 0.92));
  transition: opacity var(--transition-reveal), transform var(--transition-reveal);
}

.image-frame.revealed {
  opacity: 1;
  transform: scale(var(--reveal-to, 1));
}

/* Absolute-positioned layouts (clockwise, pos-custom) compose
   translate(-50%, -50%) + rotate + scale in a single transform so the
   image is center-anchored at its (left, top) coordinate and still
   animates via the reveal CSS vars. */
.layout-clockwise .image-frame,
.layout-pos-custom .image-frame {
  position: absolute;
  transform: translate(-50%, -50%) rotate(var(--rotate, 0deg)) scale(var(--reveal-from, 0.92));
}

.layout-clockwise .image-frame.revealed,
.layout-pos-custom .image-frame.revealed {
  transform: translate(-50%, -50%) rotate(var(--rotate, 0deg)) scale(var(--reveal-to, 1));
}

.image-frame img {
  display: block;
  max-width: 100%;
  border-radius: var(--image-radius);
  box-shadow: var(--image-shadow);
  /* Synthesis direction: halftone WEBPs are opaque 64-colour dithers.
     No blend mode, no alpha trick — the image sits on the dark canvas
     as-is. Keep the original subtle drop-shadow so photos have depth. */
}

/* Fig. caption plate — small mono beneath the image, fixed rem so size
   stays consistent everywhere regardless of parent font-size. The frame
   reserves ~2.75rem bottom padding to hold the caption + its surrounding
   padding so stacked images never have their captions collide. */
.image-frame {
  position: relative;
  padding-bottom: 2.75rem;
}

.image-frame .fig-caption {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 0.35rem 0.55rem;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  line-height: 1.3;
  color: var(--color-text);
  letter-spacing: 0.02em;
  text-align: left;
  opacity: 0;
  transition: opacity var(--transition-reveal);
  pointer-events: none;
  /* Glass-blur plate matching the subtitle bar so captions read
     legibly over any image and cohere visually with the subtitle UI. */
  background: rgba(30, 30, 30, 0.3);
  -webkit-backdrop-filter: blur(18px) saturate(1.25);
  backdrop-filter: blur(18px) saturate(1.25);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 0.35rem;
}

.image-frame.revealed .fig-caption {
  opacity: 0.9;
}

/* Secondary credit line appended beneath the caption title (e.g.
   "Fig. 2. Meredith Whittaker" then "Martin Kraft · CC BY-SA 4.0"). */
.image-frame .fig-caption .fig-credit,
.video-frame .fig-caption .fig-credit {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.68rem;
  color: var(--color-text-muted);
  letter-spacing: 0.03em;
}

/* Video caption frame — mirrors .image-frame so videos carrying
   caption:/credit: metadata get the same glass-blur plate beneath them. */
.video-frame {
  position: relative;
  padding-bottom: 2.75rem;
  display: inline-block;
  /* Inherit the video's layout role: video-frame lives wherever the
     bare <video> would have lived, so auto-center + max-height still
     come from the parent's flex rules. */
}

.video-frame video {
  display: block;
  max-width: 100%;
}

.video-frame .fig-caption {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 0.35rem 0.55rem;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  line-height: 1.3;
  color: var(--color-text);
  letter-spacing: 0.02em;
  text-align: left;
  opacity: 0.9;
  pointer-events: none;
  background: rgba(30, 30, 30, 0.3);
  -webkit-backdrop-filter: blur(18px) saturate(1.25);
  backdrop-filter: blur(18px) saturate(1.25);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 0.35rem;
}

/* Viewport-level grain. Lives above slide content at low opacity so the
   whole page reads as textured paper. */
#texture-grain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 500;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 0.05 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  background-repeat: repeat;
  mix-blend-mode: screen;
  opacity: 0.35;
}

/* Rough-line SVG filter on inline diagrams — strong (scale 10) so strokes
   read as hand-drawn. Scoped to geometry only; text and images are left
   crisp so labels and logos stay legible. */
.diagram-wrapper svg path,
.diagram-wrapper svg line,
.diagram-wrapper svg circle,
.diagram-wrapper svg rect,
.diagram-wrapper svg ellipse,
.diagram-wrapper svg polygon,
.diagram-wrapper svg polyline {
  filter: url(#rough);
}

/* Axis reference lines stay straight — they anchor the chart geometry
   and shouldn't wobble. Class added in the SVG source to opt out. */
.diagram-wrapper svg .axis-line {
  filter: none;
}

/* Also roughen SVG images rendered as <img> inside .image-frame — lets
   slides 2_11-14 apply the same hand-drawn distortion to the tetrad
   SVGs they load via `![alt](path.svg)` in lecture.md. The filter is
   applied to the whole image so the edges displace organically. */
.image-frame img[src$=".svg"] {
  filter: url(#rough);
}

/* ---- Layout: contact-cta (closing slide) ----
   Centered composition: title, headline, bulleted CTAs, and a row of
   action buttons for tips, workshop-booking, email, and site. The
   primary CTA (tips) gets a warm palette fill; secondary (workshop)
   is outlined; email + site render as quiet underlined links. */
.slide.layout-contact-cta {
  justify-content: center;
  align-items: center;
  text-align: center;
  gap: var(--space-lg);
  padding: var(--slide-padding);
  position: relative;
  overflow: hidden;
}

.slide.layout-contact-cta .slide-title {
  font-size: clamp(1.8rem, 3.2vw, 3.4rem);
  font-family: var(--font-wide);
  font-weight: 600;
  line-height: 1.1;
}

.slide.layout-contact-cta .contact-cta-heading {
  font-size: clamp(1.1rem, 1.5vw, 1.6rem);
  color: var(--color-text-muted);
  text-align: center;
  font-weight: 400;
}

.slide.layout-contact-cta .slide-bullets {
  align-items: flex-start;
  gap: var(--space-sm);
  max-width: 780px;
  margin: 0 auto;
}

.slide.layout-contact-cta .slide-bullet {
  text-align: left;
  opacity: 1;
  transform: none;
  font-family: var(--font-condensed);
}

.contact-cta-actions {
  display: flex;
  gap: var(--space-md);
  flex-wrap: wrap;
  justify-content: center;
  margin-top: var(--space-lg);
  max-width: 900px;
}

.contact-cta-action {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.9rem 1.5rem;
  border-radius: 0.4rem;
  text-decoration: none;
  color: var(--color-text);
  font-family: var(--font-condensed);
  transition: transform var(--transition-fast), background var(--transition-fast), border-color var(--transition-fast);
  min-width: 180px;
}

.contact-cta-action .cta-label {
  font-size: clamp(1rem, 1.3vw, 1.4rem);
  font-weight: 600;
  letter-spacing: -0.005em;
}

.contact-cta-action .cta-sub {
  font-size: 0.78rem;
  color: var(--color-text-muted);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.contact-cta-action:hover {
  transform: translateY(-2px);
}

.contact-cta-action.cta-primary {
  background: var(--palette-4, #cc7e52);
  color: #111;
}

.contact-cta-action.cta-primary .cta-sub {
  color: rgba(0, 0, 0, 0.55);
}

.contact-cta-action.cta-primary:hover {
  background: var(--palette-8, #edb56e);
}

.contact-cta-action.cta-secondary {
  border: 1px solid var(--palette-4, #cc7e52);
  color: var(--palette-4, #cc7e52);
}

.contact-cta-action.cta-secondary:hover {
  background: rgba(204, 126, 82, 0.1);
}

.contact-cta-action.cta-quiet {
  padding-left: 0;
  padding-right: 0;
  min-width: 0;
}

.contact-cta-action.cta-quiet .cta-label {
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
  padding-bottom: 2px;
}

.contact-cta-action.cta-quiet:hover .cta-label {
  border-bottom-color: var(--palette-4, #cc7e52);
  color: var(--palette-4, #cc7e52);
}

/* ---- Layout: stack-right (7_33 Where Do You Stand) ----
   Bullets + title in a left column (same flex:1 proportions as
   bullets-left-images-right), a vertically-stacked mix of images and
   videos on the right. Each item spans the full column width; the reveal
   engine still targets .image-frame nodes inside .image-container. */
.slide.layout-stack-right {
  flex-direction: row;
  gap: var(--space-xl);
}

.slide.layout-stack-right .slide-content-area {
  flex: 1;
}

.slide.layout-stack-right .slide-image-area.stack-column {
  flex: 1;
}

.image-container.stack-flow {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: var(--space-md);
  align-items: flex-start;
  align-content: flex-start;
  justify-content: flex-start;
  position: static;
  width: 100%;
  height: 100%;
}

.image-container.stack-flow .image-frame {
  position: relative;
  width: 100%;
  flex: 0 0 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}

.image-container.stack-flow .image-frame img {
  width: 100%;
  height: auto;
  object-fit: contain;
  object-position: top left;
}

/* Videos in the right-side stack size to their intrinsic aspect.
   width:auto + object-fit:contain means a 16:9 clip gets 16:9, a square
   clip stays square — no forced 100% width that would letterbox. */
.image-container.stack-flow .video-stack {
  flex: 0 0 calc(50% - var(--space-md) / 2);
  width: calc(50% - var(--space-md) / 2);
  height: auto;
  max-height: 30vh;
  object-fit: contain;
  background: transparent;
  align-self: flex-start;
}

/* stack-right: image(s) broken out of the right stack into the LEFT column
   below the bullets. Used for e.g. 7_33 "Below the Machine" where the
   closing image reads as a visual coda tied to the text, not another
   entry in the vertical stack. */
.slide.layout-stack-right .slide-content-area .stack-left-below {
  margin-top: auto;
  padding-top: var(--space-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.slide.layout-stack-right .stack-left-below .image-frame {
  position: relative;
  width: 100%;
}

.slide.layout-stack-right .stack-left-below .image-frame img {
  width: 100%;
  height: auto;
  max-height: 35vh;
  object-fit: contain;
}

/* Ensure the content-area fills the left column's vertical space so the
   left-below image can sit at the bottom (margin-top: auto above). */
.slide.layout-stack-right .slide-content-area {
  display: flex;
  flex-direction: column;
}

/* Inline hyperlinks rendered inside slide rawContent or subtitle lines.
   Warm palette accent underline that matches the halftone tint without
   shouting. */
.inline-link {
  color: inherit;
  text-decoration-color: var(--palette-4, #cc7e52);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color var(--transition-fast);
}

.inline-link:hover,
.inline-link:focus-visible {
  color: var(--palette-4, #cc7e52);
}

/* ---- Layout: Section Opener (synthesis) ----
   Type-as-image composition on a 12×12 grid. The grid is locked to
   100vh using negative margins that cancel the #slide-area's top
   progress-bar padding and bottom subtitle-bar padding, so every
   grid cell sits inside the viewport and no scrolling is needed. */
/* Systematic approach: the section-opener is pinned to the viewport
   (position: fixed, 100vw × 100vh) so the 12×12 grid measures against
   the viewport directly, not against the #slide-area's padded content
   box. This means:
   1. Every grid cell — including outer rows 1-2 / 11-12 — sits inside
      the visible viewport, so `justify-self: end` at col 11 lands on
      the visible right edge, never past it.
   2. Text zones can use the full 10×10 inner area (rows 2-11, cols
      2-11) without the outermost cells being chopped off by slide-
      area padding.
   3. Because the grid is always 100vw × 100vh, font sizing via vw
      units stays proportional: a word sized to clamp(1.8rem, 3.3vw)
      at 4 cols of 12 = 33vw of grid width, never exceeds its cell.
   z-index stays below the progress bar (100), subtitle bar (90), and
   controls so navigation chrome remains interactive. */
.slide.layout-section-opener {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100vw;
  height: 100vh;
  padding: 0;
  margin: 0;
  overflow: hidden;
  z-index: 5;
  display: block;
}

/* Hidden state — the .slide base rule uses display:none when not
   active; re-apply that explicitly so inactive section-openers don't
   cover each other. */
.slide.layout-section-opener:not(.active) {
  display: none;
}

.section-opener-wrap {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: repeat(12, 1fr);
  grid-template-columns: repeat(12, 1fr);
  padding: 0;
  gap: 0;
}

/* Low-opacity xerox-noise backdrop. */
.section-opener-wrap::before {
  content: '';
  position: absolute;
  inset: -5%;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='400' height='400'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 0.07 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  background-size: 400px 400px;
  background-repeat: repeat;
  opacity: 0.5;
  mix-blend-mode: screen;
  pointer-events: none;
  z-index: 0;
}

/* Each word / number is a grid item. Data attrs pick zone + font + size.
   text-shadow at (0,0) with a modest blur + semi-opaque black produces
   a crisp halo that keeps titles legible against animated dither
   backgrounds of any local lightness (bone/cream/blue/etc.). */
.section-opener-num,
.section-opener-word {
  color: var(--color-text);
  line-height: 0.92;
  letter-spacing: -0.01em;
  position: relative;
  z-index: 1;
  text-shadow: 0 0 6px rgba(0, 0, 0, 0.85);
}

/* Position zones pull the word toward one of the 9 conceptual zones of
   a 3×3 composition. Each zone spans 4 cols × 3-4 rows, all inside the
   inner 10×10 of the 12-col grid so text always stays within the slide
   bounds but never hits the very edge. Long words like "LANDSCAPE" and
   "McLuhan's" get a 4-col span (33% of slide width) so they fit at the
   clamp()-ed font size without clipping. */
.section-opener-wrap [data-pos='tl'] {
  grid-row: 2 / span 4;
  grid-column: 2 / span 4;
  align-self: start;
  justify-self: start;
  text-align: left;
}

.section-opener-wrap [data-pos='tc'] {
  grid-row: 2 / span 4;
  grid-column: 5 / span 4;
  align-self: start;
  justify-self: center;
  text-align: center;
}

.section-opener-wrap [data-pos='tr'] {
  grid-row: 2 / span 4;
  grid-column: 8 / span 4;
  align-self: start;
  justify-self: end;
  text-align: right;
}

.section-opener-wrap [data-pos='ml'] {
  grid-row: 5 / span 4;
  grid-column: 2 / span 4;
  align-self: center;
  justify-self: start;
  text-align: left;
}

.section-opener-wrap [data-pos='mc'] {
  grid-row: 5 / span 4;
  grid-column: 5 / span 4;
  align-self: center;
  justify-self: center;
  text-align: center;
}

.section-opener-wrap [data-pos='mr'] {
  grid-row: 5 / span 4;
  grid-column: 8 / span 4;
  align-self: center;
  justify-self: end;
  text-align: right;
}

.section-opener-wrap [data-pos='bl'] {
  grid-row: 8 / span 4;
  grid-column: 2 / span 4;
  align-self: end;
  justify-self: start;
  text-align: left;
}

.section-opener-wrap [data-pos='bc'] {
  grid-row: 8 / span 4;
  grid-column: 5 / span 4;
  align-self: end;
  justify-self: center;
  text-align: center;
}

.section-opener-wrap [data-pos='br'] {
  grid-row: 8 / span 4;
  grid-column: 8 / span 4;
  align-self: end;
  justify-self: end;
  text-align: right;
}

/* Composite section-opener blot: ONE element spanning the whole wrap,
   holding all N triangle centers in a single SVG. No per-cell grid
   placement — the SVG viewBox covers the full 100vw × 100vh and the
   blots are positioned in SVG units within that viewBox. */
.section-opener-wrap .ink-blot.composite {
  position: absolute;
  inset: 0;
  grid-column: 1 / -1;
  grid-row: 1 / -1;
  width: 100%;
  height: 100%;
  opacity: 0.95;
  z-index: 0;
}

/* Face: Graphik family. `wide` is the default body voice; `cond` /
   `xxcond` are for condensed display impact. `sb` = Semibold. */
.section-opener-num[data-face='wide-r'],
.section-opener-word[data-face='wide-r'] {
  font-family: var(--font-wide);
  font-weight: 400;
}

.section-opener-num[data-face='wide-sb'],
.section-opener-word[data-face='wide-sb'] {
  font-family: var(--font-wide);
  font-weight: 600;
}

.section-opener-num[data-face='cond-r'],
.section-opener-word[data-face='cond-r'] {
  font-family: var(--font-condensed);
  font-weight: 400;
}

.section-opener-num[data-face='xxcond-sb'],
.section-opener-word[data-face='xxcond-sb'] {
  font-family: var(--font-xxcond);
  font-weight: 500;
}

/* Size steps — clamps sized so each word fits inside its 4-col grid
   span (≈33% of slide width) without overflow at any character count.
   `sizeFor()` in app.js picks a step from the title's longest word.
   Base values are the word sizes (×1.25 over the original "small"
   typography to give titles more presence against the animated
   backgrounds). Numbers then override these with an additional 1.2×
   (for 1.5× net over the original) below. */
[data-size='sm'] {
  font-size: clamp(1.06rem, 1.75vw, 2rem);
}

[data-size='smd'] {
  font-size: clamp(1.75rem, 3.25vw, 3.75rem);
}

[data-size='md'] {
  font-size: clamp(2.25rem, 4.13vw, 4.88rem);
}

[data-size='lg'] {
  font-size: clamp(3rem, 5.63vw, 6.63rem);
}

[data-size='xl'] {
  font-size: clamp(4rem, 7.5vw, 8.75rem);
}

[data-size='xxl'] {
  font-size: clamp(4.5rem, 8.26vw, 9.76rem);
}

/* Numbers are 1.5× the original (1.2× the new word size) so the
   section number reads as a dominant graphic element. */
.section-opener-num[data-size='sm'] {
  font-size: clamp(1.27rem, 2.1vw, 2.4rem);
}

.section-opener-num[data-size='smd'] {
  font-size: clamp(2.1rem, 3.9vw, 4.5rem);
}

.section-opener-num[data-size='md'] {
  font-size: clamp(2.7rem, 4.95vw, 5.85rem);
}

.section-opener-num[data-size='lg'] {
  font-size: clamp(3.6rem, 6.75vw, 7.95rem);
}

.section-opener-num[data-size='xl'] {
  font-size: clamp(4.8rem, 9vw, 10.5rem);
}

.section-opener-num[data-size='xxl'] {
  font-size: clamp(5.4rem, 9.9vw, 11.7rem);
}

/* Each word stays on a single line; we scale size per character count so
   nothing needs to wrap. overflow-wrap / hyphens are unset so long words
   don't break mid-letter. */
.section-opener-num,
.section-opener-word {
  max-width: 100%;
  white-space: nowrap;
  overflow-wrap: normal;
  hyphens: manual;
}

/* Number has a trailing period. */
.section-opener-num::after {
  content: '.';
  color: var(--palette-5, var(--color-text-muted));
}

/* Ink blots — halftone-dithered color blobs used as atmospheric accents
   on section openers and single-video slides. Absolutely-positioned,
   below all content, with low opacity + mix-blend-mode so they read as
   a printed-paper stain rather than a UI element. Per-blot gradient
   colors come from CSS custom properties set by the JS generator so the
   blot participates in the School-of-Athens palette. */
.ink-blot {
  position: absolute;
  pointer-events: none;
  z-index: 0;
  opacity: 1;
  mix-blend-mode: screen;
  /* Strong saturation + slight hue rotation (subtle, deterministic) so
     the palette colors read as chromatic rather than muted earth tones
     against the dark background. */
  filter: saturate(1.8) contrast(1.1);
}

.ink-blot svg {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
  /* let the blur halo extend past the SVG box */
}

/* (Section-opener blot placement is defined under the `.section-opener-wrap
   .ink-blot` rule above — blots are grid items, not absolutely-positioned
   accents. One blot per empty module of the 12-col grid.) */

/* Single-video slides — blot is anchored to a corner and peeks out from
   BEHIND the video's border rather than sitting fully behind it. The
   result reads as a stain where the video was placed on top of it, not
   a uniform tinted plate. Deterministic corner picked per slide in JS
   via the `data-blot-corner` attr on the blot wrapper. */
.slide.layout-video .ink-blot {
  width: 60%;
  height: 60vh;
  opacity: 0.8;
}

.slide.layout-video .ink-blot[data-blot-corner='tr'] {
  top: 4%;
  right: -6%;
  left: auto;
  bottom: auto;
  transform: none;
}

.slide.layout-video .ink-blot[data-blot-corner='bl'] {
  bottom: 4%;
  left: -6%;
  right: auto;
  top: auto;
  transform: none;
}

.slide.layout-video .ink-blot[data-blot-corner='tl'] {
  top: 4%;
  left: -6%;
  right: auto;
  bottom: auto;
  transform: none;
}

.slide.layout-video .ink-blot[data-blot-corner='br'] {
  bottom: 4%;
  right: -6%;
  left: auto;
  top: auto;
  transform: none;
}

/* The video itself must sit above the blot so the blot reads as a
   backdrop, not a foreground tint. */
.slide.layout-video .video-container,
.slide.layout-video video,
.slide.layout-video .slide-title,
.slide.layout-video .slide-heading {
  position: relative;
  z-index: 1;
}

.slide.layout-video {
  position: relative;
  overflow: hidden;
}

.image-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 200px;
  background: var(--color-image-placeholder);
  border-radius: var(--image-radius);
  color: var(--color-text-muted);
  font-size: var(--size-caption);
}

/* Layout: columns */
.layout-columns-3,
.layout-columns-4 {
  display: grid;
  gap: var(--space-md);
  align-content: start;
}

.layout-columns-2 {
  grid-template-columns: repeat(2, 1fr);
  display: grid;
  gap: var(--space-lg);
  align-items: center;
  height: 100%;
}

.layout-columns-2 .image-frame img {
  max-height: 70vh;
  object-fit: contain;
}

.layout-columns-3 {
  grid-template-columns: repeat(3, 1fr);
}

.layout-columns-4 {
  grid-template-columns: repeat(4, 1fr);
}

/* Layout: clockwise */
.layout-clockwise .image-frame {
  width: var(--image-clockwise-size);
}

/* Layout: pos-custom — same absolute-positioning family as clockwise, but
   each image's (left, top) is derived from its `pos:` keyword in markdown.
   Width inherits the clockwise sizing; per-image scale is layered on via
   --reveal-to so `scale:2` images render at 2× this base width. */
.layout-pos-custom .image-frame {
  width: var(--image-clockwise-size);
}

/* Layout: default / left-right grid */
.layout-default {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-md);
  align-content: start;
}

/* ---- Subtitle Bar ---- */
#subtitle-bar {
  position: fixed;
  bottom: var(--subtitle-bottom);
  left: 50%;
  transform: translateX(-50%);
  max-width: var(--subtitle-max-width);
  width: calc(100% - 4rem);
  padding: var(--subtitle-padding);
  background: rgba(30, 30, 30, 0.2);
  -webkit-backdrop-filter: blur(24px) saturate(1.3);
  backdrop-filter: blur(24px) saturate(1.3);
  border: 1px solid rgba(255, 255, 255, 0.06);
  backdrop-filter: blur(12px);
  border-radius: var(--subtitle-radius);
  font-size: var(--size-subtitle);
  color: var(--color-subtitle-text);
  line-height: var(--leading-loose);
  z-index: 90;
  opacity: 1;
  transition: opacity var(--transition-normal);
}

#subtitle-bar.hidden {
  opacity: 0;
  pointer-events: none;
}

#subtitle-bar:not(.active) {
  opacity: 0;
  pointer-events: none;
}

.subtitle-word {
  opacity: 0.2;
  transition: opacity var(--transition-fast);
}

.subtitle-word.revealed {
  opacity: 1;
}

/* ---- Controls Overlay ----
   Collapsed badge in the LOWER-LEFT corner. Hovering expands it to
   show the keyboard legend. Stays readable without cluttering slides. */
#controls-hint {
  position: fixed;
  bottom: var(--space-sm);
  left: var(--space-md);
  right: auto;
  font-size: var(--size-caption);
  font-family: var(--font-mono);
  color: var(--color-text-muted);
  z-index: 80;
  display: flex;
  align-items: center;
  gap: 0.6em;
  padding: 0.35rem 0.6rem;
  background: rgba(30, 30, 30, 0.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  backdrop-filter: blur(14px) saturate(1.2);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.35rem;
  opacity: 0.7;
  pointer-events: auto;
  cursor: default;
  transition: opacity var(--transition-fast), max-width var(--transition-normal);
  max-width: 4.5rem;
  overflow: hidden;
}

#controls-hint:hover {
  opacity: 1;
  max-width: 32rem;
}

#controls-hint .controls-badge {
  flex: 0 0 auto;
  letter-spacing: 0.05em;
  color: var(--color-text);
}

#controls-hint .controls-legend {
  flex: 1 1 auto;
  white-space: nowrap;
  opacity: 0;
  transition: opacity var(--transition-normal);
}

#controls-hint:hover .controls-legend {
  opacity: 1;
}

/* ---- Status Indicators ---- */
.status-toast {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: var(--space-sm) var(--space-lg);
  background: var(--color-surface);
  border-radius: var(--image-radius);
  font-size: var(--size-body);
  color: var(--color-text);
  opacity: 0;
  transition: opacity var(--transition-fast);
  z-index: 200;
  pointer-events: none;
}

.status-toast.visible {
  opacity: 1;
}

/* ---- Section Overview (Escape key) ---- */
/* Full-viewport dim backdrop. Inside, the 9 .overview-section rows
   stack VERTICALLY and center vertically — the list reads as a
   column, inset from all viewport edges by --slide-padding. Height is
   auto (only as tall as the 9 rows need); very tall viewports get
   vertical centering, overflow scrolls if needed. */
#section-overview {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.9);
  z-index: 300;
  display: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 0;
  padding: var(--slide-padding);
  overflow-y: auto;
}

#section-overview.visible {
  display: flex;
}

#section-overview>.overview-section {
  max-width: 900px;
  width: 100%;
  flex: 0 0 auto;
}

.overview-section {
  cursor: pointer;
  padding: var(--space-sm) var(--space-md);
  border: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0;
  transition: color var(--transition-fast), background var(--transition-fast), padding-left var(--transition-fast);
  display: flex;
  align-items: baseline;
  gap: var(--space-lg);
}

/* Warm-palette accent on hover — uses --palette-4 (#cc7e52, matches
   the inline-link treatment elsewhere) plus a subtle tinted wash and a
   small indent so the row reads as actively selectable. */
.overview-section:hover {
  color: var(--palette-4, #cc7e52);
  background: rgba(204, 126, 82, 0.08);
  padding-left: calc(var(--space-md) + 0.5rem);
}

.overview-section:hover .overview-section-slides {
  color: var(--palette-4, #cc7e52);
  opacity: 0.85;
}

.overview-section-title {
  font-family: var(--font-condensed);
  font-size: clamp(1.2rem, 2vw, 1.75rem);
  font-weight: var(--weight-regular);
  margin-bottom: 0;
  flex: 1;
}

.overview-section-slides {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--color-text-muted);
  letter-spacing: 0.04em;
  white-space: nowrap;
}

/* ---- Speed + Language Selector (bottom-right) ----
   Pinned to the lower-right corner so it mirrors the controls badge
   in the lower-left — two small corner UIs, one for "how I'm watching"
   (speed/language) and one for "where I am + how to navigate". */
#lang-selector {
  position: fixed;
  bottom: var(--space-sm);
  right: var(--space-md);
  top: auto;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

#speed-select,
#lang-select {
  background: rgba(0, 0, 0, 0.6);
  color: var(--color-text-muted);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 0.25rem;
  padding: 0.2rem 0.5rem;
  font-size: var(--size-caption);
  font-family: var(--font-mono);
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}

#speed-select:hover,
#speed-select:focus,
#lang-select:hover,
#lang-select:focus {
  color: var(--color-text);
  border-color: rgba(255, 255, 255, 0.35);
  outline: none;
}

/* Language selector hides when only English available, speed always visible */

/* ---- Layout: Video Questions (7_36) ---- */
.slide.layout-video-questions {
  justify-content: center;
  align-items: center;
  gap: var(--space-md);
}

.video-questions-wrapper {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  flex: 1;
  min-height: 0;
}

.slide.layout-video-questions .slide-heading {
  font-family: var(--font-condensed);
  font-size: var(--size-heading);
  text-align: center;
}

.video-questions-video {
  max-width: 70%;
  max-height: 65vh;
  border-radius: var(--image-radius);
}

.video-question {
  position: absolute;
  font-family: var(--font-condensed);
  font-size: clamp(1.1rem, 1.5vw, 1.6rem);
  color: var(--color-text);
  max-width: 22%;
  line-height: 1.4;
}

.video-question-top-left {
  top: 5%;
  left: 2%;
  transform: rotate(-3deg);
}

.video-question-top-right {
  top: 5%;
  right: 2%;
  transform: rotate(2deg);
  text-align: right;
}

.video-question-bottom-left {
  bottom: 8%;
  left: 2%;
  transform: rotate(2deg);
}

.video-question-bottom-right {
  bottom: 8%;
  right: 2%;
  transform: rotate(-2deg);
  text-align: right;
}

/* ---- Layout: Centered List (7_35 "8 Rules") ----
   Vertically + horizontally centered composition. The bullet list uses
   numbered-hierarchy styling: top-level items read as large numbered
   rules, sub-bullets indent beneath in a condensed face for typographic
   hierarchy between rule and elaboration. */
.slide.layout-centered-list {
  justify-content: center;
  align-items: stretch;
  text-align: left;
  gap: var(--space-md);
  padding-left: clamp(2rem, 10vw, 8rem);
  padding-right: clamp(2rem, 10vw, 8rem);
}

.slide.layout-centered-list .slide-title {
  text-align: center;
  margin-bottom: var(--space-sm);
}

.slide.layout-centered-list .slide-heading {
  text-align: center;
  color: var(--color-text-muted);
}

.slide.layout-centered-list .slide-bullets {
  align-items: stretch;
  gap: var(--space-md);
  list-style: none;
  padding: 0;
  margin: 0;
}

/* Numbered hierarchy — top-level rule in Graphik Wide Semibold with
   number + text on the same baseline, then any sub-bullets below in
   Graphik Condensed. The number anchors flush-left; the text starts
   right next to the number and sub-bullets stack below, indented to
   the text's left edge. */
.slide.layout-centered-list .numbered-hierarchy {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  width: 100%;
  max-width: 1100px;
  margin: 0 auto;
}

.slide.layout-centered-list .numbered-bullet {
  display: flex;
  align-items: baseline;
  gap: 0.6em;
  border-left: none;
  padding-left: 0;
  text-align: left;
  font-family: var(--font-wide);
  font-weight: 600;
  font-size: clamp(1.1rem, 1.7vw, 1.85rem);
  line-height: 1.2;
  letter-spacing: -0.005em;
  /* Inherit the same per-click reveal transition the base .slide-bullet
     has — starts at opacity 0, applyClick() adds .revealed one at a
     time so each rule reveals with its narration block. */
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--transition-reveal), transform var(--transition-reveal);
}

.slide.layout-centered-list .numbered-bullet.revealed {
  opacity: 1;
  transform: translateY(0);
}

.slide.layout-centered-list .numbered-bullet::before {
  content: none;
}

.slide.layout-centered-list .numbered-bullet .bullet-number {
  font-family: var(--font-wide);
  font-weight: 600;
  color: var(--plus-color, var(--palette-4, #cc7e52));
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
  min-width: 1.6em;
}

.slide.layout-centered-list .numbered-bullet .numbered-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35em;
}

.slide.layout-centered-list .numbered-bullet .slide-bullet-text {
  color: var(--color-text);
  display: block;
}

/* Sub-bullets stack beneath the rule text, indented to match the
   text's left edge (numbered-body is already indented past the
   number, so margin-left:0 here keeps them aligned under the text). */
.slide.layout-centered-list .sub-bullets {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15em;
}

.slide.layout-centered-list .sub-bullet {
  font-family: var(--font-condensed);
  font-weight: 400;
  font-size: clamp(0.85rem, 1.05vw, 1.05rem);
  line-height: 1.45;
  color: var(--color-text-muted);
  letter-spacing: 0;
}

.centered-list-source {
  margin-top: var(--space-md);
  font-size: var(--size-caption);
  opacity: 0.6;
}

/* ---- Layout: Slop Timeline (2_15) ---- */
.slide.layout-slop-timeline {
  flex-direction: row;
}

.slop-timeline-row {
  display: flex;
  gap: var(--space-lg);
  width: 100%;
  height: 100%;
}

.slop-timeline-left {
  flex: 0 0 22%;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: var(--space-md);
  position: relative;
}

/* Title / heading / caption sit above the scaled cover (z:0) so the
   cover functions as a backdrop, not an occluder. */
.slop-timeline-left .slide-title,
.slop-timeline-left .slide-heading,
.slop-timeline-left .slop-timeline-caption {
  position: relative;
  z-index: 1;
}

.slop-timeline-caption {
  font-family: var(--font-wide);
  font-size: var(--size-caption);
  color: var(--color-text-muted);
  line-height: 1.5;
}

.slop-timeline-caption p {
  margin-bottom: var(--space-xs);
}

.slop-cover {
  /* Anchored to the bottom of the left column so its bottom edge aligns
     with the big timeline image on the right (both bottom out at the
     slide's bottom padding). Scales up from that anchor — grows upward
     only, never into the title/caption above. */
  --reveal-to: 2.5;
  --reveal-from: 2.3;
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 100%;
  transform: translateX(-50%) scale(var(--reveal-from, 2.3));
  transform-origin: center bottom;
  z-index: 0;
}

/* Preserve the centering translate alongside the final scale factor. */
.slop-cover.revealed {
  transform: translateX(-50%) scale(var(--reveal-to, 2.5));
}

.slop-cover img {
  width: 100%;
  border-radius: var(--image-radius);
  box-shadow: var(--image-shadow);
}

/* Right column (big timeline image) is raised above the cover's
   spillover so overlap reads as "cover behind timeline". */
.slop-timeline-right {
  position: relative;
  z-index: 1;
}

.slop-timeline-right {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
}

.slop-timeline-right img {
  max-width: 100%;
  max-height: 80vh;
  object-fit: contain;
  border-radius: var(--image-radius);
  box-shadow: var(--image-shadow);
}

/* ---- Play/Pause Indicator ---- */
#play-pause-indicator {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.8);
  font-size: 5rem;
  color: rgba(255, 255, 255, 0.8);
  background: rgba(0, 0, 0, 0.5);
  width: 120px;
  height: 120px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  z-index: 200;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease, transform 0.3s ease;
}

#play-pause-indicator.visible {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
  animation: fade-out 0.8s ease 0.4s forwards;
}

@keyframes fade-out {
  to {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.8);
  }
}

/* ---- Section Opener Animations ---- */
/* Words and blots breathe gently via CSS custom props set by JS per element.
   All motion uses transform + filter only so animations stay compositor-
   threaded and never trigger layout. Seeded per-element so each opener
   has a unique personality but replays identically on every visit. */

@keyframes opener-word-drift {
  from {
    transform: translate(0, 0);
  }

  to {
    transform: translate(var(--drift-x, 0vw), var(--drift-y, 0vh));
  }
}

/* Word drift: one-shot entrance animation. Each word eases from its grid
   position to a conflict-free diagonal-neighbor position and rests there.
   Blot animation was intentionally removed — blots now sit still and the
   pattern screens carry the visual interest. */
.layout-section-opener .section-opener-word,
.layout-section-opener .section-opener-num,
.layout-debug-opener .section-opener-word,
.layout-debug-opener .section-opener-num {
  animation: opener-word-drift var(--drift-dur, 8s) var(--drift-ease, ease-in-out) var(--drift-delay, 0s) 1 forwards;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {

  .layout-section-opener .section-opener-word,
  .layout-section-opener .section-opener-num,
  .layout-section-opener .section-opener-wrap .ink-blot,
  .layout-debug-opener .section-opener-word,
  .layout-debug-opener .section-opener-num,
  .layout-debug-opener .section-opener-wrap .ink-blot {
    animation: none;
  }
}

/* ---- Proportional Responsiveness ---- */
/* Safety net for smaller desktop windows — not full mobile */

@media (max-width: 1200px) {
  .slide.layout-bullets-left-images-right {
    gap: var(--space-lg);
  }

  .bibliography-columns {
    gap: var(--space-lg);
  }

  .compare-column {
    padding: var(--space-md) var(--space-lg);
  }
}

@media (max-width: 960px) {

  /* Stack 2-column layouts when window gets narrow */
  .slide.layout-bullets-left-images-right {
    flex-direction: column;
  }

  .slide-image-area {
    max-height: 40vh;
  }

  .bibliography-columns {
    flex-direction: column;
  }

  .layout-columns-2 {
    grid-template-columns: 1fr;
  }

  /* Reduce padding */
  :root {
    --slide-padding: clamp(1.5rem, 3vw, 3rem);
  }

  /* Hide section labels at narrow widths */
  .progress-section-title {
    display: none;
  }

  /* Language selector tighter */
  #lang-selector {
    right: 1rem;
  }
}

@media (max-width: 768px) {
  .cover-image {
    margin-top: 0;
  }
}

/* ---- Fullscreen ---- */
:fullscreen #app {
  width: 100vw;
  height: 100vh;
}

/* ---- Debug Panel (?debug=1) ---- */
/* Floating panel overlaid on the section-opener preview. Gated behind
   ?debug=1 — zero production footprint. Purposely utilitarian: this is
   a dev tool, not a UI showcase. */

.slide.layout-debug-opener {
  position: fixed;
  inset: 0;
  display: block;
  z-index: 5;
}

#debug-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 300px;
  background: rgba(10, 10, 10, 0.94);
  border-left: 1px solid rgba(255, 255, 255, 0.1);
  overflow-y: auto;
  z-index: 500;
  padding: 0.75rem;
  font-family: 'SF Mono', 'Fira Code', monospace;
  font-size: 11px;
  color: rgba(255, 255, 255, 0.75);
  scrollbar-width: thin;
}

#debug-panel h3 {
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.45);
  margin-bottom: 0.75rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

#debug-panel h4 {
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.35);
  margin: 0.75rem 0 0.4rem;
}

.debug-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 0.3rem;
}

.debug-row label {
  flex: 0 0 90px;
  color: rgba(255, 255, 255, 0.55);
  font-size: 10px;
}

.debug-row input[type='range'] {
  flex: 1;
  height: 3px;
  accent-color: var(--palette-5, #e8c77a);
  cursor: pointer;
}

.debug-row .debug-val {
  flex: 0 0 36px;
  text-align: right;
  color: rgba(255, 255, 255, 0.85);
  font-size: 10px;
}

.debug-row select {
  flex: 1;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.8);
  font-size: 10px;
  padding: 0.15rem 0.3rem;
  border-radius: 3px;
  cursor: pointer;
}

.debug-row input[type='number'] {
  flex: 1;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.8);
  font-size: 10px;
  padding: 0.15rem 0.3rem;
  border-radius: 3px;
  width: 60px;
}

.debug-triplet {
  display: flex;
  gap: 0.25rem;
  flex: 1;
}

.debug-triplet select {
  flex: 1;
}

/* Palette swatch strips — 12 clickable color chips per triangle vertex. */
.debug-swatch-strip {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 2px;
  flex: 1;
}

.debug-swatch {
  width: 100%;
  aspect-ratio: 1;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 2px;
  padding: 0;
  cursor: pointer;
  transition: transform 0.1s, border-color 0.1s;
}

.debug-swatch:hover {
  transform: scale(1.15);
  z-index: 2;
  border-color: rgba(255, 255, 255, 0.4);
}

.debug-swatch.active {
  border-color: #ffffff;
  box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px rgba(0, 0, 0, 0.8);
  z-index: 1;
}

.debug-btn-row {
  display: flex;
  gap: 0.35rem;
  margin-top: 0.75rem;
  flex-wrap: wrap;
}

.debug-btn {
  flex: 1;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.12);
  color: rgba(255, 255, 255, 0.75);
  font-family: inherit;
  font-size: 10px;
  padding: 0.35rem 0.5rem;
  border-radius: 3px;
  cursor: pointer;
  transition: background 0.15s;
}

.debug-btn:hover {
  background: rgba(255, 255, 255, 0.13);
}

.debug-preset-row {
  display: flex;
  gap: 0.3rem;
  margin-top: 0.4rem;
  align-items: center;
}

.debug-preset-row input[type='text'] {
  flex: 1;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.8);
  font-size: 10px;
  font-family: inherit;
  padding: 0.25rem 0.4rem;
  border-radius: 3px;
}

#debug-section-tag {
  display: inline-block;
  margin-top: 0.25rem;
  font-size: 9px;
  color: rgba(255, 255, 255, 0.3);
  letter-spacing: 0.05em;
}

.debug-divider {
  border: none;
  border-top: 1px solid rgba(255, 255, 255, 0.07);
  margin: 0.6rem 0;
}