/* ==========================================================================
   Peter & Consuelo
   ========================================================================== */

:root {
  --header-h: 60px;
  --thumb-h: 76px;
  --fade: 900ms;          /* carousel crossfade */
  --story-fade: 400ms;    /* story overlay fade */
  --story-delay: 250ms;   /* beat before the text follows the overlay in */
  --chapter-fade: 800ms;  /* unread -> active, slow enough to catch while scrolling */
  --gallery-fade: 700ms;  /* crossfade inside a chapter's own carousel */

  /* Dark theme. Header, carousel and thumbnail rail all share --stage, so the
     chrome reads as one continuous surface with the photographs sitting on it.

     The neutrals carry roughly half the red-to-blue spread they used to, so
     they read as near-grey with a faint warm cast rather than as cream. The
     accent keeps its full warmth and is the only saturated colour here — every
     tan on the page comes from it. */
  --paper: #171614;      /* raised surface: the story overlay */
  --stage: #0c0b0a;      /* the page surface, and the letterbox behind a photo */
  --ink: #ebe8e3;
  --ink-soft: #9b958d;
  --rule: #2c2a27;       /* hairline borders */
  --accent: hsl(0, 58%, 52%);

  /* The field the opening sequence plays on. The accent is already the
     one red on the page; the curtain is that same red at full strength. */
  --intro-red: var(--accent);


  /* The timeline rail reads as a faint sibling of its markers rather than as a
     border, so it is mixed from the marker colour instead of using --rule. */
  --rail-line: color-mix(in srgb, var(--ink-soft) 35%, transparent);

  /* The floating controls sit on photographs, not on the theme background, so
     they carry their own scrim and stay dark whichever way the theme goes. */
  --scrim: #000;
  --on-scrim: #fff;

  /* Both faces come from the Typekit link in the head. new-frank carries
     everything; the display serif is reserved for the story headings and the
     ampersand in the wordmark. */
  --sans: new-frank, ui-sans-serif, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --display: "dm-serif-display", Georgia, "Times New Roman", serif;
}

