/* The rule this stylesheet has: nothing is ever laid over a photograph, and no
   photograph is drawn at anything but its own values. No filter, no opacity at
   rest, no blend mode, no gradient, no vignette, no tint. Every state is drawn
   in the space around the picture.

   This breaks the set's rules 2 (composite images into the medium) and 6 (glass
   overlay) on purpose, which is why it is not a station on the tuner. */

:root {
  --gap: 1.5rem;
  /* The sheet's row height. Every cell is this tall; only width varies. */
  --row: 240px;
  --row-pick: 380px;
  /* The ground either side of the plate, where the walk lives. */
  --gutter: 3.25rem;
}

/* The filter sets `hidden` on cells, the band and its label, all of which carry
   an author `display` that beats the browser's own `[hidden]` rule. */
[hidden] {
  display: none !important;
}

body {
  display: flex;
  flex-direction: column;
  overscroll-behavior-y: contain;
}

.head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  height: 44px;
  flex-shrink: 0;
  padding: 0 var(--gap);
  border-bottom: 1px solid var(--c-line);
}

.mark {
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-width: 0;
  font-size: 13px;
  font-weight: 400;
}

.wordmark {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.22em;
  color: var(--c-text);
  white-space: nowrap;
}

/* Gives way first on a narrow screen. */
.slug {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.slug,
.ctl {
  font-size: 10px;
  line-height: 14px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--c-dim);
}

.controls,
.years {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
}

button.ctl {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  font-size: 10px;
  line-height: 14px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--c-dim);
  cursor: pointer;
}

a.ctl {
  text-decoration: none;
}

.ctl:hover,
.ctl:focus-visible,
.ctl.on {
  color: var(--c-text);
  transition: color 120ms linear;
}

.rule {
  width: 1px;
  height: 10px;
  background: var(--c-line);
  flex-shrink: 0;
}

.status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  height: 28px;
  flex-shrink: 0;
  padding: 0 var(--gap);
  border-top: 1px solid var(--c-line);
  /* Not the bezel's uppercase: this line carries values. */
  font-size: 11px;
  color: var(--c-dim);
  user-select: none;
}

.stamp {
  flex-shrink: 0;
  color: var(--c-land);
}

.message {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.by {
  flex-shrink: 0;
  color: inherit;
  text-decoration: none;
}

.by:hover,
.by:focus-visible {
  color: var(--c-text);
  transition: color 120ms linear;
}

.scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* Justified rows: each cell is one row tall and its aspect ratio is both the
   flex basis and the growth factor, which fills a row without measuring at
   runtime. Costs a few per cent off the sides, spent by `object-fit: cover`. */
.sheet {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 1px;
  background: var(--c-line);
}

/* The same sheet at a larger row; nothing else marks the band out. `.cell` takes
   both height and flex basis from `--row`. */
.band {
  --row: var(--row-pick);
  /* One row always: wrapped picks pushed the year off the bottom of the screen.
     Past the viewport width the strip scrolls sideways. */
  flex-wrap: nowrap;
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

/* No growth, so the picks keep exact ratios and `cover` crops nothing. */
.band .cell {
  flex: 0 0 calc(var(--ar) * var(--row));
}

.label {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem var(--gap) 0.5rem;
  border-top: 1px solid var(--c-line);
  font-size: 10px;
  line-height: 14px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--c-dim);
  user-select: none;
}

.tagrow .tags {
  display: flex;
  align-items: baseline;
  gap: 12px;
  min-width: 0;
  /* Scrolls sideways past the viewport width rather than wrapping to two lines. */
  overflow-x: auto;
  scrollbar-width: none;
}

/* The first block on the page has the bezel's rule above it already. */
.scroll > .label:first-child {
  border-top: 0;
}

.label span + span {
  color: var(--c-land);
  flex-shrink: 0;
}

/* A link, so opening in a new tab works and a scriptless sheet is still a
   gallery. Nothing about it is drawn as a link. */
.cell {
  height: var(--row);
  flex: var(--ar) 1 calc(var(--ar) * var(--row));
  min-width: 0;
  position: relative;
  border: 0;
  padding: 0;
  text-decoration: none;
  color: inherit;
  -webkit-tap-highlight-color: transparent;
  overflow: hidden;
  background: var(--c-void);
  cursor: pointer;
  outline-offset: -2px;
}

.fill {
  /* Absorbs the last row's slack, which would otherwise stretch the final
     frames across the full width. */
  flex: 999999 0 0;
  height: var(--row);
  background: var(--c-void);
}

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

/* Approach is marked with a hairline in the gap, not by treating the picture. */
.cell::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid transparent;
  pointer-events: none;
  transition: border-color 120ms linear;
}

