/*
 * The welcome.
 *
 * A visitor's first arrival opens with one card: a painting, and a voice reading over it. It is the
 * only thing on screen while it is up, so it is a real <dialog> shown modally -- the page behind it
 * is inert, Escape closes it, and focus cannot wander out into a table of contents nobody is looking
 * at yet.
 *
 * Everything here styles a box that is already in the document and already hidden. intro.js opens it
 * and never builds it, so a visitor without JavaScript -- or with a script that failed to arrive --
 * simply lands on the contents, which is the page they came for anyway.
 *
 * The card is the image. There is no panel around the picture with the picture inside it: the frame
 * *is* the artwork, at its own 1500x856, with the controls floated over the bottom of it under a
 * scrim. The image carries transparency, so what shows through it is the surface colour rather than
 * white -- which is why the frame is painted at all.
 */

/* ================================================================
 * The book over the wheel
 *
 * The one thing on the landing page that is neither a title nor a link: the cover of the book, held
 * one row above the first title, and the way back into the welcome once the session's automatic
 * opening has been spent.
 *
 * It is styled here rather than in cylinder.css because it belongs to the card, not to the cylinder
 * -- intro.js still owns everything the button does when pressed. cylinder.js only reads its size
 * and writes the same rotateX/translateZ/opacity a title would carry, so the cover rides the wheel
 * and fades into the top of the stage exactly as a title rotating past that point would, rather than
 * hanging over it as a fixture. It is excluded from the stage's own pointerdown handling (a
 * cover.contains() guard next to the one the jump rail already has), so a press on it is still never
 * also the start of a drag of the cylinder.
 * ================================================================ */

/* `display` from the rule below would otherwise beat the UA sheet's `[hidden] { display: none }`,
   and the book would be on screen before the script that gives it something to do. */
.toc__cover[hidden] { display: none; }

.toc__cover {
  display: block;
  margin-inline: auto;
  margin-block: var(--space-5) 0;
  padding: 0;
  border: 0;
  background: none;
  inline-size: clamp(112px, 17vh, 200px);
  cursor: pointer;
  /* The wheel is a wall of Persian titles at every size; the cover has to sit in front of it and be
     seen to. A drop-shadow rather than a box-shadow, because the artwork carries transparency and a
     box shadow would draw the square it is delivered in. */
  filter:
    drop-shadow(0 18px 30px rgb(0 0 0 / 55%))
    drop-shadow(0 0 18px color-mix(in oklab, var(--color-accent-bright) 28%, transparent));
  transition: transform 0.35s cubic-bezier(0.2, 0.8, 0.25, 1), filter 0.35s ease;
}

/* Anchored on the same axis point every title in the wheel is: the centre of .toc__stage. A title
   gets there by sitting inside .toc__list, which is translated to that point once; the cover is not
   a list item, so it carries the identical translate(50%, -50%) itself before cylinder.js adds the
   rotateX/translateZ that puts it one row above the first title. See renderCover() in cylinder.js.

   Only under the shell. With no cylinder there is no stage to ride, and the cover is a centred block
   above the list it introduces. */
.shell-home .toc__cover {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  z-index: 4;
  margin: 0;
  transform: translate(50%, -50%);
  transform-style: preserve-3d;
  backface-visibility: hidden;
  will-change: transform, opacity;
  /* transform is rewritten every animation frame by cylinder.js; a CSS transition on it would
     always be racing the last frame rather than the current one. Opacity is the one property left
     for this stylesheet to animate -- cylinder.js only ever sets a bare number on it. */
  transition: filter 0.35s ease, opacity 0.2s ease;
}

/*
 * A slow rise and fall, so the cover reads as suspended over the wheel rather than pinned to it --
 * and so that something on the page is still moving when the cylinder is not.
 *
 * On the image rather than on the button, and that is the whole reason there are two elements here.
 * An animated property beats every ordinary declaration in the cascade, so a float and a hover lift
 * written against the same element are not composed -- the float simply wins, and the hover does
 * nothing. Split across the two, they stack: the button lifts, the image goes on breathing inside it.
 */
.toc__cover-image {
  display: block;
  inline-size: 100%;
  block-size: auto;
  animation: toc-cover-float 7s ease-in-out infinite;
}

@keyframes toc-cover-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-9px); }
}