*, *::before, *::after { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  /* Header + carousel fill the viewport exactly; the page itself never scrolls. */
  display: grid;
  grid-template-rows: var(--header-h) 1fr;
  height: 100dvh;
  overflow: hidden;
  background: var(--stage);
  color: var(--ink);
  font-family: var(--sans);
  font-weight: 300;
  -webkit-font-smoothing: antialiased;
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Opening sequence

   Four layers over the whole page: the red field, the photograph, the two
   bands of woven trim, and the wordmark. intro.js only ever adds the state
   classes below and drives the final morph — every duration lives here, so the
   whole sequence can be retimed without touching the script.

   The beats run back to back, each one starting where the last finished:

     hold -> name-in -> name-hold -> photo-in -> photo-hold
          -> name-out + round -> morph       (~9.2s in total)

   `round` is a beat of its own, and deliberately so. It used to run inside the
   morph, which is where the eye cannot see it: 12px of corner arriving while
   the photograph is racing down to a fifth of its size is arithmetic, not
   animation. Standing still at full bleed, the same 12px reads clearly — the
   photograph stops being the screen and becomes a card, and only then moves.
   On a narrow screen the slide has no rounding to match, so the beat is
   skipped and the sequence is that much shorter.

   Three of the beats overlap rather than follow. `throbber-out` runs during
   `hold`, handing the centre of the screen from the throbber to the wordmark.
   `name-out` and `round` start together, so the photograph begins to change at
   the same moment the wordmark begins to clear rather than one waiting on the
   other — `--intro-round-at` offsets the second against the first if the two
   ever need pulling apart again. And `reveal` runs during the morph, so the
   page is already arriving underneath while the photograph is still shrinking
   into place.

   The clock only starts once the fonts are usable. Everything before that is
   red, with the throbber if the wait runs long.
   -------------------------------------------------------------------------- */

.intro {
  /* --- the timeline. Every value is a gap, not a clock time: change one and
         everything after it simply moves along with it. --- */
  --intro-throbber-in:  400ms;  /* wait before the throbber is worth showing */
  --intro-throbber-out: 250ms;  /* it leaving again, over the hold below */
  --intro-hold:         250ms;  /* red alone, before anything arrives */
  --intro-name-in:     1300ms;  /* wordmark fading down into place */
  --intro-name-hold:    450ms;  /* wordmark alone on the red */
  --intro-photo-in:    4300ms;  /* photograph rising behind it */
  --intro-photo-hold:   600ms;  /* both together */
  --intro-name-out:    1000ms;  /* wordmark leaving */
  --intro-round-at:       0ms;  /* how long after the wordmark starts leaving
                                   the photograph starts changing; 0 runs the
                                   two together */
  --intro-round:        700ms;  /* corners rounding off, still at full bleed */
  --intro-morph:       1500ms;  /* full bleed -> its place in the carousel */
  --intro-reveal:       900ms;  /* red clearing to show the page */
  --intro-reveal-in:    300ms;  /* how far into the morph the red starts to go */

  /* The trim starts with the wordmark at each end and then takes twice as long
     over it, so the bands are still travelling after the names have settled
     and still leaving after they have gone. Written against the wordmark's own
     two figures rather than as 2600ms and 2000ms, so retiming the names keeps
     the pair in proportion. Nothing in the sequence waits on these: they are
     the one thing here that is free to overrun the beat it started on. */
  --intro-trim-in:  calc(var(--intro-name-in) * 2);
  --intro-trim-out: calc(var(--intro-name-out) * 2);

  /* How long the sequence will wait on a slow photograph before going ahead
     without it. The beats after this one chain off the photograph actually
     arriving, so a slow connection stretches the red rather than playing the
     whole sequence out over an empty frame. */
  --intro-photo-wait: 8000ms;

  /* Eased at both ends. Everything the sequence animates uses this, so the
     whole opening moves to one curve. */
  --intro-ease: cubic-bezier(0.4, 0, 0.2, 1);

  position: fixed;
  inset: 0;
  z-index: 100;                  /* over the header, which is the top of the page stack */
  place-items: center;
  overflow: hidden;

  /* Hidden until script says otherwise: with JavaScript off nothing is coming
     to take this down again. */
  display: none;
}

.is-intro .intro { display: grid; }

/* The red itself. A layer of its own rather than a background on .intro,
   because it has to fade out from under the photograph while the photograph
   stays fully opaque. */
.intro__field {
  position: absolute;
  inset: 0;
  background: var(--intro-red);
  transition: opacity var(--intro-reveal) ease;
}

.intro.is-revealing .intro__field { opacity: 0; }

/* --- the throbber ---

   Up only while the page is genuinely still waiting on the Typekit faces, and
   only once that wait has gone past --intro-throbber-in: a warm cache has them
   within a few frames, and a throbber that appears and leaves again inside half
   a second is its own flicker.

   The spin runs from the first frame and never stops — it is the opacity alone
   that comes and goes, so the ring is already turning at whatever speed by the
   time anyone sees it, and it can be faded away mid-turn without a snap.

   White ring, black quarter: the same two colours as the wordmark it is
   standing in for, since the accent red is the field here. */
.intro__throbber {
  position: absolute;
  inset: 0;
  margin: auto;
  z-index: 2;

  width: 38px;
  height: 38px;
  border: 3px solid #fff;
  border-block-start-color: #000;
  border-radius: 50%;

  opacity: 0;
  transition: opacity var(--intro-throbber-out) ease;
  animation: intro-spin 900ms linear infinite;
}

.intro.is-waiting .intro__throbber {
  opacity: 1;
  transition: opacity var(--intro-throbber-in) ease;
}

@keyframes intro-spin { to { transform: rotate(1turn); } }

/* --- the photograph ---

   The frame is the viewport, and the photograph inside it is scaled to cover
   that frame — `object-fit: contain` plus a scale, rather than `cover`,
   because cover *crops*, and cropped pixels cannot be recovered by the morph
   later on. Contain keeps the whole photograph in the element at all times and
   lets the frame's clip decide how much of it shows, which is what makes the
   full-bleed -> letterboxed move possible at all. See intro.js. */
.intro__frame {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;

  opacity: 0;
  transition: opacity var(--intro-photo-in) ease;
}

.intro.is-photo-in .intro__frame { opacity: 1; }

.intro__photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;

  /* Set by intro.js once it knows the viewport: the factor that takes the
     contained photograph up to covering the page. */
  transform: scale(var(--intro-cover-scale, 1));
}

/* --- the wordmark ---

   The page's own wordmark, set large and centred: the names in white, the
   ampersand in black. The accent red it wears in the header is the field it is
   standing on here, so the odd one out has to go the other way. */
.intro__wordmark {
  position: relative;
  z-index: 2;
  margin: 0;
  padding-inline: 1rem;

  display: flex;
  align-items: baseline;
  gap: clamp(0.35rem, 1.2vw, 0.85rem);

  font-size: clamp(1.75rem, 8vw, 5rem);
  font-weight: 800;
  letter-spacing: -.05em;
  text-transform: uppercase;
  white-space: nowrap;
  color: #fff;

  /* How far it travels, both on the way in and on the way out. One value for
     the pair so the two halves of the move cannot drift apart. */
  --intro-name-shift: 0.35em;

  /* Starts a little high and comes down as it arrives.

     Both halves of the move share one curve, and it eases at both ends: the
     wordmark gathers itself rather than snapping into motion, and settles
     rather than stopping dead. Opacity used to run on the bare `ease` keyword
     while the travel ran on a different curve entirely, which meant the fade
     and the slide were never quite in step with each other. */
  opacity: 0;
  transform: translateY(calc(var(--intro-name-shift) * -1));
  transition: opacity var(--intro-name-in) var(--intro-ease),
              transform var(--intro-name-in) var(--intro-ease);
}