.cell:hover::after,
.cell:focus-visible::after {
  border-color: var(--c-text);
}

.plate {
  /* Pinned rather than sized at 100%: a dialog resolves that against a
     containing block that is not the viewport and came up 87px short. `dvh` for
     the same reason on a phone, where browser chrome slides away. */
  position: fixed;
  inset: 0;
  margin: 0;
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  border: 0;
  padding: 0;
  background: var(--c-void);
  color: var(--c-text);
}

.plate[open] {
  display: flex;
  flex-direction: column;
  /* Fade in only. Fading out too needs `opacity: 0` on the closed dialog plus
     `display` and `overlay` on `allow-discrete`, which makes the transition the
     only thing that renders the plate: where it does not run, the viewer opens
     to nothing. Opaque by default, so the worst case is no fade. */
  opacity: 1;
  transition: opacity 180ms linear;
}

/* Must come after the rule it starts from. */
@starting-style {
  .plate[open] {
    opacity: 0;
  }
}

.plate::backdrop {
  background: var(--c-void);
}

.plate-stage {
  position: relative;
  flex: 1;
  min-height: 0;
  /* A zoomed plate would otherwise run over the bezel and out of the dialog. */
  overflow: hidden;
  contain: layout paint;
  padding: var(--gap);
}

.plate-holder {
  position: absolute;
  /* The gutters are the arrows' room. */
  inset: var(--gap) var(--gutter);
  display: grid;
  place-items: center;
  /* Definite on both axes, so the container units below measure the stage. */
  container-type: size;
}

/* The element's box IS the picture's box: no letterboxing inside, no dead
   margin around. `object-fit: contain` in a full-size holder instead made the
   opening morph grow the thumbnail into the whole stage, put clicks on empty
   space, and would not resolve without absolute positioning and percentages.

   Height is the smaller of the stage's height and what its width allows at this
   ratio; `aspect-ratio` gives the width. `--ar` is written by the script from
   the build's dimensions, so the box is right before a byte has decoded. */
.plate-holder img,
.plate-holder video {
  display: block;
  aspect-ratio: var(--ar, 1.5);
  height: min(100cqh, calc(100cqw / var(--ar, 1.5)));
  max-width: 100%;
}

.plate-holder video {
  cursor: pointer;
}

/* Zoom is `scale` about a `transform-origin` that follows the pointer, which
   keeps the point under the cursor without arithmetic. The scale transitions;
   the origin must not, or the pan lags the hand. */
.plate-holder img {
  cursor: zoom-in;
  transition: transform 180ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.plate-holder img.zoom {
  cursor: zoom-out;
}

/* Smaller than the stage: nothing to go to. */
.plate-holder img.flat {
  cursor: default;
}

/* The frame the photograph is about to fill, at its own ratio. Drawn behind the
   holder only, so it is never over a photograph. */
.plate-wait {
  position: absolute;
  inset: var(--gap);
  place-items: center;
  pointer-events: none;
  /* Definite on both axes, so the container units below measure the stage. */
  container-type: size;
}

.plate-wait:not([hidden]) {
  display: grid;
}

.plate-wait-frame {
  /* Container units, not percentages: `aspect-ratio` with `width: 100%` and a
     `max-height` draws the full stage at any ratio, because the width wins and
     nothing narrows it back. */
  aspect-ratio: var(--ar, 1.5);
  height: min(100cqh, calc(100cqw / var(--ar, 1.5)));
  border: 1px solid var(--c-line);
}

/* Always drawn, never hidden: taking it away for a still moved the stage a pixel
   on every walk that crossed between a still and a clip. */
.plate-progress {
  height: 1px;
  flex-shrink: 0;
  margin: 0 var(--gap);
  background: var(--c-line);
}

.plate-progress i {
  display: block;
  height: 1px;
  width: 0;
  background: var(--c-text);
}

.plate-bezel {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  height: 28px;
  flex-shrink: 0;
  padding: 0 var(--gap);
  border-top: 1px solid var(--c-line);
  font-size: 10px;
  line-height: 14px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--c-dim);
  user-select: none;
}

