/*
 * The introduction film, and the player it opens in.
 *
 * Loaded only by the home page, alongside book.css and intro.css. Everything here hangs off the
 * <dialog data-video> Index.cshtml renders closed: with the stylesheet and without video-intro.js
 * the markup is an inert, invisible dialog, which is the same fallback intro.css already relies on.
 *
 * The open/close transition is *not* here. It is a FLIP -- the player grows out of the rectangle the
 * book's front cover occupies on screen, and only book.js knows where that is -- so the transform
 * keyframes are built at run time by video-intro.js and played through the Web Animations API. What
 * this file owns is where the player comes to rest, the perspective that transition is seen through,
 * and every piece of chrome inside it.
 */

/* ================================================================
 * 1. The dialog, the backdrop, the shell
 * ================================================================ */

.vp {
  /* A <dialog> is centred by its own UA margins; these override that with a box that fills the
     viewport, so .vp__shell can be laid out in the middle of it and measured by the FLIP without
     the dialog's own size changing under the animation. */
  position: fixed;
  inset: 0;
  inline-size: 100%;
  max-inline-size: 100%;
  block-size: 100%;
  max-block-size: 100%;
  margin: 0;
  padding: clamp(var(--space-3), 3vh, var(--space-7));
  border: 0;
  background: transparent;
  overflow: hidden;

  /* The shell arrives out of the book, which is lying at 60 degrees on a desk. Without a perspective
     on the containing element the rotateX video-intro.js animates is an affine squash and reads as a
     rendering fault rather than as a plane turning to face the viewer. */
  perspective: 1800px;
  perspective-origin: 50% 42%;

  direction: rtl;
  color: var(--color-text);
  font-family: var(--font-body);
}

/* `display` is set here and *only* here, behind [open] -- the same shape .intro[open] uses, and for
   a reason that is easy to get wrong. A closed <dialog> is hidden by `dialog:not([open])
   { display: none }` in the user-agent stylesheet, and an author declaration beats a user-agent one
   whatever the specificity says: putting `display: grid` on the bare `.vp` above would override it
   and paint the player over the page from the moment it loads, without showModal() ever being
   called. Which then looks like a second, unrelated bug -- the close button appears dead, because
   dismiss() correctly refuses to close a dialog that was never open. */
.vp[open] {
  display: grid;
  place-items: center;
}

.vp::backdrop {
  background: rgb(4 9 7 / 78%);
  backdrop-filter: blur(14px) saturate(0.85);
  -webkit-backdrop-filter: blur(14px) saturate(0.85);
  animation: vp-backdrop 380ms ease both;
}

.vp[data-closing]::backdrop { animation: vp-backdrop 300ms ease reverse both; }

