/* keynote.css — gate + post-unlock placeholder. Inherits :root tokens from /styles.css. */

:root {
  --line: rgb(255 255 255 / 14%);
  --line-strong: rgb(255 255 255 / 28%);
  --fg-dim: rgb(255 255 255 / 78%);
  --fg-muted: rgb(255 255 255 / 56%);
  --field-bg: rgb(255 255 255 / 4%);
  --field-bg-focus: rgb(255 255 255 / 8%);
}

.page {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.top-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.25rem clamp(1rem, 4vw, 2rem);
  gap: 1rem;
}

.top-nav__mark {
  font-family: var(--font-stack-display);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--color-fg);
  text-decoration: none;
  white-space: nowrap;
}

.top-nav__menu {
  display: flex;
  gap: clamp(0.75rem, 2vw, 1.5rem);
  flex-wrap: wrap;
  justify-content: flex-end;
}

.top-nav__link {
  font-family: var(--font-stack);
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  color: var(--fg-dim);
  text-decoration: none;
  transition: color 160ms ease;
}

.top-nav__link:hover,
.top-nav__link:focus-visible {
  color: var(--color-fg);
  outline: none;
}

.top-nav__link.is-current {
  color: var(--color-fg);
}

.keynote-page {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: clamp(2rem, 6vh, 4rem) clamp(1rem, 4vw, 2rem)
    clamp(3rem, 7vh, 5rem);
  gap: clamp(2rem, 5vh, 3rem);
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
}

/* Widen the page when the player is the active section — the gate stays
   narrow (form-card design), the player wants a 16:9 frame at up to
   1024px so the video isn't crammed into the gate's 720px column.
   Also flip justify-content to flex-start so the video sits just below
   the top-nav instead of getting pushed toward the vertical center on
   tall viewports (the page's flex column has flex:1 from .page). The
   gate keeps the center alignment so the form floats nicely. */
.keynote-page:has(#keynote-player:not([hidden])) {
  max-width: 1080px;
  justify-content: flex-start;
}

/* Class rules beat the UA's [hidden]{display:none}, so this raises
   specificity to make the JS toggle actually hide sections. */
.keynote-page > [hidden] {
  display: none;
}

.keynote-checking {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

.keynote-checking-spinner {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 1.5px solid rgb(255 255 255 / 14%);
  border-top-color: var(--color-fg);
  animation: keynote-spin 700ms linear infinite;
}

@keyframes keynote-spin {
  to {
    transform: rotate(360deg);
  }
}

.keynote-gate {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(2rem, 4vh, 2.75rem);
}

.keynote-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
  text-align: center;
  max-width: 32rem;
}

.keynote-kicker {
  margin: 0;
  font-family: var(--font-stack);
  font-size: 0.65rem;
  font-weight: 500;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

.keynote-title {
  margin: 0;
  font-family: var(--font-stack-display);
  font-weight: 700;
  font-size: clamp(1.5rem, 4vw, 2rem);
  line-height: 1.15;
  letter-spacing: 0.08em;
}

.keynote-lede {
  margin: 0;
  font-family: var(--font-stack);
  font-size: 0.88rem;
  line-height: 1.55;
  color: var(--fg-dim);
}

.keynote-card {
  position: relative;
  overflow: hidden;
  width: 100%;
  max-width: 440px;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  padding: clamp(1.5rem, 4vw, 2.25rem);
  border: 1px solid var(--line);
  border-radius: 12px;
  background: rgb(255 255 255 / 3%);
}

.keynote-form {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.keynote-form-field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.keynote-form-label {
  display: block;
  font-family: var(--font-stack);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

.keynote-form input {
  width: 100%;
  padding: 0.95rem 1rem;
  background: var(--field-bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--color-fg);
  font-family: var(--font-stack);
  /* 16px floor — anything smaller triggers iOS Safari's auto-zoom on focus. */
  font-size: clamp(1rem, 1.4vw, 1.1rem);
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-align: center;
  transition:
    background-color 160ms ease,
    border-color 160ms ease;
}

.keynote-form input::placeholder {
  color: rgb(255 255 255 / 30%);
  letter-spacing: 0.2em;
  font-weight: 400;
}

.keynote-form input:focus {
  outline: none;
  background: var(--field-bg-focus);
  border-color: var(--line-strong);
}

.keynote-form-field.is-error input {
  border-color: #ff5d5d;
}

.keynote-form-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
  pointer-events: none;
}

.keynote-form-submit {
  margin-top: 0.25rem;
  padding: 0.85rem 1.5rem;
  background: var(--color-fg);
  color: var(--color-bg);
  border: 1px solid var(--color-fg);
  border-radius: 6px;
  font-family: var(--font-stack-display);
  font-weight: 700;
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  transition:
    background-color 160ms ease,
    color 160ms ease,
    opacity 160ms ease;
}

.keynote-form-submit:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.keynote-form-submit:hover:not(:disabled),
.keynote-form-submit:focus-visible:not(:disabled) {
  background: transparent;
  color: var(--color-fg);
  outline: none;
}

.keynote-form-arrow {
  font-size: 1rem;
  line-height: 1;
}

/* Spinner reuses the button's text color so it stays visible whether
   the button is filled (black on white) or outlined on hover. */
.keynote-form-submit-spinner {
  display: none;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  border-top-color: transparent;
  animation: keynote-spin 700ms linear infinite;
}

.keynote-form-submit.is-loading .keynote-form-arrow {
  display: none;
}

.keynote-form-submit.is-loading .keynote-form-submit-spinner {
  display: inline-block;
}

.keynote-form.is-loading .keynote-form-field input {
  opacity: 0.55;
  pointer-events: none;
}

/* Hide the divider + Request-access link while the form is mid-flow
   (auto-unlock 2.5s wait OR a manual submit). Without this, a user
   who clicked a share link sees "Checking you're not a bot…" but the
   Request-access link stays tappable below it — they sometimes tap it
   thinking the page is stuck, breaking the auto-unlock flow. Sibling
   combinator on the form's is-loading state covers both flows; on
   failure the class is removed by the existing finally block and the
   CTA returns. */
.keynote-form.is-loading ~ .keynote-card-divider,
.keynote-form.is-loading ~ .keynote-request-cta {
  display: none;
}

.keynote-form-status {
  margin: 0;
  padding: 0.6rem 0.8rem;
  border-radius: 6px;
  font-family: var(--font-stack);
  font-size: 0.8rem;
  line-height: 1.45;
  background: rgb(255 255 255 / 6%);
  border: 1px solid var(--line);
  color: var(--fg-dim);
}

.keynote-form-status.is-error {
  background: rgb(255 93 93 / 10%);
  border-color: rgb(255 93 93 / 32%);
  color: #ffc1c1;
}

.keynote-card::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgb(255 255 255 / 60%) 50%,
    transparent 100%
  );
  transform: translateX(-100%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms ease;
}

.keynote-card.is-loading::after {
  opacity: 0.85;
  animation: keynote-progress 1.4s linear infinite;
}

@keyframes keynote-progress {
  from {
    transform: translateX(-100%);
  }

  to {
    transform: translateX(100%);
  }
}

.keynote-card-divider {
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgb(255 255 255 / 14%) 50%,
    transparent 100%
  );
  margin: 0.25rem 0;
}