.intro.is-name-in .intro__wordmark {
  opacity: 1;
  transform: none;
}

/* Leaves the way it came: still heading down, rather than retreating back up
   the way it arrived. Read end to end the wordmark drifts one way through the
   whole sequence — down into place, and then on down and out — instead of
   bouncing back to where it started. */
.intro.is-name-out .intro__wordmark {
  opacity: 0;
  transform: translateY(var(--intro-name-shift));
  transition: opacity var(--intro-name-out) var(--intro-ease),
              transform var(--intro-name-out) var(--intro-ease);
}

.intro__amp {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.75rem, 8.8vw, 6rem);
  color: #000;
}

/* On a narrow screen the wordmark stacks:

     PETER &
     CONSUELO

   The block is only as wide as its longest line and centred in the page, while
   the two lines inside it range left against each other — so "Peter &" sits
   above the C of "Consuelo" rather than being centred over it.

   Flex is dropped for block layout here on purpose. The break is forced by a
   newline after the ampersand, and inside a flex row that newline would break
   *within* the ampersand's own box instead of ending the line. As block, the
   three spans are inline again, the newline ends the line as intended, and the
   whitespace already between them in the markup does the spacing that `gap`
   was doing. */
@media (max-width: 600px) {
  .intro__wordmark {
    display: block;
    width: max-content;
    max-width: 100%;
    margin-inline: auto;
    text-align: left;

    /* As large as the width will allow. "CONSUELO" is the longer of the two
       lines and measures 4.88em across in new-frank at this weight and
       tracking, so dividing what is left of the viewport after a 30px gutter
       either side by that figure gives the biggest the line can be set and
       still clear both edges. 4.9 rather than 4.883 leaves a hair in hand.

       The block is centred, so its own padding falls outside this sum: the
       distance from the edge of the screen to the first letter works out as
       (100vw - text) / 2 whatever the padding is. */
    font-size: calc((100vw - 60px) / 4.9);

    /* Two short lines of display type want far less leading than one long one
       — `normal` leaves them reading as two separate thoughts rather than one
       wordmark. A length rather than a number, so both lines are given the
       same leading: a unitless value would be multiplied by the ampersand's
       own larger size and open the first line back up. */
    line-height: 0.92em;
  }

  /* Back to being sized off the names, so the ampersand keeps its proportion
     as they grow. Its own clamp is tuned for the single-line layout and would
     leave it stranded at a third of the size here. */
  .intro__amp {
    font-size: 1.25em;
  }

  .intro__amp::after {
    content: '\A';
    white-space: pre;
  }
}

/* --- the trim ---

   Two bands of the woven Guatemalan ribbon the wedding outfits are trimmed
   with, one across the top of the red and one across the bottom. They ride the
   wordmark's own two classes rather than beats of their own: the top band
   drops in from above and the bottom band rises from below at the moment the
   names begin to arrive, and both retreat the way they came as the names begin
   to leave. One gesture, three pieces of it — starting together and drawn out
   over twice the time, so the trim is the slow part of the move and the type
   the quick one.

   Each band travels diagonally — down and to the right at the top, up and to
   the left at the bottom — so the pair reads as a single sweep opening the
   screen rather than two shutters closing on it.

   Neither end of a band may ever come into shot, which is what the width below
   is for, and it has to hold for every frame of the travel and not just at
   rest. That makes the width a consequence of the shift rather than a free
   choice, and the depth a consequence of the width; see --intro-trim-width. */