/* Held still while the pointer is on it. The lift is the answer to the pointer, and a book still
   drifting inside a button that has stopped reads as two separate movements. */
.toc__cover:hover .toc__cover-image,
.toc__cover:focus-visible .toc__cover-image { animation-play-state: paused; }

.toc__cover:hover,
.toc__cover:focus-visible {
  transform: translateY(-7px) scale(1.06);
  filter:
    drop-shadow(0 26px 40px rgb(0 0 0 / 60%))
    drop-shadow(0 0 30px color-mix(in oklab, var(--color-accent-bright) 55%, transparent));
}

.toc__cover:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus);
  outline-offset: calc(var(--focus-ring-offset) + 4px);
  border-radius: var(--radius-sm);
}

.toc__cover:active { transition-duration: 0.08s; }

.toc__cover:active { transform: translateY(-2px) scale(1.01); }

/* Under the shell, cylinder.js writes transform every frame (see renderCover()), so these three only
   apply outside it -- the resting, non-enhanced page. */
@media (prefers-reduced-motion: reduce) {
  .toc__cover-image { animation: none; }

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

/* ================================================================
 * The card
 * ================================================================ */

.intro {
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  /* Under the artwork's transparent regions, and behind it while it is still arriving. The tint
     leans on the accent so the card reads as lit from within rather than as a hole in the page. */
  background:
    radial-gradient(120% 90% at 50% 0%, color-mix(in oklab, var(--color-accent-soft) 55%, transparent), transparent 70%),
    var(--color-surface);
  color: var(--color-text);
  overflow: hidden;

  /* Sized from the picture, not from a guess: the natural ratio means `cover` below never actually
     crops anything on a wide window. The block cap is what a short one enforces instead, and there
     the ratio gives way rather than the card growing past the fold. */
  inline-size: min(62rem, calc(100vw - 2 * var(--space-5)));
  max-inline-size: none;
  aspect-ratio: 1500 / 856;
  max-block-size: calc(100dvh - 2 * var(--space-6));

  box-shadow:
    0 50px 140px -40px rgb(0 0 0 / 85%),
    0 0 90px -50px var(--color-accent-bright);
}

/* On the attribute rather than the element: the UA rule hiding a closed <dialog> sits in a weaker
   origin than this file, so an unqualified `display` would leave the card on screen when shut. */
.intro[open] { display: block; }

.intro::backdrop {
  background: rgb(3 6 5 / 82%);
  backdrop-filter: blur(10px);
}

/* ---------- the artwork ---------- */

.intro__image {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* The drift below scales the image up; without this the growth would be measured from a corner
     and the picture would slide off one edge instead of breathing in place. */
  transform-origin: center;
}

/* A scrim only where the controls sit. The painting keeps its own contrast everywhere else, and a
   veil across the whole card to make one small button legible would be paying for the button with
   the picture. */
.intro__scrim {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: 34%;
  pointer-events: none;
  background: linear-gradient(to top, rgb(4 9 7 / 88%) 0%, rgb(4 9 7 / 55%) 45%, transparent 100%);
}

/* The light crossing the card once as it opens, and gone. It is what separates "a box appeared"
   from "something was unveiled". */
.intro__sheen {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  background-image: linear-gradient(
    102deg,
    transparent 40%,
    rgb(255 255 255 / 6%) 47%,
    rgb(190 255 225 / 14%) 50%,
    rgb(255 255 255 / 6%) 53%,
    transparent 60%
  );
  background-size: 320% 100%;
}

/* ---------- the way out ---------- */

/*
 * Top edge, inline-end -- which on this page is the top left, the same corner the summary panel puts
 * its close in. A visitor who wants the contents and not the welcome should not have to look for it.
 */
.intro__close {
  position: absolute;
  z-index: 3;
  inset-block-start: var(--space-4);
  inset-inline-end: var(--space-4);
  inline-size: 2.4rem;
  block-size: 2.4rem;
  display: grid;
  place-items: center;
  border: 1px solid rgb(255 255 255 / 18%);
  border-radius: 50%;
  /* Its own dark disc rather than a surface token: this button sits on a painting whose colours are
     not known here, and only something opaque is legible against all of them. */
  background: rgb(6 12 9 / 62%);
  backdrop-filter: blur(6px);
  color: var(--color-text);
  font-size: var(--text-base);
  line-height: 1;
  cursor: pointer;
  transition: background 0.2s, transform 0.2s, border-color 0.2s;
}

.intro__close:hover {
  background: rgb(6 12 9 / 88%);
  border-color: var(--color-accent-bright);
  transform: rotate(90deg);
}

.intro__close:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus);
  outline-offset: var(--focus-ring-offset);
}