.keynote-request-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  align-self: center;
  padding: 0.7rem 1.3rem;
  font-family: var(--font-stack);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  color: var(--fg-dim);
  text-decoration: none;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 6px;
  transition:
    color 160ms ease,
    border-color 160ms ease,
    background-color 160ms ease;
}

.keynote-request-cta:hover,
.keynote-request-cta:focus-visible {
  color: var(--color-fg);
  border-color: var(--line-strong);
  background: rgb(255 255 255 / 4%);
  outline: none;
}

.keynote-player {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  gap: clamp(1.25rem, 3vh, 2rem);
  /* No padding here — the parent .keynote-page already provides the
     page-level breathing room. Doubling it was pushing the video down
     by up to ~8rem on tall viewports. */
}
/* ---------- Player chrome (post-unlock video player) ---------- */
/* Ported from www-anser-rossii-com/keynote.css (player section).
   Chapter ticks, chapters menu, chapter title overlay, and the
   chapter overview section have been intentionally omitted — the
   player ships without chapters in this round. Recolor pass:
   cockpit-orange (#ff6b00) accents replaced with the metallic
   silver palette used by the Z.P.P. mark. */

:root {
  --player-accent: #d8dde2;
  --player-accent-dim: rgb(216 221 226 / 78%);
  --player-accent-grad: linear-gradient(180deg, #2a2d30 0%, #8a8f94 24%, #d8dde2 50%, #8a8f94 76%, #2a2d30 100%);
}

.keynote-frame-wrap {
  width: 100%;
  /* Cap width so the 16:9 frame can't push the downloads bar or footer
     off-screen on short / landscape viewports (laptop split-screen,
     phone landscape, etc.). min() picks the smaller of the desktop
     cap and the width that fits the available viewport height at 16:9.
     The 18rem subtraction approximates: top-nav (~4rem) + page top
     padding (~4rem) + gap below frame (~2rem) + downloads row
     (~3rem) + page bottom padding (~3rem) + footer (~2rem).
     max(320px, ...) floors the cap at a usable size — without it, on
     absurdly short viewports (~<288px tall: iPad split-screen, dev-
     tools responsive) the calc goes negative, min() returns negative,
     and max-width clamps to 0 → invisible frame. 320px gives ~180px
     tall video; user scrolls if everything else doesn't fit.
     vh line is the fallback for Safari 14/15 which doesn't parse dvh
     (drops the whole rule if it can't resolve the value); the second
     line overrides on browsers that do understand dvh and benefits
     from iOS Safari's dynamic viewport when the address bar collapses. */
  max-width: min(1024px, max(320px, calc((100vh - 18rem) * 16 / 9)));
  max-width: min(1024px, max(320px, calc((100dvh - 18rem) * 16 / 9)));
  margin-inline: auto;
}

/* Download buttons under the player — only shown when the admin has
   attached a PDF and/or Keynote file. Hidden by default; keynote.js
   un-hides + populates when verify/unlock response carries assets. */
.keynote-downloads {
  width: 100%;
  max-width: 1024px;
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  justify-content: center;
}

/* The display rule above defeats the UA's [hidden] — without this the
   empty downloads bar reserves space even when JS hides it. */
.keynote-downloads[hidden] {
  display: none;
}

.keynote-download {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  /* 0.85rem vertical padding + 0.78rem line-height ≈ 44px touch target,
     matches iOS HIG. */
  padding: 0.85rem 1.2rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgb(255 255 255 / 4%);
  color: var(--fg-dim);
  font-family: var(--font-stack);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  transition:
    color 160ms ease,
    background-color 160ms ease,
    border-color 160ms ease;
}

.keynote-download:hover,
.keynote-download:focus-visible {
  color: var(--player-accent, #fff);
  background: rgb(255 255 255 / 8%);
  border-color: rgb(255 255 255 / 28%);
  outline: none;
}

.keynote-download-icon {
  width: 14px;
  height: 14px;
  flex: none;
  /* Inherits currentColor via the icon's stroke attribute so it fades
     with the label on hover. */
}

.keynote-download-meta {
  margin-left: 0.4rem;
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: none;
  color: rgb(255 255 255 / 56%);
  font-variant-numeric: tabular-nums;
}

.keynote-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--line);
  border-radius: 16px;
  overflow: hidden;
  background: #000;

  /* Account for the fixed .top-panel header when chapter links call
     scrollIntoView({ block: 'start' }). Without this the player's
     top edge lines up with the viewport top and disappears under
     the menu. Matches the page padding-top scale so the offset
     adapts to the same viewport-height curve. */
  scroll-margin-top: clamp(5rem, 8vh, 6rem);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 5%),
    0 8px 28px rgb(0 0 0 / 32%),
    0 4px 24px rgb(216 221 226 / 4%);
}