.plate-name {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Each holds half the leftover space, centred. Under `space-between` an empty
   slot moved the other halfway across the bezel on every walk. */
.plate-ex,
.plate-tags {
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.plate-tags {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 12px;
}

.plate-ex {
  color: var(--c-land);
  text-transform: none;
  letter-spacing: 0.08em;
}

.plate-keys {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
}

.plate-count {
  color: var(--c-text);
  font-variant-numeric: tabular-nums;
}

/* Full height of the stage, so the target is the whole margin rather than the
   glyph, and inside the gutter the holder was inset to leave. */
.plate-step {
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--gutter);
  display: grid;
  place-items: center;
  font-size: 12px;
  /* Uppercase and letter-spacing are for words. */
  text-transform: none;
  letter-spacing: 0;
}

#plate-prev {
  left: 0;
}

#plate-next {
  right: 0;
}

/* A zoomed plate runs under the gutters, and nothing may sit over a photograph.
   Escape steps back out to the whole frame, where these return. */
.plate-stage:has(.zoom) .plate-step {
  display: none;
}

/* View transitions. Opening and closing morph the cell's rectangle into the
   plate's; walking cross-fades in place. The root says which is running. */

/* Only the plate is named, so the sheet is never snapshotted. */
:root {
  view-transition-name: none;
}

/* Both dimensions, or the incoming photograph is cut in half: the UA sizes
   snapshots `inline-size: 100%; block-size: auto`, top-aligned, so a portrait
   arriving in a landscape group hangs off the bottom of the viewport. */
::view-transition-old(plate),
::view-transition-new(plate) {
  width: 100%;
  height: 100%;
}

/* On BOTH, or the picture pops halfway: whichever snapshot is left to size
   itself is stretched to fill a group box interpolating between two ratios.
   `cover` is right at both ends of the morph. */
:root[data-vt='open']::view-transition-old(plate),
:root[data-vt='open']::view-transition-new(plate) {
  object-fit: cover;
}

:root[data-vt='open']::view-transition-group(plate),
:root[data-vt='open']::view-transition-old(plate),
:root[data-vt='open']::view-transition-new(plate) {
  animation-duration: 260ms;
  animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* A walk names the HOLDER, whose box is identical for every photograph, so the
   group has one rectangle at both ends. Naming the photograph instead puts a
   zoom in the walk, and killing the group animation freezes the box at the old
   geometry rather than fixing it. */
:root[data-vt='walk']::view-transition-group(plate),
:root[data-vt='walk']::view-transition-old(plate),
:root[data-vt='walk']::view-transition-new(plate) {
  /* A running transition's overlay swallows pointer events, so a long one eats
     the clicks of a reader moving quickly. */
  animation-duration: 220ms;
}

:root[data-vt='walk']::view-transition-old(plate),
:root[data-vt='walk']::view-transition-new(plate) {
  /* `contain`, not `cover`: both ends are whole photographs. */
  object-fit: contain;
  animation-timing-function: linear;
}

/* Type size never changes; the row height and the room around a control do. */

@media (max-width: 900px) {
  :root {
    --gap: 1rem;
    --row: 180px;
    --row-pick: 280px;
    --gutter: 2.25rem;
  }

  /* The bezel already carries the name, the position and the way out. */
  .plate-ex,
  .plate-tags {
    display: none;
  }
}

@media (max-width: 560px) {
  :root {
    --row: 140px;
    --row-pick: 220px;
  }

  .slug {
    display: none;
  }
}

/* Keyed off the pointer, not the viewport: a tablet is wide and still coarse. */
@media (pointer: coarse) {
  .controls,
  .years,
  .plate-keys {
    gap: 16px;
  }

  .cell {
    -webkit-tap-highlight-color: transparent;
  }
}

/* Keeps its space on a still rather than being taken out of the row: removing it
   moved the count and the arrows on every walk that crossed between kinds. */
.plate-rate {
  display: flex;
  align-items: center;
  gap: 12px;
}

.plate-rate[hidden] {
  display: flex;
  visibility: hidden;
}

.plate-rate[hidden] + .rule {
  visibility: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .plate-holder img {
    transition: none;
  }

  /* The script starts no view transition under this query, so there are no
     pseudo-elements left to cancel. */
  .plate {
    transition: none;
  }
}