.intro__trim {
  /* How far across a band drifts as it arrives, as a share of its own width. */
  --intro-trim-shift: 20%;

  /* Wide enough that the drift cannot drag either end into view: a band
     overhangs the screen by (width - 100%) / 2 at each side, and that overhang
     has to cover a shift of 20% of the width, so the width has to be at least
     100% / (1 - 2 x 0.2) = 166.7%. 175% leaves a hair in hand. */
  --intro-trim-width: 175%;

  /* A band stops growing at 150px, which on the 24.1% below is a screen about
     620px across — so every phone shows the ribbon entire and every window
     wider than a phone shows the same 150px band, however wide it gets. */
  --intro-trim-max: 150px;

  /* Which end of the ribbon survives the crop that buys that, and which way the
     shadow falls. Both are the top band's; .intro__trim--bottom flips them.
     Custom properties because they are used on the image inside and neither
     `object-position` nor `filter` inherits, but a custom property does. */
  --intro-trim-anchor: bottom;
  --intro-trim-shadow: 5px;

  position: absolute;
  z-index: 2;                     /* with the wordmark, over the photograph */
  left: calc((100% - var(--intro-trim-width)) / 2);
  width: var(--intro-trim-width);
  pointer-events: none;

  /* Off screen to begin with. Same curve as everything else in the sequence,
     eased at both ends, so a band gathers itself rather than snapping into
     motion and settles rather than stopping dead.

     The wrapper is the half of the band that moves and the image inside is the
     half that is filtered, and those two jobs are kept on two elements on
     purpose — see the image rule below. will-change so the layer is promoted
     now rather than in the frame the travel starts. */
  transform: var(--intro-trim-off);
  transition: transform var(--intro-trim-in) var(--intro-ease);
  will-change: transform;
}

/* Everything about what a band looks like, on an element that never moves.

   That split is not tidiness, it is the fix for a real artifact: an element
   carrying a filter *and* an accelerated transform has to have its filter
   rasterized again as it travels, and iOS does that into a buffer whose bounds
   and precision were settled once — so the blur below came apart into a hard
   line across the screen every time a band retreated. Filtering an element the
   compositor only ever translates means the shadow is drawn once, properly, and
   then simply moved. Do not merge this rule back into the wrapper.

   Both files are cut to bleed off their own edge of the screen — flush along
   the outside, the ragged woven edge along the inside — so the whole of each
   one is meant to be seen, and the depth follows from the width rather than
   being chosen: the ribbon is 2714 x 374, so a band is 24.1% of the screen
   across. On a phone that is a trim. On a desktop it is a mural, hence the cap,
   and hence `cover` — the crop it takes comes off the flush outer edge, hard
   against the side of the screen, and never off the woven edge facing in.

   drop-shadow() rather than box-shadow because the edge that needs the shadow
   is not a rectangle: the woven edge is ragged and feathers out over the last
   few rows of the file, and drop-shadow follows the alpha, so the shadow is the
   same ragged shape. A box-shadow would draw a straight line under a crooked
   edge. Only the inner edge is meant to show it — the offset pushes the shadow
   that way and the blur is small enough that what spills the other way stays
   inside the band or falls off the screen behind it. */
.intro__trim img {
  display: block;
  width: 100%;
  height: auto;
  max-height: var(--intro-trim-max);
  object-fit: cover;
  object-position: var(--intro-trim-anchor);

  filter: drop-shadow(0 var(--intro-trim-shadow) 12px rgba(0, 0, 0, .3));
  transform: translateZ(0);       /* a layer of its own, rasterized once */
}

/* Where each band sits when it is away. Held as a custom property because the
   way out is the way in run backwards, so the two states are the same value. */
.intro__trim--top {
  top: 0;
  --intro-trim-off: translate(calc(var(--intro-trim-shift) * -1), -100%);
}

.intro__trim--bottom {
  bottom: 0;
  --intro-trim-anchor: top;       /* its woven edge is the one facing up */
  --intro-trim-shadow: -5px;      /* so the shadow falls that way too */
  --intro-trim-off: translate(var(--intro-trim-shift), 100%);
}

.intro.is-name-in .intro__trim { transform: none; }

/* Unlike the wordmark, which carries on the way it was already going, the trim
   reverses out: back up and left, back down and right. It came in off the
   edges of the screen and that is where it belongs when it is not wanted. */
.intro.is-name-out .intro__trim {
  transform: var(--intro-trim-off);
  transition-duration: var(--intro-trim-out);
}

/* --------------------------------------------------------------------------
   Header
   -------------------------------------------------------------------------- */

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  width: 100%;
  height: var(--header-h);
  padding-inline: clamp(1rem, 4vw, 2.5rem);
  background: var(--stage);

  /* Above the story overlay, so "Close Story" stays clickable while it is up. */
  position: relative;
  z-index: 5;
}

.brand {
  margin: 0;
  font-size: clamp(1.25rem, 2.6vw, 1.45rem);
  font-weight: 800;
  white-space: nowrap;
  text-transform: uppercase;
  letter-spacing: -.05em;
}

/* The two names are set heavy; the ampersand between them is the one place in
   the header the display serif appears. */
.brand__amp {
  font-size: clamp(1.55rem, 2.5vw, 1.9rem);
  font-family: var(--display);
  font-weight: 400;
  color: var(--accent);
}

.story-button {
  flex: none;
  padding: 0.5rem 1.15rem;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  font-weight: 400;
  transition: background-color 200ms ease, color 200ms ease;
}