@keyframes vp-backdrop {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The player itself.
 *
 * The film's own shape first, then bounded by both axes, so it fills whatever the viewport actually
 * gives it: on a wide desktop the height runs out first and the width follows from the ratio; on a
 * phone held upright the width runs out first and the box is short. `min()` on the inline size
 * rather than a media query, because the constraint is continuous and a breakpoint would only
 * approximate it.
 *
 * ---- on 3 / 2, and on the 1176px ----
 *
 * intro.mp4 is 1176x784, which is exactly 3:2. This box was 16/9 to begin with, which is wider, and
 * `object-fit: contain` did the honest thing with the mismatch -- it letterboxed the film with a
 * pillar of black down each side. The two numbers below are the film's, so there is nothing to
 * letterbox: the shell *is* the picture.
 *
 * The cap is the film's own width rather than a round 1280 for the same reason. Past 1176 every
 * extra pixel is upscaled, and a 3:2 film stretched to fill a wide monitor is softer than the same
 * film shown at the size it was made. Raise it if a bigger, slightly softer picture is the better
 * trade on a large screen -- it is the one number here that is a judgement rather than a fact.
 *
 * Both of these follow the file. If intro.mp4 is ever re-encoded at another shape, this rule and the
 * portrait one below are what have to change with it. */
.vp__shell {
  position: relative;
  inline-size: min(100%, 1176px, calc((100vh - 2 * clamp(12px, 3vh, 48px)) * 3 / 2));
  aspect-ratio: 3 / 2;
  max-block-size: 100%;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: #05100b;
  box-shadow:
    0 40px 120px rgb(0 0 0 / 70%),
    0 0 0 1px rgb(255 255 255 / 8%),
    0 0 90px color-mix(in oklab, var(--color-accent) 12%, transparent);
  will-change: transform, opacity;
}

/* Fullscreen is the shell, not the <video> -- taking the element itself would hand the browser's own
   chrome the film and throw every control here away. So the box has to stop being a 16/9 card and
   become the screen; the UA's own `:fullscreen` rules size it, and these are what stop the author
   declarations above from fighting them. */
/* Written twice, not as one selector list: a list containing a pseudo-class the browser does not
   know is invalid *entirely*, so pairing the prefixed one with the standard one would drop this rule
   in every engine that only has the standard one. */
.vp__shell:fullscreen {
  inline-size: 100%;
  block-size: 100%;
  max-block-size: none;
  aspect-ratio: auto;
  border-radius: 0;
  box-shadow: none;
}

.vp__shell:-webkit-full-screen {
  inline-size: 100%;
  block-size: 100%;
  max-block-size: none;
  aspect-ratio: auto;
  border-radius: 0;
  box-shadow: none;
}

/* A phone held upright gives the film the full width and not much of it: 3:2 across a 390px screen
   is 260px tall, and the control bar overlays the bottom third of that. So the shell there is the
   picture's own height *plus* a band -- the film sits in the middle at its true shape, the title
   strip lies in the space above it and the controls in the space below, neither of them on top of
   anything. Deliberately not `aspect-ratio: auto` with a large height: that letterboxes a short film
   inside a tall box, which is the problem this rule exists to avoid rather than cause. */
@media (max-aspect-ratio: 1 / 1) {
  .vp__shell {
    inline-size: 100%;
    aspect-ratio: auto;
    block-size: min(100%, calc(100vw * 2 / 3 + 132px));
  }
}

.vp__video {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: contain;
  background: #05100b;
  cursor: pointer;
}

/* ================================================================
 * 2. The chrome
 *
 * One layer over the film, in three bands: a title strip along the top, the big play button in the
 * middle, and the controls along the bottom. It is a single element so that video-intro.js can fade
 * the whole thing -- in, half a beat behind the box; out, when the pointer has been still for a
 * while and the film is running.
 * ================================================================ */

.vp__chrome {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: auto 1fr auto;
  /* Off, so a click lands on the video underneath and toggles play. Each band turns it back on. */
  pointer-events: none;
  transition: opacity 260ms ease;
  will-change: opacity;
}

.vp[data-idle] .vp__chrome { opacity: 0; }
.vp[data-idle] .vp__video { cursor: none; }

/* ---------- the title strip ---------- */

.vp__top {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  pointer-events: auto;
  background: linear-gradient(to bottom, rgb(3 9 6 / 78%), rgb(3 9 6 / 0%));
}

.vp__title {
  margin: 0;
  font-size: var(--size-h3);
  font-weight: 700;
  line-height: var(--lh-heading);
  color: var(--color-text);
  text-shadow: 0 1px 10px rgb(0 0 0 / 70%);
}

.vp__spacer { flex: 1 1 auto; }

/* ---------- the button in the middle ----------
 *
 * The one control that is not on a bar: it is what a paused film offers, and it is sized to be
 * pressed from across a room. It goes away the moment the film is running, except when autoplay was
 * refused (`data-blocked`), where it is the only way in and stays put. */

.vp__big {
  place-self: center;
  pointer-events: auto;
  display: grid;
  place-items: center;
  inline-size: clamp(64px, 11vh, 104px);
  aspect-ratio: 1;
  padding: 0;
  border: 1px solid rgb(255 255 255 / 22%);
  border-radius: 50%;
  background: rgb(9 20 15 / 62%);
  color: var(--color-text);
  cursor: pointer;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 10px 40px rgb(0 0 0 / 55%);
  transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1), background 200ms ease, opacity 200ms ease;
}

.vp__big svg { inline-size: 42%; block-size: 42%; fill: currentcolor; }

.vp__big:hover,
.vp__big:focus-visible {
  transform: scale(1.09);
  background: color-mix(in oklab, var(--color-accent) 62%, rgb(9 20 15 / 70%));
}

