/* ===========================================================================
   How AMBE Works — animation figure chrome
   ---------------------------------------------------------------------------
   Presentation only. Nothing here knows anything about AMBE; it is the frame
   around a canvas, a video, or an image, plus the controls that drive it.
   Every colour comes from a token defined in extra.css.

   Markup contract (animation authors: copy this shape)
   ---------------------------------------------------------------------------
   <figure class="anim-figure" data-anim="pitch-track">
     <div class="anim-figure__head">
       <p class="anim-figure__title">Pitch track over one utterance</p>
     </div>

     <div class="anim-figure__scroll">          <!-- optional: wide plots -->
       <div class="anim-figure__frame" style="--anim-aspect: 16 / 9">
         <canvas class="anim-figure__canvas"></canvas>
       </div>
     </div>

     <ul class="anim-legend">
       <li class="anim-legend__item"><i class="anim-legend__swatch is-data-1"></i>voiced</li>
       <li class="anim-legend__item"><i class="anim-legend__swatch is-data-2"></i>unvoiced</li>
     </ul>

     <div class="anim-controls">
       <button class="anim-btn anim-btn--play" type="button" aria-pressed="false">Play</button>
       <label class="anim-field">
         <span class="anim-field__label">Frame</span>
         <input class="anim-range" type="range" min="0" max="0" step="1" value="0">
       </label>
       <output class="anim-readout">0.00 s</output>
     </div>

     <p class="anim-figure__hint">Animation held on a static frame because your
        system asks for reduced motion. Use the slider to step through it.</p>

     <figcaption class="anim-figure__caption">
       What the reader is looking at.
       <span class="anim-figure__source">Source: ...</span>
     </figcaption>
   </figure>

   For a manim clip, swap the frame contents for:
     <video class="anim-video" controls playsinline preload="none"
            poster="../assets/video/foo.jpg">
       <source src="../assets/video/foo.mp4" type="video/mp4">
     </video>

   MIT licensed (see LICENSE-MIT).
   =========================================================================== */

/* ---------------------------------------------------------------------------
   Figure surface
   ------------------------------------------------------------------------ */

.md-typeset .anim-figure,
.anim-figure {
  background-color: var(--ambe-surface-1);
  border: 1px solid var(--ambe-hairline);
  border-radius: var(--ambe-radius-lg);
  box-shadow: var(--ambe-shadow-1);
  color: var(--ambe-text-2);
  contain: layout paint;
  display: block;
  /* zensical's `.md-typeset figure` is `width: fit-content; margin: auto;
     text-align: center` — none of which suits a full-width instrument. */
  margin: 2.2rem 0;
  max-width: var(--ambe-measure-wide);
  padding: 0.9rem;
  text-align: left;
  width: 100%;
}

/* Opt-in full-bleed within the content column, for wide spectrograms.
   Note the `.md-typeset` prefix throughout this file: base rules have to
   match zensical's own specificity, so modifiers must match it too. */
.md-typeset .anim-figure--wide,
.anim-figure--wide {
  max-width: 100%;
}

.anim-figure__head {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.7rem;
}

.md-typeset .anim-figure__title,
.anim-figure__title {
  color: var(--ambe-text);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: -0.008em;
  line-height: 1.3;
  margin: 0;
  border: 0;
  padding: 0;
}

.md-typeset .anim-figure__subtitle,
.anim-figure__subtitle {
  color: var(--ambe-text-4);
  font-size: 0.62rem;
  margin: 0;
}

/* ---------------------------------------------------------------------------
   Plot area
   The canvas is sized entirely by CSS; anim-core.js matches the backing store
   to the CSS box at devicePixelRatio. Authors set --anim-aspect (default 16/9)
   and, for plots that must not squash, --anim-min-width to trigger internal
   horizontal scrolling instead of page-level scrolling.
   ------------------------------------------------------------------------ */

.anim-figure__scroll {
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--ambe-border) transparent;
  -webkit-overflow-scrolling: touch;
}

.anim-figure__frame {
  aspect-ratio: var(--anim-aspect, 16 / 9);
  background-color: var(--ambe-plot-bg);
  border: 1px solid var(--ambe-hairline);
  border-radius: var(--ambe-radius-md);
  min-width: var(--anim-min-width, 0);
  overflow: hidden;
  position: relative;
  width: 100%;
}

.anim-figure__canvas,
.anim-figure__frame > canvas {
  display: block;
  height: 100%;
  width: 100%;
}

/* An overlay layer for HTML labels pinned over the canvas (crisper text than
   canvas fillText for anything the reader must select or a screen reader must
   see). Position children absolutely inside it. */