.story-button:hover,
.story-button:focus-visible {
  background: var(--accent);
  color: var(--paper);
}

/* The browser's own focus ring is a blue that belongs to nothing else on the
   page. Restated in the palette's own terms rather than switched off: the fill
   above arrives on hover too, so on its own it cannot tell a keyboard user
   that this is where focus now is. --ink reads against both the accent the
   button fills with and the dark header it sits on, and the offset keeps the
   ring clear of the pill instead of tracing its edge. */
.story-button:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

/* --------------------------------------------------------------------------
   Carousel shell
   -------------------------------------------------------------------------- */

.carousel {
  display: grid;
  grid-template-rows: 1fr auto;

  /* Grid and flex items default to min-width/height:auto, which would let the
     189-item thumbnail track set the page width (~17,000px). Both axes have to
     be allowed to shrink so the strip scrolls instead of stretching the page. */
  min-height: 0;
  min-width: 0;
  background: var(--stage);
}

.stage {
  position: relative;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   Slides — CSS-only crossfade
   -------------------------------------------------------------------------- */

.slides {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.slide {
  position: absolute;
  inset: 0;
  opacity: 0;

  /* Each slide carries its own opaque backdrop, and this is load-bearing.
     Photos are `contain`ed, so an incoming portrait does not cover an outgoing
     landscape — roughly one transition in five leaves a strip down each side.
     Without a backdrop those strips keep showing the previous photo at full
     opacity for the whole fade and then blink out. With it, the incoming layer
     is opaque edge to edge: the strips crossfade to the stage colour, and the
     outgoing slide is completely hidden by the time it snaps out. */
  background: var(--stage);

  /* The outgoing slide holds opaque *underneath* the incoming one for the
     length of the fade, then snaps out. Two opaque layers crossfading means no
     dip to the background mid-transition — and no JS driving the animation. */
  transition: opacity 0s linear var(--fade);

  /* Sized by `inset: 0`, so both axes are already independent of the photo
     inside — `size` rather than `inline-size` because the corner rounding
     below has to measure the stage's height as well as its width. */
  container-type: size;
}

.slide.is-active {
  opacity: 1;
  z-index: 2;
  transition: opacity var(--fade) cubic-bezier(0.4, 0, 0.2, 1);
}

/* Sizing the <img> to the photo rather than to the stage. object-fit: contain
   letterboxes *inside* a full-stage element box, so a border-radius there would
   round invisible corners. Capping both axes instead makes the element the photo
   — same "fit without cropping" result, but now the corners are the real ones. */
.slide img {
  /* Absolute centring: for a replaced element, width/height:auto resolve to the
     intrinsic size, the max-* caps then shrink it to fit while preserving the
     ratio, and margin:auto shares out what is left. The containing block has a
     definite height, so unlike a grid/flex track the percentages resolve. */
  position: absolute;
  inset: 0;
  margin: auto;

  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;

  /* Rounded corners only while the photo is letterboxed at the sides. Once it
     runs the full width of the stage its corners are the page's corners, and
     rounding them cuts a notch out of the layout.

     `100cqb * --ratio` is how wide this photo would be if it were the one
     filling the stage's height; if the stage is wider than that, the height is
     what limits the photo and bars appear either side. The `* 1e5` turns that
     comparison into a step — any positive difference saturates the clamp at
     12px, zero or less floors it at 0.

     Measured against the photo's ratio rather than against its own `100%`
     width, which is what an earlier pass did: a single border-radius value
     sets both the horizontal and the vertical radius, and a percentage in the
     vertical one resolves against the element's HEIGHT. That made the vertical
     half read `stage width - photo height`, which goes negative on any photo
     standing taller than the stage is wide — every portrait on a phone. A
     corner whose radius is 0 in either axis is a square corner, so the whole
     rounding collapsed while the photo was still nowhere near full width. */
  border-radius: clamp(0px, (100cqi - 100cqb * var(--ratio)) * 1e5, 12px);
}

/* --------------------------------------------------------------------------
   Click zones
   -------------------------------------------------------------------------- */

/* Half the stage each, over the slides. z-index matches the floating controls
   rather than beating them: the controls come later in the source, so at an
   equal z-index they paint on top and keep their own hit areas. */
.stage__nav {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
}

.stage__nav-zone {
  flex: 1 1 50%;
  cursor: pointer;
}

/* --------------------------------------------------------------------------
   Floating controls
   -------------------------------------------------------------------------- */

/* The row sits where the lone play/pause button used to. The gap is wide
   enough that the two 44px targets never blur into one control. */
.stage__controls {
  position: absolute;
  z-index: 3;
  inset-block-end: clamp(0.75rem, 2vw, 1.25rem);
  inset-inline-end: clamp(0.75rem, 2vw, 1.25rem);
  display: flex;
  gap: 10px;
}

/* The round glass button both controls are built on. */
.control {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--scrim) 55%, transparent);
  color: var(--on-scrim);
  cursor: pointer;
  backdrop-filter: blur(6px);
  transition: background-color 200ms ease, transform 200ms ease;
}