.keynote-frame iframe,
.keynote-frame .keynote-video {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* Self-hosted HLS player chrome. Native UA controls are off (set in
   keynote.js); we layer a click-catcher, a buffering spinner, a big-play
   overlay, and a custom bottom control bar built by setupPlayerControls.
   The bar matches the metallic-silver accent + display-stack labels used
   elsewhere on the page. */

:root {
  /* LQIP — 40×22 WebP @ q=50 of media/keynote/v1/poster.jpg, pre-tone-
     shifted with "modulate 100,95,100" so the inline preview matches
     the full poster's saturate(0.95) tone before the CSS filter even
     runs. Without that pre-shift, the LQIP rendered at full saturation
     looked noticeably brighter than the poster fading in over it.
     ~248 bytes raw / ~336 bytes after base64. Regenerate after a
     poster swap with:
       magick media/keynote/v1/poster.jpg -resize 40x22^ -gravity center \
         -extent 40x22 -modulate 100,95,100 -quality 50 - | base64 -w0 */
  --keynote-poster-lqip: url('data:image/webp;base64,UklGRvAAAABXRUJQVlA4IOQAAADQBgCdASooABYAPr1Kn0unJCKhtVv8AOAXiWMAxvQhWeEowqL62lBzPRhr6BBHRf1AwRFsum63pEhjKi1zxgAA/vRPQ34gSs6Rt/pEcxGUzY9ki7oCO3C00qiAfr2wDDfAkrV4aEF+t4EUc/S6AwtFHjw82nd41o48yIzYNtDDYmcWM7PXf4ewMi2yxnw977RCNDL0CrClwQxnuO3YV+bX5tdOQaKKTw+eWX5VkbuW1FnW/yIwFNmLyyjhs5iycAD3tkADvNFdvqvIw2dxU/Rkznwa+8XSgSKJb1ZygOIs00dRQAA=');
}

.keynote-frame .keynote-video {
  background: #000;
  object-fit: contain;
}

/* ---------- Poster blur-dissolve overlay ---------- */

/* Three layers stacked over the <video>: LQIP (instant) -> full poster
   (decoded) -> unblur (after a short hold). All three fade out together
   on play. .is-played drops again when the video ENDS, so the poster
   returns as a closing card instead of a frozen last frame; mid-
   session pauses keep the current frame (class stays on). */
.keynote-poster {
  position: absolute;
  inset: 0;
  z-index: 2;

  /* Big-play overlay (z=5) sits above this and catches the play click;
     elsewhere on the poster, clicks pass through to the click-catcher
     (z=1) below so any tap on the video area starts playback. */
  pointer-events: none;
  transition: opacity 380ms ease;
  overflow: hidden;
}

.keynote-frame.is-played .keynote-poster {
  opacity: 0;
}

/* Stage 1 — LQIP: a few hundred bytes of inline WebP, paints on first
   byte (no network). Sized + filtered to match the poster's initial
   blurred state so the LQIP -> poster handoff is invisible: same
   scale (1.015), same saturate/contrast so the color tone matches,
   only the blur radius differs (22px LQIP vs 14px poster) and that's
   the whole point of the technique. Fades out once the full poster
   decodes. The wrap clips any blur bleed at the edges. */
.keynote-poster-lqip {
  position: absolute;
  inset: 0;
  background: var(--keynote-poster-lqip) center / cover no-repeat;
  filter: saturate(0.95) contrast(1.05) blur(22px);
  transform: scale(1.015);
  transition: opacity 1100ms ease;
  will-change: opacity;
}

.keynote-frame.is-poster-loaded .keynote-poster-lqip {
  opacity: 0;
}

/* Stage 2 — full-resolution poster, fades in once the <img> decodes.
   Holds a soft blur for ~320ms (set in JS) before the .is-poster-clear
   class triggers the unblur, giving the eye the "blur -> clear" arc. */
.keynote-poster-img {
  position: relative;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  filter: saturate(0.95) contrast(1.05) blur(14px);
  transform: scale(1.015);
  transition:
    opacity 600ms ease 80ms,
    filter 1100ms ease 240ms,
    transform 1100ms ease 240ms;
  will-change: opacity, filter, transform;
}

.keynote-frame.is-poster-loaded .keynote-poster-img {
  opacity: 1;
}

.keynote-frame.is-poster-clear .keynote-poster-img {
  filter: saturate(0.95) contrast(1.05) blur(0);
  transform: scale(1);
}

/* iOS Safari injects an overlay play button by default — silence it so
   our big-play + control bar are the only chrome the user sees. */
.keynote-frame .keynote-video::-webkit-media-controls,
.keynote-frame .keynote-video::-webkit-media-controls-start-playback-button {
  display: none !important;
  appearance: none;
}

/* Click-catcher — full-frame transparent layer that turns clicks on the
   video area into play/pause toggles. Sits above <video> but below the
   control bar, so clicking the bar's buttons doesn't propagate. */
.keynote-clickcatcher {
  position: absolute;
  inset: 0;
  z-index: 1;
  cursor: pointer;

  /* Disable iOS double-tap-zoom on the video area — without this Safari
     waits ~300ms after each tap to see if a second tap is coming, which
     adds visible latency to the play/pause toggle on mobile. Keeps
     pinch-zoom working since "manipulation" only blocks the double-tap
     gesture. */
  touch-action: manipulation;
}

/* Visible error banner — surfaced by keynote.js paintErrorBanner. The
   default tone is neutral/warm ("Slow connection…", "Reconnecting…");
   red styling is reserved for the terminal modifier below. */
.keynote-error-banner {
  position: absolute;
  bottom: 4.5rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 7;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  max-width: calc(100% - 2rem);
  padding: 0.55rem 0.85rem;
  background: rgb(0 0 0 / 68%);
  border: 1px solid rgb(255 255 255 / 8%);
  border-radius: 8px;
  color: #fff;
  backdrop-filter: blur(10px) saturate(140%);
  font-family: var(--font-stack);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  box-shadow: 0 6px 18px rgb(0 0 0 / 50%);
  pointer-events: none;
  text-align: center;
}

/* The display: flex above defeats the UA's [hidden] rule (author origin
   wins at equal specificity) and this banner sits too deep for the
   .keynote-page > [hidden] guard — re-assert hidden explicitly. Without
   this the empty banner box floats over the player permanently. */
.keynote-error-banner[hidden] {
  display: none;
}

/* Red is reserved for terminal states (unsupported codec) — transient
   connection trouble stays in the neutral tone above. */
.keynote-error-banner.is-terminal {
  border-color: rgb(255 93 93 / 50%);
  color: #ffd5d5;
}

/* Freeze-frame overlay — holds the last picture over a recovery
   reload (video.load() blanks the element). Sits above the video,
   below the spinner/big-play chrome. */
.keynote-freeze-frame {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  background: #000;
  pointer-events: none;
}

/* Same [hidden]-vs-display trap class — re-assert. */
.keynote-freeze-frame[hidden] {
  display: none;
}

/* Buffering spinner — same recipe as the gate spinner, scaled up.
   Toggled by the WAITING/PLAYING events. */
.keynote-player-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 38px;
  height: 38px;
  margin: -19px 0 0 -19px;
  border-radius: 50%;
  border: 2px solid rgb(255 255 255 / 14%);

  /* dark halo so the ring never melts into bright footage */
  box-shadow: 0 0 0 14px rgb(0 0 0 / 45%);
  background: rgb(0 0 0 / 45%);
  border-top-color: var(--player-accent, #d8dde2);
  animation: keynote-spin 700ms linear infinite;
  z-index: 4;
  pointer-events: none;
}

.keynote-bigplay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;

  /* Above click-catcher + spinner, below controls. The user clicks
     anywhere on the poster to start; once playing it slides + fades
     out and the click-catcher takes over toggling play/pause. */
  z-index: 5;
  transition:
    opacity 240ms ease,
    transform 240ms ease;
}