.anim-figure__overlay {
  inset: 0;
  pointer-events: none;
  position: absolute;
}

.anim-figure__overlay > * {
  pointer-events: auto;
}

/* Loading / empty state while precomputed JSON is in flight. */
.anim-figure__frame[data-state="loading"]::after {
  align-items: center;
  color: var(--ambe-text-4);
  content: "Loading data…";
  display: flex;
  font-size: 0.62rem;
  inset: 0;
  justify-content: center;
  position: absolute;
}

.anim-figure__frame[data-state="error"]::after {
  align-items: center;
  color: var(--ambe-accent-warm);
  content: "This figure's data failed to load.";
  display: flex;
  font-size: 0.62rem;
  inset: 0;
  justify-content: center;
  position: absolute;
}

/* ---------------------------------------------------------------------------
   Video figures (manim clips)
   ------------------------------------------------------------------------ */

.md-typeset .anim-video,
.anim-video {
  background-color: var(--ambe-sunken);
  border: 1px solid var(--ambe-hairline);
  border-radius: var(--ambe-radius-md);
  display: block;
  height: auto;
  margin: 0;
  max-width: 100%;
  width: 100%;
  aspect-ratio: var(--anim-aspect, 16 / 9);
  object-fit: contain;
}

/* A bare <video> dropped straight into prose still behaves. */
.md-typeset video:not(.anim-video) {
  border-radius: var(--ambe-radius-md);
  max-width: 100%;
  height: auto;
}

.anim-video:focus-visible {
  outline: none;
  box-shadow: var(--ambe-focus-ring);
}

/* ---------------------------------------------------------------------------
   Legend
   ------------------------------------------------------------------------ */

.md-typeset .anim-legend,
.anim-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  list-style: none;
  margin: 0.7rem 0 0;
  padding: 0;
}

.md-typeset .anim-legend__item,
.anim-legend__item {
  align-items: center;
  color: var(--ambe-text-3);
  display: flex;
  font-size: 0.62rem;
  gap: 0.35rem;
  margin: 0;
}

.anim-legend__swatch {
  background-color: var(--ambe-data-4);
  border-radius: 2px;
  display: inline-block;
  flex: none;
  height: 0.5rem;
  width: 0.5rem;
}

.anim-legend__swatch.is-data-1 { background-color: var(--ambe-data-1); }
.anim-legend__swatch.is-data-2 { background-color: var(--ambe-data-2); }
.anim-legend__swatch.is-data-3 { background-color: var(--ambe-data-3); }
.anim-legend__swatch.is-data-4 { background-color: var(--ambe-data-4); }

/* Line-style swatches, for series drawn as strokes rather than fills. */
.anim-legend__swatch--line {
  border-radius: 999px;
  height: 2px;
  width: 0.9rem;
}

.anim-legend__swatch--dashed {
  background: repeating-linear-gradient(
    90deg,
    currentColor 0 4px,
    transparent 4px 7px
  );
  border-radius: 0;
  height: 2px;
  width: 0.9rem;
}

/* The ghosted comparison series: vertical dashes, matching the faint dashed
   stems the canvas draws for the voice the reader did not select. */
.anim-legend__swatch--ghost {
  background: repeating-linear-gradient(
    90deg,
    var(--ambe-plot-muted) 0 1px,
    transparent 1px 4px
  );
  border-radius: 0;
  height: 0.5rem;
  opacity: 0.7;
  width: 0.9rem;
}

/* ---------------------------------------------------------------------------
   Two-voice comparison strip

   Shown under the harmonics figure once a voice is selected. The canvas makes
   the difference visible; this makes it readable — and gives a screen reader
   something better than a picture of two combs.
   ------------------------------------------------------------------------ */

.anim-compare {
  border: 1px solid var(--ambe-hairline);
  border-radius: var(--ambe-radius-md);
  margin-top: 0.7rem;
  padding: 0.55rem 0.7rem;
}

.anim-compare[hidden] {
  display: none;
}

.anim-compare__row {
  align-items: center;
  color: var(--ambe-text-3);
  display: grid;
  font-size: 0.62rem;
  gap: 0 0.6rem;
  grid-template-columns: 4.2rem 3.6rem 4rem 1fr;
}

.anim-compare__row + .anim-compare__row {
  margin-top: 0.3rem;
}

/* The selected voice is the one the solid comb belongs to. */
.anim-compare__row.is-current {
  color: var(--ambe-text);
  font-weight: 600;
}