/* Playing, the big button has nothing to say -- the film is the answer. Kept in the layout rather
   than removed so it can fade rather than vanish, and so the middle band stays click-through. */
.vp[data-playing]:not([data-blocked]) .vp__big {
  opacity: 0;
  pointer-events: none;
  transform: scale(0.82);
}

/* ---------- the spinner ---------- */

.vp__wait {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  translate: 50% -50%;
  inline-size: 46px;
  block-size: 46px;
  border-radius: 50%;
  border: 3px solid rgb(255 255 255 / 16%);
  border-block-start-color: var(--color-accent-bright);
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms ease;
}

.vp[data-waiting] .vp__wait { opacity: 1; animation: vp-spin 900ms linear infinite; }

@keyframes vp-spin { to { rotate: 360deg; } }

/* ================================================================
 * 3. The control bar
 * ================================================================ */

.vp__bar {
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-5) var(--space-4);
  background: linear-gradient(to top, rgb(3 9 6 / 88%) 30%, rgb(3 9 6 / 0%));
}

/* ---------- the scrubber ----------
 *
 * Right to left. The bars are laid out with `inline-size` inside a dir="rtl" box, so a fill of 40%
 * starts at the right edge and runs left with the film -- the same way the welcome card's elapsed
 * bar already reads. video-intro.js's pointer maths is the only place the direction is written down.
 *
 * The track is 6px and the grab area is the whole 20px row, so the bar can stay slim without being
 * fiddly to catch -- the same trick .book__rail uses.
 */

.vp__scrub {
  /* Both written by video-intro.js, as plain fractions: how far the film has played, and how far it
     has buffered ahead of that. One custom property drives three boxes -- the fill, the knob riding
     its end, and nothing else has to agree about where "40% through" is. */
  --vp-f: 0;
  --vp-b: 0;

  position: relative;
  display: flex;
  align-items: center;
  block-size: 20px;
  cursor: pointer;
  touch-action: none;
  user-select: none;
}

.vp__scrub:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-pill);
}

.vp__track {
  position: relative;
  inline-size: 100%;
  block-size: 6px;
  border-radius: var(--radius-pill);
  background: rgb(255 255 255 / 16%);
  overflow: hidden;
  transition: block-size 160ms ease;
}

.vp__scrub:hover .vp__track,
.vp__scrub:focus-visible .vp__track,
.vp[data-scrubbing] .vp__track { block-size: 10px; }

.vp__buffered,
.vp__played {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  border-radius: var(--radius-pill);
}

.vp__buffered {
  inline-size: calc(var(--vp-b) * 100%);
  background: rgb(255 255 255 / 22%);
}

.vp__played {
  inline-size: calc(var(--vp-f) * 100%);
  background: linear-gradient(to left, var(--color-accent-bright), var(--color-accent));
  box-shadow: 0 0 12px color-mix(in oklab, var(--color-accent-bright) 45%, transparent);
}

/* The knob rides the end of the played bar. `inset-inline-start` resolves from the right under
   dir="rtl", which is the edge the fill starts at, so the same fraction places both with no
   mirroring anywhere -- and the translate is logical too, hence the positive 50%. */
.vp__knob {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: calc(var(--vp-f) * 100%);
  inline-size: 14px;
  block-size: 14px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 6px rgb(0 0 0 / 60%);
  transform: translate(50%, -50%) scale(0);
  transition: transform 160ms cubic-bezier(0.16, 1, 0.3, 1);
  pointer-events: none;
}

.vp__scrub:hover .vp__knob,
.vp__scrub:focus-visible .vp__knob,
.vp[data-scrubbing] .vp__knob { transform: translate(50%, -50%) scale(1); }

/* ---------- the row of controls ---------- */

.vp__row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.vp__btn {
  display: grid;
  place-items: center;
  inline-size: 38px;
  block-size: 38px;
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-text);
  font-family: inherit;
  font-size: var(--size-sm);
  font-weight: 600;
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease, transform 160ms ease;
}

.vp__btn svg { inline-size: 20px; block-size: 20px; fill: currentcolor; }