/* `.control` sets a display, which would otherwise beat the UA `hidden` rule. */
.control[hidden] { display: none; }

.control:hover,
.control:focus-visible {
  background: color-mix(in srgb, var(--scrim) 78%, transparent);
  transform: scale(1.06);
}

/* Same reasoning as the "Our Story" button: the browser's blue belongs to
   nothing else here, and the deepened scrim above cannot stand in for a focus
   ring because hover reaches it too.

   Drawn *inside* the disc, though, which is the difference. These controls
   float over the photographs rather than over the theme, so a ring sitting
   outside them would be laid on whatever happens to be behind — white on a
   pale sky is no ring at all. Inset, it is always on the button's own scrim,
   which at 78% black holds a white ring at full contrast over any photograph.
   The outline follows the border-radius, so it reads as a circle. */
.control:focus-visible {
  outline: 2px solid var(--on-scrim);
  outline-offset: -3px;
}

/* --- soundtrack toggle ---------------------------------------------------

   One icon, two states: the cone is always drawn, and the waves and the cross
   swap places. Both are in the markup and only their opacity changes, so the
   glyph never reflows and the crossfade matches the play/pause morph.
   ------------------------------------------------------------------------- */

.soundtrack__icon {
  width: 22px;
  height: 22px;
}

.soundtrack__cone { fill: currentColor; }

.soundtrack__wave path,
.soundtrack__cross path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
}

.soundtrack__wave { opacity: 0; }
.soundtrack__cross { opacity: 1; }

.carousel[data-sound="on"] .soundtrack__wave { opacity: 1; }
.carousel[data-sound="on"] .soundtrack__cross { opacity: 0; }

.soundtrack__wave,
.soundtrack__cross {
  transition: opacity 160ms ease;
}

/* Pause glyph: two bars, drawn with a border. Flips to a triangle when paused. */
.playpause__icon {
  width: 12px;
  height: 14px;
  border-inline: 4px solid currentColor;
  transition: border-width 160ms ease;
}

.carousel[data-playing="false"] .playpause__icon {
  width: 0;
  height: 0;
  border-inline: 0;
  border-block: 7px solid transparent;
  border-inline-start: 12px solid currentColor;
  margin-inline-start: 3px;
}

/* --- counter and credit --------------------------------------------------

   The pair sits bottom-left, mirroring the controls bottom-right. The row
   stops short of those controls and wraps upward rather than into them, so a
   narrow viewport lifts the credit onto its own line instead of overlapping.
   ------------------------------------------------------------------------- */

.stage__meta {
  --meta-inset: clamp(0.75rem, 2vw, 1.25rem);

  position: absolute;
  z-index: 3;
  inset-block-end: var(--meta-inset);
  inset-inline-start: var(--meta-inset);
  /* Clear the 44px control opposite, plus the gutter on either side of it. */
  inset-inline-end: calc(44px + var(--meta-inset) * 2);

  display: flex;
  align-items: center;
  gap: 8px;
  /* Bottom line first, extra lines stacked above it. */
  flex-wrap: wrap-reverse;

  /* Both children sit inside the "previous" click zone. The row itself lets
     clicks through so the slideshow still steps back; only the credit link
     takes them, and it opts back in below. */
  pointer-events: none;
}

/* The glass pill both children are built on. */
.counter,
.credit {
  margin: 0;
  padding: 0.3rem 0.7rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--scrim) 55%, transparent);
  color: var(--on-scrim);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  backdrop-filter: blur(6px);
}

.counter {
  font-variant-numeric: tabular-nums;
}

.credit {
  min-width: 0;   /* lets the pill shrink instead of pushing the row wider */
}

.credit a {
  color: inherit;
  pointer-events: auto;   /* the one thing in this row that is clickable */
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  text-underline-offset: 0.2em;
  transition: text-decoration-color 200ms ease;
}

.credit a:hover,
.credit a:focus-visible {
  text-decoration-color: currentColor;
}

/* --------------------------------------------------------------------------
   Thumbnail strip
   -------------------------------------------------------------------------- */

.thumbs {
  min-width: 0;
  background: var(--stage);
}

.thumbs__track {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 6px clamp(0.5rem, 2vw, 1rem);
  list-style: none;

  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  scrollbar-color: var(--rule) transparent;
}

.thumbs__track::-webkit-scrollbar { height: 6px; }
.thumbs__track::-webkit-scrollbar-thumb {
  border-radius: 3px;
  background: var(--rule);
}