.anim-compare__f0,
.anim-compare__l {
  font-family: var(--ambe-font-mono);
  font-variant-numeric: tabular-nums;
}

.anim-compare__bar {
  background: var(--ambe-sunken);
  border-radius: var(--ambe-radius-pill);
  display: block;
  height: 0.35rem;
  overflow: hidden;
  width: 100%;
}

.anim-compare__bar > i {
  background: var(--ambe-data-4);
  border-radius: inherit;
  display: block;
  height: 100%;
  transition: width var(--ambe-dur-med) var(--ambe-ease);
  width: 0;
}

.anim-compare__row.is-current .anim-compare__bar > i {
  background: var(--ambe-data-1);
}

.md-typeset .anim-compare__delta,
.anim-compare__delta {
  border-top: 1px solid var(--ambe-hairline);
  color: var(--ambe-text-2);
  font-size: 0.62rem;
  line-height: 1.5;
  margin: 0.5rem 0 0;
  padding-top: 0.45rem;
}

/* On a phone the three number columns already consume the row: the bar column
   collapses to a few pixels and reads as a rendering artefact rather than a
   comparison. Drop it and keep the numbers, which carry the point anyway. */
@media screen and (max-width: 30em) {
  .anim-compare__row {
    grid-template-columns: 1fr auto auto;
  }

  .anim-compare__bar {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .anim-compare__bar > i {
    transition: none;
  }
}

/* ---------------------------------------------------------------------------
   Controls: transport, sliders, toggles
   ------------------------------------------------------------------------ */

.anim-controls {
  align-items: center;
  border-top: 1px solid var(--ambe-hairline);
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.7rem;
  margin-top: 0.8rem;
  padding-top: 0.7rem;
}

.anim-controls__group {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* -- Buttons ----------------------------------------------------------- */

.md-typeset .anim-btn,
.anim-btn {
  appearance: none;
  background-color: var(--ambe-surface-2);
  border: 1px solid var(--ambe-border);
  border-radius: var(--ambe-radius-pill);
  color: var(--ambe-text-2);
  cursor: pointer;
  flex: none;
  font: inherit;
  font-size: 0.62rem;
  font-weight: 550;
  letter-spacing: -0.004em;
  line-height: 1;
  padding: 0.4rem 0.8rem;
  transition:
    background-color var(--ambe-dur-fast) var(--ambe-ease),
    border-color var(--ambe-dur-fast) var(--ambe-ease),
    color var(--ambe-dur-fast) var(--ambe-ease);
  user-select: none;
}

.anim-btn:hover {
  background-color: var(--ambe-surface-3);
  border-color: var(--ambe-border-strong);
  color: var(--ambe-text);
}

.anim-btn:active {
  transform: translateY(0.5px);
}

.anim-btn:focus-visible {
  outline: none;
  box-shadow: var(--ambe-focus-ring);
}

.anim-btn[disabled],
.anim-btn[aria-disabled="true"] {
  cursor: default;
  opacity: 0.45;
}

/* Primary transport button (play/pause). anim-core.js flips aria-pressed. */
.md-typeset .anim-btn--play,
.anim-btn--play {
  background-color: var(--ambe-accent-soft);
  border-color: color-mix(in srgb, var(--ambe-accent) 45%, transparent);
  color: var(--ambe-accent);
  min-width: 4.4rem;
}

.md-typeset .anim-btn--play:hover,
.anim-btn--play:hover {
  background-color: color-mix(in srgb, var(--ambe-accent) 22%, transparent);
  border-color: var(--ambe-accent);
  color: var(--ambe-accent-hover);
}

/* Segmented toggles: a row of .anim-btn with aria-pressed. */
.md-typeset .anim-btn[aria-pressed="true"],
.anim-btn[aria-pressed="true"] {
  background-color: var(--ambe-accent-soft);
  border-color: var(--ambe-accent);
  color: var(--ambe-accent);
}

.anim-toggle-group {
  background-color: var(--ambe-surface-2);
  border: 1px solid var(--ambe-hairline);
  border-radius: var(--ambe-radius-pill);
  display: inline-flex;
  gap: 2px;
  padding: 2px;
}

.md-typeset .anim-toggle-group .anim-btn,
.anim-toggle-group .anim-btn {
  background-color: transparent;
  border-color: transparent;
  padding: 0.3rem 0.7rem;
}

.md-typeset .anim-toggle-group .anim-btn[aria-pressed="true"],
.anim-toggle-group .anim-btn[aria-pressed="true"] {
  background-color: var(--ambe-surface-3);
  border-color: transparent;
  color: var(--ambe-text);
}

/* -- Fields and sliders ------------------------------------------------ */

.md-typeset .anim-field,
.anim-field {
  align-items: center;
  display: flex;
  flex: 1 1 12rem;
  gap: 0.5rem;
  margin: 0;
  min-width: 8rem;
}

.anim-field__label {
  color: var(--ambe-text-4);
  flex: none;
  font-size: 0.6rem;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.md-typeset .anim-range,
.anim-range {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  cursor: pointer;
  flex: 1 1 auto;
  height: 1rem;
  margin: 0;
  min-width: 5rem;
  padding: 0;
  width: 100%;
}

.anim-range:focus-visible {
  outline: none;
}

.anim-range::-webkit-slider-runnable-track {
  background-color: var(--ambe-surface-3);
  border-radius: 999px;
  height: 3px;
}

.anim-range::-moz-range-track {
  background-color: var(--ambe-surface-3);
  border-radius: 999px;
  height: 3px;
}

.anim-range::-moz-range-progress {
  background-color: var(--ambe-accent);
  border-radius: 999px;
  height: 3px;
}

.anim-range::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  background-color: var(--ambe-text);
  border: 0;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
  height: 0.62rem;
  margin-top: calc((0.62rem - 3px) / -2);
  transition: transform var(--ambe-dur-fast) var(--ambe-ease);
  width: 0.62rem;
}

.anim-range::-moz-range-thumb {
  background-color: var(--ambe-text);
  border: 0;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
  height: 0.62rem;
  width: 0.62rem;
}

.anim-range:hover::-webkit-slider-thumb,
.anim-range:active::-webkit-slider-thumb {
  transform: scale(1.15);
}

.anim-range:focus-visible::-webkit-slider-thumb {
  box-shadow: var(--ambe-focus-ring);
}

.anim-range:focus-visible::-moz-range-thumb {
  box-shadow: var(--ambe-focus-ring);
}

/* Checkbox/radio options inside a control row. */
.anim-check {
  accent-color: var(--ambe-accent);
  margin: 0 0.3rem 0 0;
}

/* -- Readout ----------------------------------------------------------- */

.md-typeset .anim-readout,
.anim-readout {
  color: var(--ambe-text-3);
  flex: none;
  font-family: var(--ambe-font-mono);
  font-size: 0.6rem;
  font-variant-numeric: tabular-nums;
  min-width: 5.5ch;
  text-align: right;
}

.md-typeset .anim-readout--wide,
.anim-readout--wide {
  min-width: 12ch;
}

.anim-readout em {
  color: var(--ambe-text);
  font-style: normal;
}

/* ---------------------------------------------------------------------------
   Caption, source line, reduced-motion hint
   ------------------------------------------------------------------------ */

.md-typeset .anim-figure__caption,
.anim-figure__caption {
  color: var(--ambe-text-3);
  font-size: 0.64rem;
  font-style: normal;
  line-height: 1.6;
  margin: 0.8rem 0 0;
  max-width: var(--ambe-measure);
  text-align: left;
}

.anim-figure__caption > :last-child {
  margin-bottom: 0;
}

/* Every figure names where its data came from. */
.md-typeset .anim-figure__source,
.anim-figure__source {
  color: var(--ambe-text-4);
  display: block;
  font-size: 0.92em;
  margin-top: 0.3em;
}

/* -- Narration transcript ----------------------------------------------
 *
 * The text of what a video says, under the video. Generated by
 * tools/make-transcripts.py from the same scripts the narration is
 * synthesised from, so the two cannot drift.
 *
 * It is collapsed by default because it is long and most readers will play
 * the clip instead. It is not a substitute for the caption track, which is
 * still shipped and still checked by CI; it is the alternative that does not
 * cover the picture, which matters because these scenes use the whole frame.
 */

.md-typeset .anim-transcript,
.anim-transcript {
  border-top: 1px solid var(--ambe-hairline);
  margin-top: 0.8rem;
  padding-top: 0.6rem;
}

.md-typeset .anim-transcript > summary,
.anim-transcript > summary {
  color: var(--ambe-text-3);
  cursor: pointer;
  font-size: 0.64rem;
  list-style: none;
}

.anim-transcript > summary::-webkit-details-marker {
  display: none;
}

/* A caret the reader can see, since the native marker is suppressed. */
.anim-transcript > summary::before {
  content: "▸";
  display: inline-block;
  margin-right: 0.4em;
  transition: transform var(--ambe-dur-fast) var(--ambe-ease);
}

.anim-transcript[open] > summary::before {
  transform: rotate(90deg);
}

.anim-transcript > summary:hover {
  color: var(--ambe-text-2);
}

.anim-transcript > summary:focus-visible {
  outline: 2px solid var(--ambe-accent);
  outline-offset: 2px;
}

.anim-transcript__body {
  color: var(--ambe-text-3);
  font-size: 0.64rem;
  line-height: 1.7;
  margin-top: 0.5rem;
  max-width: var(--ambe-measure);
}

.md-typeset .anim-transcript__body p,
.anim-transcript__body p {
  margin: 0 0 0.6em;
}

.anim-transcript__body p:last-child {
  margin-bottom: 0;
}

/* Shown only when the reader asks for reduced motion. */
.md-typeset .anim-figure__hint,
.anim-figure__hint {
  color: var(--ambe-text-4);
  display: none;
  font-size: 0.6rem;
  line-height: 1.5;
  margin: 0.6rem 0 0;
}

/* ---------------------------------------------------------------------------
   Reduced motion
   Figures hold on a representative static frame (anim-core.js does not start
   the rAF loop); the hint appears and the transport stays usable so the
   reader can step through by hand.
   ------------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  /* `.md-typeset` prefix required: the base rule that hides the hint is
     `.md-typeset .anim-figure__hint`, so an unprefixed override loses. */
  .md-typeset .anim-figure__hint,
  .anim-figure__hint {
    display: block;
  }

  .md-typeset .anim-btn--play,
  .anim-btn--play {
    display: none;
  }

  .anim-video {
    /* Any autoplay attribute is also stripped by anim-core.js. */
    animation: none;
  }
}

/* JS can force the same presentation with .is-static on the figure. */
.md-typeset .anim-figure.is-static .anim-btn--play,
.anim-figure.is-static .anim-btn--play {
  display: none;
}

.md-typeset .anim-figure.is-static .anim-figure__hint,
.anim-figure.is-static .anim-figure__hint {
  display: block;
}

/* ---------------------------------------------------------------------------
   Small screens
   ------------------------------------------------------------------------ */

@media screen and (max-width: 44.9375em) {
  .anim-figure {
    border-left: 0;
    border-radius: var(--ambe-radius-md);
    border-right: 0;
    margin-inline: -0.4rem;
    padding: 0.7rem;
  }

  .anim-controls {
    gap: 0.45rem;
  }

  .anim-field {
    flex-basis: 100%;
  }

  .anim-readout {
    text-align: left;
  }
}

/* ---------------------------------------------------------------------------
   Print: keep the static frame and the caption, drop the transport.
   ------------------------------------------------------------------------ */

@media print {
  .anim-controls,
  .anim-figure__hint {
    display: none;
  }

  .anim-figure {
    box-shadow: none;
    break-inside: avoid;
  }
}

/* ---------------------------------------------------------------------------
   Three-forms readout

   Used by the complex-plane figure, where the point on screen is written three
   ways at once and the whole argument is that the three never disagree. Laid
   out as labelled rows rather than a paragraph so the eye can check them
   against each other.
   ------------------------------------------------------------------------ */

.anim-forms {
  border: 1px solid var(--ambe-hairline);
  border-radius: var(--ambe-radius-md);
  margin-top: 0.7rem;
  padding: 0.5rem 0.7rem;
}

.md-typeset .anim-forms__row,
.anim-forms__row {
  align-items: baseline;
  display: grid;
  gap: 0 0.7rem;
  grid-template-columns: 9.5rem 1fr;
  margin: 0;
}

.anim-forms__row + .anim-forms__row {
  margin-top: 0.3rem;
}

.anim-forms__tag {
  color: var(--ambe-text-4);
  font-size: 0.6rem;
  text-align: right;
}

.md-typeset .anim-forms code,
.anim-forms code {
  background: none;
  border: 0;
  color: var(--ambe-text);
  font-family: var(--ambe-font-mono);
  font-size: 0.66rem;
  font-variant-numeric: tabular-nums;
  padding: 0;
}

/* The label column costs more than it earns on a phone: stack instead.
 *
 * `.md-typeset` has to be carried into this block. A media query adds no
 * specificity, so a bare `.anim-forms__row` here loses to the
 * `.md-typeset .anim-forms__row` rule above and the columns never collapse.
 * That left the readouts on a phone as a 190px label beside a 70px value,
 * breaking every reading onto one word per line. */
@media screen and (max-width: 30em) {
  .md-typeset .anim-forms__row,
  .anim-forms__row {
    grid-template-columns: 1fr;
  }

  .md-typeset .anim-forms__tag,
  .anim-forms__tag {
    text-align: left;
  }
}