/* ---------- the transport ---------- */

.intro__bar {
  position: absolute;
  z-index: 3;
  inset-inline: 0;
  inset-block-end: 0;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5) var(--space-5);
}

.intro__play {
  flex: 0 0 auto;
  inline-size: 2.9rem;
  block-size: 2.9rem;
  display: grid;
  place-items: center;
  border: 1px solid color-mix(in oklab, var(--color-accent-bright) 45%, transparent);
  border-radius: 50%;
  background: rgb(6 12 9 / 62%);
  backdrop-filter: blur(6px);
  color: var(--color-accent-bright);
  cursor: pointer;
  transition: background 0.2s, color 0.2s, border-color 0.2s, transform 0.2s;
}

.intro__play:hover {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-text-on-accent);
  transform: scale(1.06);
}

.intro__play:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus);
  outline-offset: var(--focus-ring-offset);
}

.intro__play svg { inline-size: 1.05rem; block-size: 1.05rem; fill: currentColor; }

/* One button, two glyphs, and the state lives on the dialog so the halo below can read it too. */
.intro__icon--pause { display: none; }

.intro[data-playing] .intro__icon--play { display: none; }

.intro[data-playing] .intro__icon--pause { display: block; }

/*
 * A halo that breathes while the voice is speaking. It is the one piece of feedback that says the
 * audio is running when the reader is looking at the painting rather than at the button -- and it
 * stops the moment they pause, which is the whole of its job.
 */
.intro[data-playing] .intro__play {
  animation: intro-pulse 2.6s ease-in-out infinite;
}

@keyframes intro-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--color-accent-bright) 40%, transparent); }
  60% { box-shadow: 0 0 0 12px transparent; }
}

/* Autoplay refused. The button becomes the invitation rather than a control nobody was told to
   press, and intro.js also takes the first click anywhere on the card as that press. */
.intro[data-blocked] .intro__play {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-text-on-accent);
  animation: intro-nudge 2.2s ease-in-out infinite;
}

@keyframes intro-nudge {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--color-accent) 55%, transparent); }
  55% { box-shadow: 0 0 0 14px transparent; }
}

.intro__track {
  flex: 1 1 auto;
  block-size: 4px;
  border-radius: var(--radius-pill);
  background: rgb(255 255 255 / 16%);
  overflow: hidden;
}

/* Percentage width in an RTL box, so it fills from the right -- the direction the voice is read in.
   No transition on the width: `timeupdate` fires often enough to be smooth on its own, and easing
   each step would put the line permanently behind the audio. */
.intro__elapsed {
  display: block;
  block-size: 100%;
  inline-size: 0;
  border-radius: inherit;
  background: linear-gradient(to left, var(--color-accent), var(--color-accent-bright));
}

.intro__time {
  flex: 0 0 auto;
  color: var(--color-text-secondary);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  text-shadow: 0 1px 3px rgb(0 0 0 / 70%);
}

/* ================================================================
 * Motion
 *
 * The card does not appear; it is unveiled. It rises out of the backdrop from behind its own blur,
 * swings a few degrees past flat on the hinge every other surface on this site opens from, and
 * settles -- while the painting inside it comes up from slightly over-scaled, so the picture is
 * still moving when the swing has stopped. That overlap is what makes it read as one gesture
 * instead of two.
 * ================================================================ */

.intro[open] {
  animation: intro-open 0.86s cubic-bezier(0.16, 0.84, 0.24, 1) both;
}

.intro[open]::backdrop { animation: intro-fade 0.5s ease both; }

/*
 * The artwork carries both of its animations at all times -- the reveal, and the slow drift that
 * accompanies the voice -- and playing is a play-state rather than a different list. Written the
 * obvious way, where [data-playing] adds the drift, the list changes on every pause and the reveal
 * restarts from 35% opacity: pressing pause would replay the opening.
 *
 * The drift is declared second so it wins the transform once its delay has elapsed, and carries no
 * fill, so it contributes nothing while the reveal is still running.
 */