.thumb {
  flex: none;
  scroll-snap-align: center;
}

.thumb button {
  display: block;
  height: var(--thumb-h);
  padding: 0;
  border: 0;
  border-radius: 3px;
  background: var(--stage);
  cursor: pointer;
  overflow: hidden;
  opacity: 0.55;
  outline-offset: 2px;
  transition: opacity 220ms ease, box-shadow 220ms ease;
}

.thumb button:hover,
.thumb button:focus-visible { opacity: 1; }

.thumb button[aria-current="true"] {
  opacity: 1;
  box-shadow: 0 0 0 2px var(--accent);
}

.thumb img {
  display: block;
  height: 100%;
  width: auto;
  object-fit: cover;
}

.noscript {
  position: absolute;
  inset-block-start: calc(var(--header-h) + 2rem);
  inset-inline: 0;
  text-align: center;
  color: var(--ink-soft);
}


/* --------------------------------------------------------------------------
   Our Story overlay

   Two-stage fade, entirely in CSS: the backdrop fades first, then the text
   follows one --story-delay later via transition-delay. No JS timers.
   -------------------------------------------------------------------------- */

.story {
  position: fixed;
  inset: 0;
  z-index: 4;                    /* over the carousel, under the header */

  /* 20px all round. The top also clears the fixed header so the first line
     never slides underneath it. */
  padding: 20px;
  padding-block-start: calc(var(--header-h) + 20px);

  overflow-y: auto;              /* the body cannot scroll, so this must */
  overscroll-behavior: contain;

  background: color-mix(in srgb, var(--paper) 97%, transparent);
  backdrop-filter: blur(12px);

  opacity: 0;
  visibility: hidden;            /* also drops it out of the tab order */
  transition: opacity var(--story-fade) ease,
              visibility 0s linear var(--story-fade);
}

.story.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--story-fade) ease, visibility 0s;
}

.story__inner {
  --rail: 34px;                  /* gutter the timeline lives in */
  --rail-w: 3px;                 /* thickness of the timeline line */
  --marker: 11px;

  position: relative;
  max-width: 600px;
  margin-inline: auto;           /* centres the column once past 600px */
  padding-inline-start: var(--rail);
  text-align: left;
  font-size: clamp(1rem, 2.4vw, 1.125rem);
  line-height: 1.7;

  opacity: 0;
  transition: opacity var(--story-fade) ease;
}

/* On the way in the text waits for the backdrop; on the way out it leaves
   with it, so closing does not feel sluggish. */
.story.is-open .story__inner {
  opacity: 1;
  transition: opacity var(--story-fade) ease var(--story-delay);
}

.story__inner p { margin: 0 0 1.25rem; }
.chapter:last-child p:last-child { margin-bottom: 0; }

.chapter { margin-block-end: 100px; }
.chapter:last-child { margin-block-end: 0; }

/* --- a chapter's own carousel --------------------------------------------

   Every chapter's photographs share one frame and crossfade between each
   other. The dots over the bottom of that frame are the whole of the
   navigation: no arrows, no swipe, and nothing bound to the keyboard.

   That last part is deliberate rather than lazy. This overlay sits on top of a
   slideshow that already answers the arrow keys, and a second carousel
   listening for them would make it a coin toss which one moved.
   ------------------------------------------------------------------------- */

.chapter__gallery {
  position: relative;
  aspect-ratio: 16 / 9;      /* reserves the box before any file loads */
  border-radius: 12px;
  overflow: hidden;          /* clips the slides to the rounded corners */
  background: var(--stage);
  box-shadow: 0 2px 10px 2px var(--stage);

  /* Sits back with the rest of an unread chapter. Safe to use opacity here,
     unlike on the heading — no timeline marker hangs off this element. */
  opacity: 0.5;
  transition: opacity var(--chapter-fade) ease;
}

.chapter.is-active .chapter__gallery { opacity: 1; }

.chapter__slides {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The same crossfade the main slideshow uses: the outgoing photograph holds at
   full opacity underneath the incoming one for the length of the fade and only
   then snaps out, so the frame never dips towards the background mid-swap. */
.chapter__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0s linear var(--gallery-fade);
}

.chapter__slide.is-active {
  opacity: 1;
  z-index: 2;
  transition: opacity var(--gallery-fade) ease;
}

.chapter__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* --- the dots ---

   Centred over the foot of the frame. Filled black when unselected, filled
   white when selected, both inside the same black border, so the pair reads
   the same way over a dark photograph as over a bright one. */
.chapter__dots {
  position: absolute;
  z-index: 3;
  inset-inline: 0;
  inset-block-end: 6px;
  display: flex;
  justify-content: center;
}