.vp__btn:hover,
.vp__btn:focus-visible {
  background: rgb(255 255 255 / 12%);
  color: var(--color-accent-bright);
}

.vp__btn:active { transform: scale(0.93); }

/* The speed button carries text rather than an icon, so it needs room for "۱٫۲۵×". */
.vp__btn--speed {
  inline-size: auto;
  min-inline-size: 46px;
  padding-inline: var(--space-2);
  font-variant-numeric: tabular-nums;
}

.vp__time {
  flex: 0 0 auto;
  padding-inline: var(--space-2);
  font-size: var(--size-sm);
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.4px;
  white-space: nowrap;
}

/* ---------- the two play/pause glyphs ----------
 *
 * Both are in the markup and one is shown, rather than a path being rewritten in script: the state
 * lives on the dialog (`data-playing`), and every part of the chrome that answers to it should read
 * off the same attribute. Same pattern intro.css uses for the welcome card's toggle.
 */
.vp__icon--pause { display: none; }
.vp[data-playing] .vp__icon--play { display: none; }
.vp[data-playing] .vp__icon--pause { display: block; }

.vp__icon--unmute { display: none; }
.vp[data-muted] .vp__icon--mute { display: none; }
.vp[data-muted] .vp__icon--unmute { display: block; }

.vp__icon--shrink { display: none; }
.vp[data-fullscreen] .vp__icon--grow { display: none; }
.vp[data-fullscreen] .vp__icon--shrink { display: block; }

/* ---------- volume ----------
 *
 * Collapsed to nothing until the sound button is reached for, so the bar stays four small controls
 * and a clock rather than a console. Width, not visibility: the slider stays focusable, so a
 * keyboard reader tabs straight onto it whether or not a pointer has ever been near.
 */

.vp__sound {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
}

.vp__volume {
  inline-size: 0;
  opacity: 0;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  transition: inline-size 200ms cubic-bezier(0.16, 1, 0.3, 1), opacity 200ms ease;
  accent-color: var(--color-accent-bright);
}

.vp__sound:hover .vp__volume,
.vp__sound:focus-within .vp__volume {
  inline-size: 84px;
  opacity: 1;
  margin-inline-start: var(--space-1);
}

/* ================================================================
 * 4. The fallback opener
 *
 * The labelled, focusable way into the film. On a page that built the 3D book it is clipped out of
 * sight -- book.css does that -- because the cover itself is the control there and book.js calls
 * the player directly. On every path that leaves the plain contents list standing (no WebGL, a
 * failed three.js import) this is a real, visible button under the welcome cover.
 * ================================================================ */

.toc__video[hidden] { display: none; }

.toc__video {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  inline-size: fit-content;
  margin-inline: auto;
  margin-block-start: var(--space-4);
  padding: var(--space-2) var(--space-5);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text);
  font-family: inherit;
  font-size: var(--size-base);
  font-weight: 600;
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
}

.toc__video svg { inline-size: 18px; block-size: 18px; fill: var(--color-accent-bright); }

.toc__video:hover,
.toc__video:focus-visible {
  background: var(--color-surface-raised);
  border-color: var(--color-accent-strong);
  transform: translateY(-1px);
}

/* ================================================================
 * 5. Reduced motion
 *
 * video-intro.js already skips the FLIP outright when this is set -- the player is simply there, and
 * gone. What is left to turn off is everything CSS animates on its own: the backdrop's fade, the
 * spinner, and the small hover movements.
 * ================================================================ */

@media (prefers-reduced-motion: reduce) {
  .vp::backdrop,
  .vp[data-closing]::backdrop { animation: none; }

  .vp__chrome,
  .vp__track,
  .vp__knob,
  .vp__big,
  .vp__btn,
  .vp__volume,
  .vp__wait,
  .toc__video { transition: none; }

  .vp[data-waiting] .vp__wait { animation: none; }

  .vp__big:hover,
  .vp__big:focus-visible { transform: none; }

  .vp__btn:active,
  .toc__video:hover,
  .toc__video:focus-visible { transform: none; }

  /* The controls must not fade out on their own when motion is unwelcome: the reader has no way to
     know they are still there. */
  .vp[data-idle] .vp__chrome { opacity: 1; }
  .vp[data-idle] .vp__video { cursor: pointer; }
}