.intro[open] .intro__image {
  animation:
    intro-reveal 1.5s cubic-bezier(0.2, 0.75, 0.2, 1) both,
    intro-drift 34s 1.5s ease-in-out infinite alternate;
  animation-play-state: running, paused;
}

.intro[open] .intro__sheen { animation: intro-sheen 1.15s 0.18s ease-out both; }

/* The controls arrive last, after the card has landed: they are chrome, and chrome that flies in
   with the artwork competes with it. */
.intro[open] .intro__close,
.intro[open] .intro__bar {
  animation: intro-controls 0.5s 0.52s ease both;
}

@keyframes intro-open {
  0% {
    transform: perspective(2400px) translateY(46px) scale(0.9) rotateX(9deg);
    opacity: 0;
    filter: blur(16px);
  }
  55% { opacity: 1; filter: blur(0); }
  76% { transform: perspective(2400px) translateY(0) scale(1.012) rotateX(-1.2deg); }
  100% {
    transform: perspective(2400px) translateY(0) scale(1) rotateX(0deg);
    opacity: 1;
    filter: blur(0);
  }
}

@keyframes intro-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes intro-reveal {
  from { transform: scale(1.14); opacity: 0.35; }
  to { transform: scale(1); opacity: 1; }
}

@keyframes intro-sheen {
  0% { opacity: 0; background-position: 100% 0; }
  25% { opacity: 1; }
  100% { opacity: 0; background-position: -20% 0; }
}

@keyframes intro-controls {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}

/*
 * And the drift moves only while the voice is speaking. Very slight, very slow, alternating rather
 * than looping back to the start, so nothing ever jumps: it is the difference between a still
 * picture and a held shot. Tying it to the audio is what makes the picture feel like it is
 * listening too -- and freezing it on pause is why the pause reads as a pause.
 */
.intro[open][data-playing] .intro__image { animation-play-state: running, running; }

@keyframes intro-drift {
  from { transform: scale(1) translate3d(0, 0, 0); }
  to { transform: scale(1.07) translate3d(1.2%, -1.4%, 0); }
}

/*
 * Closing draws the card back down into the page rather than deleting it between two frames. The
 * script holds the <dialog> open until this has run (`data-closing` in intro.js) and skips the wait
 * entirely where motion is unwelcome, because there it would buy nothing.
 */
.intro[open][data-closing] {
  animation: intro-close 0.34s cubic-bezier(0.5, 0, 0.85, 0.3) forwards;
}

/* Frozen where it stands, not reset: `animation: none` here would snap the picture back from
   whatever the drift had scaled it to, and the card would flinch on its way out. */
.intro[open][data-closing] .intro__image { animation-play-state: paused, paused; }

.intro[open][data-closing] .intro__sheen { animation: none; }

.intro[open][data-closing]::backdrop {
  animation: intro-fade 0.34s ease reverse forwards;
}

@keyframes intro-close {
  from { transform: perspective(2400px) scale(1) translateY(0); opacity: 1; filter: blur(0); }
  to { transform: perspective(2400px) scale(0.94) translateY(24px); opacity: 0; filter: blur(10px); }
}

/* A visitor who asked for less movement still gets the card, the picture and the voice -- just
   without any of the above. The pulse goes too: it is the one animation here that never ends. */
@media (prefers-reduced-motion: reduce) {
  .intro[open],
  .intro[open]::backdrop,
  .intro[open] .intro__image,
  .intro[open][data-playing] .intro__image,
  .intro[open] .intro__sheen,
  .intro[open] .intro__close,
  .intro[open] .intro__bar,
  .intro[data-playing] .intro__play,
  .intro[data-blocked] .intro__play { animation: none; }

  .intro__sheen { display: none; }
}

/* ---------- narrow ---------- */

@media (max-width: 40rem) {
  .intro {
    inline-size: calc(100vw - 2 * var(--space-4));
    border-radius: var(--radius-md);
  }

  .intro__bar { padding: var(--space-3) var(--space-4) var(--space-4); gap: var(--space-2); }

  .intro__play { inline-size: 2.6rem; block-size: 2.6rem; }

  .intro__close { inset-block-start: var(--space-3); inset-inline-end: var(--space-3); }
}