/* The button is a 24px target; the marker inside it is the 12px circle that
   actually shows. Sizing the two apart keeps the dots small without making
   them a coin toss to hit on a phone. */
.chapter__dot {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.chapter__dot::before {
  content: '';
  width: 12px;
  height: 12px;
  border: 2px solid #000;
  border-radius: 50%;
  background: #000;
  transition: background-color 200ms ease;
}

.chapter__dot[aria-current="true"]::before { background: #fff; }

/* These are black and white by design, so the focus ring is too. It goes
   outside the marker's own black border, which gives it an edge to read
   against whatever photograph happens to be behind it. */
.chapter__dot:focus-visible { outline: none; }
.chapter__dot:focus-visible::before { box-shadow: 0 0 0 2px #fff; }

/* The gallery now sets the space above a heading, so it replaces the
   heading's own top margin rather than adding to it. */
.chapter__gallery + .story__heading { margin-block-start: 20px; }

/* A dateline now sits under each heading rather than once at the top,
   so it takes the same rhythm as a paragraph. */
.story__inner .story__dates {
  position: relative;            /* anchor for this chapter's marker */
  margin-block-start: -10px;
  margin-bottom: 1.25rem;
  font-size: 0.78rem;
  font-weight: 800;
  line-height: 2;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
}

/* Keep a place name on one line when the dateline wraps on narrow screens. */
.story__dates span { white-space: nowrap; }

.story__heading {
  margin: 2.25rem 0 0.5rem;
  font-family: var(--display);
  font-size: 1.5em;
  font-weight: 400;
  letter-spacing: 0.03em;
}

/* --- timeline -------------------------------------------------------------

   The rail runs the height of the copy; each chapter punches a marker over it.

   The marker hangs off the dateline rather than the heading, so that the one
   filled-in circle and the one line of red type in a chapter sit on the same
   line as each other. It is positioned half a line box down (1em of the
   dateline's line-height: 2), which centres it on the dateline's first line
   however that line wraps.
   -------------------------------------------------------------------------- */

.story__inner::before {
  content: '';
  position: absolute;
  /* Runs from the top of the first image down to the end of the last chapter's
     copy. That end depends on rendered image heights and how the text wraps, so
     carousel.js measures it; the fallback spans the whole column if that never
     runs. Stays centred on the markers whatever --rail-w is. */
  top: 0;
  height: var(--rail-height, 100%);
  inset-inline-start: calc((var(--marker) - var(--rail-w)) / 2);
  width: var(--rail-w);
  border-radius: calc(var(--rail-w) / 2);   /* rounded caps at both ends */
  background: var(--rail-line);
}

.story__dates::before {
  content: '';
  position: absolute;
  inset-inline-start: calc(var(--rail) * -1);
  top: 1em;
  transform: translateY(-50%);

  width: var(--marker);
  height: var(--marker);
  /* --rule is right for the hairline rail but too faint to read as a circle,
     so an unvisited marker borrows the muted text colour instead. */
  border: 2px solid var(--rail-line);
  border-radius: 50%;
  background: var(--paper);      /* masks the rail running behind it */

  transition: background-color var(--chapter-fade) ease,
              border-color var(--chapter-fade) ease;
}

.chapter.is-active .story__dates::before {
  background: var(--accent);
  border-color: var(--accent);
}

/* Copy in an unread chapter sits back; the active one comes forward.

   Dimmed through the text colour rather than `opacity`, because the timeline
   marker is a ::before of the dateline — an opacity there would fade the
   marker with it, and dimming the markers drops them below ~1.3:1 and they
   stop reading. */
.chapter .story__heading,
.chapter p {
  color: color-mix(in srgb, var(--ink) 40%, transparent);
  transition: color var(--chapter-fade) ease;
}

.chapter .story__dates {
  color: color-mix(in srgb, var(--accent) 40%, transparent);
}

.chapter.is-active .story__heading,
.chapter.is-active p {
  color: var(--ink);
}

.chapter.is-active .story__dates {
  color: var(--accent);
}

/* --------------------------------------------------------------------------
   Reduced motion — no crossfade, and autoplay starts paused (see carousel.js)
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  :root {
    --fade: 1ms; --story-fade: 1ms; --story-delay: 0s; --chapter-fade: 1ms;
    --gallery-fade: 1ms;
  }
  * { scroll-behavior: auto !important; }

  /* The opening sequence is motion from end to end, so there is no reduced
     version of it to play: intro.js skips straight to clearing the red, and
     this is how long that takes. */
  .intro { --intro-reveal: 300ms; }

  /* The throbber is the one part that still runs, because the wait it reports
     on happens either way. It just no longer turns. */
  .intro__throbber { animation: none; }
}