.keynote-bigplay.is-hidden {
  opacity: 0;
  pointer-events: none;
  transform: scale(0.92);
}

.keynote-bigplay-ring {
  position: absolute;
  width: clamp(72px, 12vmin, 112px);
  height: clamp(72px, 12vmin, 112px);
  border-radius: 50%;
  background: radial-gradient(
    circle at center,
    rgb(216 221 226 / 18%) 0%,
    rgb(0 0 0 / 60%) 70%
  );
  border: 1.5px solid rgb(216 221 226 / 70%);
  box-shadow:
    0 0 32px rgb(216 221 226 / 28%),
    inset 0 0 18px rgb(216 221 226 / 18%);
  transition:
    background 220ms ease,
    border-color 220ms ease,
    box-shadow 220ms ease,
    transform 220ms ease;
}

.keynote-bigplay-icon {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: clamp(72px, 12vmin, 112px);
  height: clamp(72px, 12vmin, 112px);

  /* Titanium bright — matches the silver ring border and pops against
     the dark gradient center of the ring. Drop-shadow keeps the
     silhouette readable on the brightest poster frames. */
  color: #f0f3f5;
  filter: drop-shadow(0 1px 4px rgb(0 0 0 / 60%));
  pointer-events: none;
}

.keynote-bigplay-icon svg {
  width: clamp(34px, 5.5vmin, 52px);
  height: clamp(34px, 5.5vmin, 52px);

  /* HoollyWoody's play-triangle path is already optically balanced in
     its 24-unit viewBox; no margin-left nudge needed. */
}

.keynote-bigplay:hover .keynote-bigplay-ring,
.keynote-bigplay:focus-visible .keynote-bigplay-ring {
  background: radial-gradient(
    circle at center,
    rgb(216 221 226 / 32%) 0%,
    rgb(0 0 0 / 72%) 70%
  );
  border-color: var(--player-accent, #d8dde2);
  box-shadow:
    0 0 40px rgb(216 221 226 / 42%),
    inset 0 0 22px rgb(216 221 226 / 28%);
  transform: scale(1.04);
}

.keynote-bigplay:focus-visible {
  outline: none;
}

/* Idle pulse — only when the big-play overlay is the primary CTA
   (i.e. the user hasn't started yet). The ring breathes ~3% so the
   eye is drawn to it without feeling restless. Stops the moment the
   user hovers so the two animations don't double up. */
@keyframes keynote-bigplay-pulse {
  0%,
  100% {
    transform: scale(1);
    box-shadow:
      0 0 32px rgb(216 221 226 / 28%),
      inset 0 0 18px rgb(216 221 226 / 18%);
  }

  50% {
    transform: scale(1.035);
    box-shadow:
      0 0 44px rgb(216 221 226 / 38%),
      inset 0 0 22px rgb(216 221 226 / 24%);
  }
}

.keynote-bigplay:not(.is-hidden) .keynote-bigplay-ring {
  animation: keynote-bigplay-pulse 3.4s ease-in-out infinite;
}

.keynote-bigplay:not(.is-hidden):is(:hover, :focus-visible)
  .keynote-bigplay-ring {
  animation: none;
}

/* ---------- Custom control bar ---------- */

.keynote-controls {
  position: absolute;
  inset: auto 0 0;
  z-index: 6;
  padding: 0.95rem clamp(0.85rem, 2vw, 1.4rem) clamp(0.55rem, 1.4vh, 0.85rem);
  background: linear-gradient(
    180deg,
    transparent 0%,
    rgb(0 0 0 / 32%) 35%,
    rgb(0 0 0 / 86%) 100%
  );
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  opacity: 0;
  transform: translateY(8%);
  transition:
    opacity 220ms ease,
    transform 220ms ease;
  pointer-events: none;

  /* Hairline separator at the top of the bar — a single instrument-panel
     pixel of brand color. Dims into transparency at the edges so it
     doesn't fight the rounded video frame corners. */
  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 1px;
    background: linear-gradient(
      90deg,
      transparent 0%,
      rgb(216 221 226 / 28%) 22%,
      rgb(216 221 226 / 36%) 50%,
      rgb(216 221 226 / 28%) 78%,
      transparent 100%
    );
    pointer-events: none;
  }
}

/* Visible when JS adds .is-controls-visible (after pointer/keyboard
   activity) OR while the video is paused/ended (bar should always be
   reachable when the video isn't moving). :hover is a desktop
   convenience for windowed playback only — in fullscreen the cursor
   has nowhere to live except on the video, so a CSS-level :hover
   rule would hold the bar visible forever and override the JS-
   driven autohide we want there. */
.keynote-frame.is-controls-visible .keynote-controls,
.keynote-frame:not(.is-playing) .keynote-controls,
.keynote-frame:not(.is-fullscreen):hover .keynote-controls {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

/* ---------- Scrubber ---------- */

.keynote-scrubber {
  position: relative;
  height: 14px;
  display: flex;
  align-items: center;
  cursor: pointer;
  touch-action: none;
}

.keynote-scrubber:focus-visible {
  outline: none;
}

.keynote-scrubber-track {
  position: relative;
  width: 100%;
  height: 2px;
  background: rgb(255 255 255 / 16%);
  border-radius: 1px;
  overflow: hidden;
  transition: height 160ms ease;
}

.keynote-scrubber:hover .keynote-scrubber-track,
.keynote-scrubber:focus-visible .keynote-scrubber-track,
.keynote-scrubber.is-dragging .keynote-scrubber-track {
  height: 4px;
}

.keynote-scrubber-buffered {
  position: absolute;
  inset: 0 auto 0 0;
  background: rgb(255 255 255 / 22%);
  width: 0;
  border-radius: inherit;
}

.keynote-scrubber-progress {
  position: absolute;
  inset: 0 auto 0 0;
  /* Brushed-titanium gradient as the played-portion fill. Matches the
     Z.P.P. mark's metallic sheen. The vertical gradient reads silver
     even on a 2px-tall strip — the bright middle stop is centered. */
  background: var(--player-accent-grad);
  width: 0;
  border-radius: inherit;
  box-shadow: 0 0 8px rgb(216 221 226 / 28%);
}


.keynote-scrubber-thumb {
  position: absolute;
  top: 50%;
  width: 12px;
  height: 12px;
  margin-top: -6px;
  border-radius: 50%;
  background: var(--player-accent-grad);
  box-shadow:
    0 0 0 3px rgb(216 221 226 / 22%),
    0 1px 4px rgb(0 0 0 / 50%);
  opacity: 0;
  transform: translateX(-50%) scale(0.8);
  transition:
    opacity 180ms ease,
    transform 180ms ease;
  pointer-events: none;
}

.keynote-scrubber:hover .keynote-scrubber-thumb,
.keynote-scrubber:focus-visible .keynote-scrubber-thumb,
.keynote-scrubber.is-dragging .keynote-scrubber-thumb {
  opacity: 1;
  transform: translateX(-50%) scale(1);
}

/* Hover-time tooltip — floats above the cursor X on the scrubber.
   Uses left:N% positioning + transform translate(-50%) so it stays
   centered on the pointer. left/right clamping happens in JS via
   the rect math; the visual itself never leaves the scrubber rect. */
.keynote-scrubber-tooltip {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 0;
  transform: translateX(-50%) translateY(4px);
  padding: 0.3rem 0.55rem;
  font-family: var(--font-stack);
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: var(--color-fg);
  background: rgb(0 0 0 / 92%);
  border: 1px solid rgb(216 221 226 / 32%);
  border-radius: 6px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition:
    opacity 140ms ease,
    transform 160ms ease;
  box-shadow: 0 6px 18px rgb(0 0 0 / 50%);
}

.keynote-scrubber-tooltip.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* ---------- Controls row + buttons ---------- */

.keynote-controls-row {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: nowrap;
}

.keynote-ctrl-spacer {
  flex: 1;
  min-width: 0.5rem;
}

/* Project palette: metallic silver at rest (semi-transparent so it sits
   quieter against the video), full saturation on hover/focus/active.
   Same two-tone rest/hover pattern as the rest of the page; the
   currentColor fill in the SVG paths picks the chosen tone. */
.keynote-ctrl-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  background: transparent;
  border: 0;
  color: rgb(216 221 226 / 78%);
  cursor: pointer;
  border-radius: 6px;
  transition:
    color 160ms ease,
    background 160ms ease,
    transform 160ms ease;
}

/* The class rule above (specificity 0,1,0) beats the UA's
   [hidden]{display:none} (0,0,1), which would otherwise be enough.
   Raise specificity to 0,2,0 so JS-set `hidden` actually hides
   leaf control buttons (CC, etc.). */
.keynote-ctrl-btn[hidden] {
  display: none;
}

.keynote-ctrl-btn:hover,
.keynote-ctrl-btn:focus-visible {
  color: var(--player-accent, #d8dde2);
  background: rgb(216 221 226 / 10%);
  outline: none;
}

.keynote-ctrl-btn:active {
  transform: scale(0.94);
}

/* Tiny floating tooltip on icon buttons — uses data-tip so the JS
   doesn't need to render an extra element per button. Hidden until
   hover/focus, fades in just above the icon. Skipped entirely on
   coarse pointers (touch) where tooltips are useless. */
@media (hover: hover) and (pointer: fine) {
  .keynote-ctrl-btn[data-tip]::after {
    content: attr(data-tip);
    position: absolute;
    bottom: calc(100% + 4px);
    left: 50%;
    transform: translateX(-50%) translateY(2px);
    padding: 0.22rem 0.5rem;
    font-family: var(--font-stack-display);
    font-size: 0.52rem;
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--color-fg);
    background: rgb(0 0 0 / 92%);
    border: 1px solid rgb(255 255 255 / 12%);
    border-radius: 4px;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition:
      opacity 160ms ease 60ms,
      transform 160ms ease 60ms;
  }

  .keynote-ctrl-btn[data-tip]:hover::after,
  .keynote-ctrl-btn[data-tip]:focus-visible::after {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}

/* Vertical hairline dividers between control groups — the same
   instrument-panel idiom used on the cockpit dashboard, applied here
   so volume / quality / window controls feel like distinct clusters
   instead of one long row. */
.keynote-ctrl-divider {
  width: 1px;
  height: 18px;
  margin: 0 0.4rem;
  background: rgb(255 255 255 / 16%);
  flex-shrink: 0;
}

.keynote-ctrl-icon {
  width: 20px;
  height: 20px;
  fill: currentcolor;
  display: block;
}

.keynote-ctrl-time {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35rem;
  margin: 0 0.5rem 0 0.15rem;
  font-family: var(--font-stack);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: rgb(245 245 245 / 88%);
  user-select: none;
}

.keynote-ctrl-time-sep {
  opacity: 0.45;
  margin: 0 0.05rem;
}

.keynote-ctrl-time-total {
  color: rgb(245 245 245 / 56%);
}

/* Volume is a single mute-toggle button (HoollyWoody style — no
   slider, users handle fine-grained level via system volume). The
   button picks up its color from the shared .keynote-ctrl-btn rule
   above; nothing extra to style here. */

/* ---------- Quality menu (in-bar) ---------- */

/* Quality / AUTO chooser is hidden to match the iOS native-HLS look — no
   chooser in the control bar. iOS never populates it (native HLS exposes
   no levels); display:none also hides it on the hls.js path (Android /
   desktop Chrome / Firefox) where it would otherwise appear. Re-enable
   by removing the display:none. */
.keynote-ctrl-quality {
  position: relative;
  display: none;
}

.keynote-ctrl-quality-trigger {
  width: auto;
  min-width: 64px;
  padding: 0 0.65rem;
  font-family: var(--font-stack-display);
  font-size: 0.6rem;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
}

.keynote-ctrl-quality-trigger[aria-expanded='true'] {
  color: var(--player-accent, #d8dde2);
  background: rgb(216 221 226 / 12%);
}

/* Suppress the QUALITY data-tip while the dropdown is open — the tip
   sits ~4px above the button and the menu sits ~8px above, so they
   visually overlap in that gap. The menu itself is the affordance the
   user is already looking at, so the tip is redundant noise here. */
.keynote-ctrl-quality-trigger[aria-expanded='true']::after {
  display: none;
}

.keynote-ctrl-quality-menu {
  list-style: none;
  margin: 0;
  padding: 0.35rem;
  min-width: 11rem;
  position: absolute;
  bottom: calc(100% + 8px);
  right: 0;
  background: rgb(0 0 0 / 95%);
  border: 1px solid rgb(255 255 255 / 12%);
  border-radius: 10px;
  backdrop-filter: saturate(140%) blur(14px);
  box-shadow: 0 12px 32px rgb(0 0 0 / 60%);
  z-index: 7;
}

.keynote-ctrl-quality-menu li {
  margin: 0;
}

.keynote-ctrl-quality-item {
  position: relative;
  display: block;
  width: 100%;
  padding: 0.55rem 0.7rem 0.55rem 1.1rem;
  background: transparent;
  border: 0;
  color: var(--color-fg);
  font-family: var(--font-stack-display);
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-align: left;
  border-radius: 6px;
  cursor: pointer;
  transition:
    background 140ms ease,
    color 140ms ease;
}

.keynote-ctrl-quality-item:hover,
.keynote-ctrl-quality-item:focus-visible {
  background: rgb(216 221 226 / 14%);
  color: var(--player-accent, #d8dde2);
  outline: none;
}

/* Active rendition: vertical silver bar on the leading edge — reads
   as a "selected channel" indicator on a mixing console. Cleaner and
   more on-brand than a checkmark or dot. */
.keynote-ctrl-quality-item.is-active {
  color: var(--player-accent, #d8dde2);
  background: rgb(216 221 226 / 8%);
}

.keynote-ctrl-quality-item::before {
  content: '';
  position: absolute;
  left: 0.4rem;
  top: 50%;
  transform: translateY(-50%);
  width: 2px;
  height: 0;
  background: var(--player-accent, #d8dde2);
  border-radius: 1px;
  transition: height 160ms ease;
}

.keynote-ctrl-quality-item.is-active::before {
  height: 60%;
}

/* ---------- Captions toggle (CC) ----------
   .is-active mirrors the [aria-expanded='true'] coloring used on the
   quality/chapters triggers, so all on-state controls read the same. */

.keynote-ctrl-cc.is-active {
  color: var(--player-accent, #d8dde2);
  background: rgb(216 221 226 / 12%);
}

/* Custom-rendered captions. Sits centered along the bottom of the
   player, above the controls bar, in a frosted-glass card matching
   the chapter-title overlay. Inter is the body font on /keynote, so
   captions read as page-native prose rather than a separate UI layer.
   Position drops slightly when controls auto-hide so the card stays
   anchored to the bottom edge of the visible UI. Fullscreen scales
   sizing up and lifts the card a bit higher to clear the larger
   controls bar. */

.keynote-captions {
  position: absolute;
  z-index: 5;
  left: 50%;
  bottom: clamp(4.5rem, 9vh, 5.5rem);
  max-width: min(80%, 36rem);
  padding: 0.55rem 1rem;
  background: rgb(0 0 0 / 68%);
  border: 1px solid rgb(255 255 255 / 6%);
  border-radius: 6px;
  backdrop-filter: blur(10px) saturate(140%);
  box-shadow: 0 6px 20px rgb(0 0 0 / 50%);
  font-family: var(--font-stack-display);
  font-size: clamp(0.92rem, 1.7vw, 1.1rem);
  font-weight: 500;
  line-height: 1.45;
  letter-spacing: 0.005em;
  color: var(--color-fg, #fff);
  text-align: center;
  text-shadow: 0 1px 2px rgb(0 0 0 / 55%);
  white-space: pre-line;
  pointer-events: none;
  opacity: 0;
  transform: translateX(-50%) translateY(4px);
  transition:
    opacity 160ms ease,
    transform 200ms ease,
    bottom 200ms ease;
}

.keynote-captions.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* Slide closer to the edge when the controls bar is hidden — without
   this the captions float in dead space when the user mouses away
   and the timeline auto-hides. The earlier overlap bug from this
   pair of rules was caused by a different issue: the fullscreen
   button retained keyboard focus after click, which blocked the
   auto-hide timer (controls.contains(document.activeElement) guard).
   That's now fixed by blurring the button — see toggleFullscreen
   wiring in keynote.js. */
.keynote-frame:not(.is-controls-visible) .keynote-captions {
  bottom: clamp(1.5rem, 4vh, 2.5rem);
}

/* Fullscreen: bigger type, more breathing room above the larger
   controls bar. */
.keynote-frame.is-fullscreen .keynote-captions {
  bottom: clamp(7rem, 12vh, 9rem);
  font-size: clamp(1.05rem, 2.1vw, 1.4rem);
  padding: 0.7rem 1.2rem;
}

.keynote-frame.is-fullscreen:not(.is-controls-visible) .keynote-captions {
  bottom: clamp(2rem, 5vh, 3.5rem);
}

/* ---------- Hover thumbnail (above scrubber) ----------
   Unified card: image + meta share one dark background, one border,
   one rounded radius, one drop-shadow. The meta strip needs the
   shared backdrop because video content behind the thumbnail can be
   light and white-on-white is unreadable.

   The card has fixed width (162px = 160 image + 2 × 1px border with
   border-box). Half-width = 81px, used by the clamp() below to keep
   the card visually inside the scrubber when the cursor is near
   either edge. The silver scrubber thumb still moves freely with
   the cursor; only the popover clamps. */

.keynote-scrubber-thumbnail {
  position: absolute;
  z-index: 8;
  bottom: calc(100% + 14px);
  left: clamp(81px, var(--scrub-thumb-x, 0%), calc(100% - 81px));
  box-sizing: border-box;
  width: 162px;
  border: 1px solid rgb(216 221 226 / 28%);
  border-radius: 8px;
  filter: drop-shadow(0 8px 18px rgb(0 0 0 / 60%));
  pointer-events: none;
  overflow: hidden;
  opacity: 0;
  transform: translateX(-50%) translateY(4px);
  transition:
    opacity 140ms ease,
    transform 160ms ease;
}

.keynote-scrubber-thumbnail.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

.keynote-scrubber-thumbnail-image {
  width: 160px;
  height: 90px;
  background-color: rgb(0 0 0 / 95%);
  background-repeat: no-repeat;
}

/* Title strip overlays the bottom of the image with a frosted-glass
   backdrop. backdrop-filter blurs whatever is behind it (the bottom
   slice of the thumbnail frame), so the chapter title and timecode
   stay legible on bright frames without obscuring the whole image. */
.keynote-scrubber-thumbnail-meta {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  gap: 0.6rem;
  align-items: baseline;
  justify-content: space-between;
  margin: 0;
  padding: 0.34rem 0.55rem;
  background: linear-gradient(
    180deg,
    rgb(0 0 0 / 0%) 0%,
    rgb(0 0 0 / 70%) 100%
  );
  backdrop-filter: blur(8px) saturate(140%);
  white-space: nowrap;
}

.keynote-scrubber-thumbnail-time {
  flex: 0 0 auto;
  font-family: var(--font-stack);
  font-size: 0.62rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: var(--player-accent, #d8dde2);
}


/* ---------- Debug HUD (Ctrl+Alt+D toggle) ----------
   Hidden by default. Shows what stream the player is actually
   rendering — variant, resolution, time, buffered ahead, state.
   Useful for production diagnostics: confirms ABR pick, surfaces
   stall causes without needing devtools. */

.keynote-debug {
  position: absolute;
  top: clamp(0.75rem, 3vw, 1.5rem);
  right: clamp(0.75rem, 3vw, 1.5rem);
  z-index: 11;
  padding: 0.55rem 0.75rem;
  background: rgb(0 0 0 / 85%);
  border: 1px solid rgb(216 221 226 / 32%);
  border-radius: 6px;
  color: var(--player-accent, #d8dde2);
  font-family: var(--font-stack);
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  line-height: 1.4;
  pointer-events: none;
  white-space: nowrap;
  backdrop-filter: blur(8px);
  text-shadow: 0 1px 2px rgb(0 0 0 / 80%);
}

.keynote-debug b {
  color: var(--color-fg, white);
  font-weight: 600;
}

/* Transport event log under the live stats — wall-clock stamped ring
   buffer (probes, retries, buffer arrivals, hls errors). */
.keynote-debug-log {
  margin-top: 0.4rem;
  padding-top: 0.35rem;
  border-top: 1px solid rgb(216 221 226 / 25%);
  font-size: 0.62rem;
  line-height: 1.35;
  color: rgb(216 221 226 / 80%);
}

/* "Get log" action row — the HUD itself is pointer-events: none so it
   never eats player taps; only the button row re-enables hit testing.
   44px-ish touch target for tablet field tests. */
.keynote-debug-actions {
  margin-top: 0.45rem;
  pointer-events: auto;
}

.keynote-debug-getlog {
  min-height: 2.25rem;
  padding: 0.35rem 0.85rem;
  background: rgb(216 221 226 / 14%);
  border: 1px solid rgb(216 221 226 / 45%);
  border-radius: 4px;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  cursor: pointer;
}

.keynote-debug-getlog:hover,
.keynote-debug-getlog:focus-visible {
  background: rgb(216 221 226 / 26%);
  outline: none;
}

/* ---------- Fullscreen + responsive ---------- */

.keynote-frame.is-fullscreen {
  border-radius: 0;
  border: 0;
  box-shadow: none;
}

.keynote-frame.is-fullscreen .keynote-video {
  object-fit: contain;
}

/* Hide the cursor in fullscreen when the controls auto-hide — without
   this, the mouse pointer sits on the video the whole time and reads
   as visual noise (especially during a keynote where the screen is a
   single-display fullscreen). Standard YouTube / Netflix pattern.
   The cursor returns the moment any movement above MIN_POINTER_MOVE_PX
   re-shows the controls. Placed after .keynote-frame.is-fullscreen
   above so specificity ascends in source order (stylelint
   no-descending-specificity). */
.keynote-frame.is-fullscreen:not(.is-controls-visible),
.keynote-frame.is-fullscreen:not(.is-controls-visible) .keynote-clickcatcher,
.keynote-frame.is-fullscreen:not(.is-controls-visible) .keynote-video {
  cursor: none;
}

@media (width <= 600px) {
  .keynote-controls {
    padding: 0.65rem 0.7rem 0.6rem;
    gap: 0.4rem;
  }

  .keynote-ctrl-btn {
    width: 32px;
    height: 32px;
  }

  .keynote-ctrl-icon {
    width: 18px;
    height: 18px;
  }

  .keynote-ctrl-time {
    font-size: 0.7rem;
    margin-left: 0.05rem;
  }

  .keynote-ctrl-time-total {
    display: none;
  }

  .keynote-ctrl-quality-trigger {
    min-width: 52px;
    font-size: 0.55rem;
    padding: 0 0.5rem;
  }

  .keynote-ctrl-divider {
    margin: 0 0.2rem;
    height: 14px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .keynote-bigplay,
  .keynote-bigplay-ring,
  .keynote-controls,
  .keynote-scrubber-track,
  .keynote-scrubber-thumb,
  .keynote-scrubber-thumbnail,
  .keynote-scrubber-tooltip,
  .keynote-captions,
  .keynote-ctrl-btn,
  .keynote-ctrl-btn[data-tip]::after,
  .keynote-ctrl-quality-item::before,
  .keynote-poster,
  .keynote-poster-lqip,
  .keynote-poster-img {
    transition: none;
  }

  /* Skip the LQIP/blur arc — show the clear poster instantly. */
  .keynote-poster-img {
    opacity: 1;
    filter: saturate(0.95) contrast(1.05);
    transform: none;
  }

  .keynote-poster-lqip {
    opacity: 0;
  }

  .keynote-bigplay:not(.is-hidden) .keynote-bigplay-ring,
  .keynote-player-spinner {
    animation: none;
  }

  .keynote-player-spinner {
    opacity: 0.6;
  }
}

.keynote-frame-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: clamp(1.5rem, 4vw, 2.5rem);
  text-align: center;
  background:
    radial-gradient(ellipse at center, rgb(216 221 226 / 6%) 0%, transparent 60%),
    linear-gradient(180deg, rgb(8 14 26 / 60%) 0%, rgb(0 0 0 / 80%) 100%);
}

.keynote-placeholder-kicker {
  margin: 0;
  font-family: var(--font-stack-display);
  font-size: 0.65rem;
  font-weight: 500;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--player-accent, #d8dde2);
}

.keynote-placeholder-title {
  margin: 0;
  font-family: var(--font-stack-display);
  font-weight: 600;
  font-size: clamp(1.1rem, 2.5vw, 1.5rem);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-fg);
}

.keynote-placeholder-body {
  margin: 0;
  max-width: 32em;
  font-family: var(--font-stack-display);
  font-size: clamp(0.85rem, 1.1vw, 0.95rem);
  line-height: 1.6;
  color: var(--fg-dim);
}

/* ---------- Mobile ---------- */

@media (width <= 720px) {
  .keynote-page {
    padding-top: clamp(6rem, 12vh, 8rem);
  }

  .keynote-card {
    padding: 1.25rem;
  }
}
