@font-face {
  font-family: "Jost";
  font-style: normal;
  font-weight: 300;
  src: url("/fonts/jost/jost-latin-300-normal.woff2") format("woff2"),
       url("/fonts/jost/jost-latin-300-normal.woff") format("woff");
  font-display: swap;
}

/* Every value here was measured off the original Cargo site. Keeping them as tokens
   means the mobile breakpoint overrides them once, rather than each rule fighting a
   more specific selector further down the file. */
:root {
  --pad-y: 2.656vw;
  --pad-x: 2.656vw;
  --pad-2x: calc(var(--pad-y) * 2);

  --col-sidebar: 19.48vw;
  --col-gap: 10.3vw;

  --gap-gallery: 0.65vw;
  --gap-gallery-ttobb: 1.198vw;
  --ttobb-inset: 2.97vw;

  --carousel-inline-h: 37.55vw;

  /* --- Ink: the default palette. Themes below override these three and nothing
     else, so any rule written against them themes itself. --- */

  --bg: #000;

  /* Pure #fff is 21:1 against this background - technically excellent, but harsh
     over a full-viewport black field. #EDEDED still reads at 17.9:1. */
  --fg: #EDEDED;

  /* Original was #942f68, but that is only 2.87:1 on black - below WCAG AA. This is
     the smallest lift that passes (4.51:1): hue moves 326deg -> 329deg, the rest is
     lightness. */
  --accent: #BE4A85;
}

/* Both alternates are dark: the photos are the content, and a light surround
   flattens them. They carry real tint rather than sitting a few levels off black,
   because the error-diffusion work and the home hero have their blacks knocked out
   and show this colour straight through the image - a background nobody can tell
   from #000 makes that invisible.

   Contrast against each theme's own background, foreground then accent:
   darkroom 14.3:1 / 5.3:1, cyanotype 13.0:1 / 6.2:1. */
[data-theme="darkroom"] {
  --bg: #241309;
  --fg: #F0E4D8;
  /* Was #D8552F, which measured 4.49:1 on this deeper ground - just under AA. */
  --accent: #E2673F;
}

[data-theme="cyanotype"] {
  --bg: #08202E;
  --fg: #D3E6F2;
  --accent: #4FA8C8;
}

* {
  box-sizing: border-box;
}

/* <picture> only exists to hold the WebP <source>; it must not become a box of its
   own, or every layout rule written against `.hero img` / `.slideshow-slide img`
   would suddenly be measuring the wrapper instead of the photo. */
picture {
  display: contents;
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--fg);
}

/* iOS Safari inflates text on rotation to portrait unless this is pinned, which on a
   vw-scaled layout throws every measured size out at once. */
html {
  -webkit-text-size-adjust: 100%;
}

::selection {
  background: var(--accent);
  color: var(--bg);
}

/* The original Cargo site scales everything with viewport width: measured font-size
   is exactly 1.5vw (28.8px @1920, 14.37px @958), line-height 1.2. */
body {
  font-family: "Jost", sans-serif;
  font-weight: 300;
  font-size: 1.5vw;
  line-height: 1.2;
}

/* --- accessibility utilities --- */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 300;
  padding: 0.6em 1em;
  background: var(--fg);
  color: var(--bg);
  transform: translateY(-120%);
}

.skip-link:focus {
  transform: translateY(0);
}

/* The UA default focus ring resolves to near-black, which is invisible on this
   background. :focus-visible keeps it off for mouse users. */
:focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 3px;
}

/* --- links --- */

a {
  color: inherit;
  text-decoration: none;
  transition: color 0.15s ease;
}

a:hover {
  text-decoration: underline;
}

.nav a {
  transition: color 0.2s ease;
}

/* Colour shift instead of the global underline - the nav is the one place where an
   underline would break the flush left column. */
.nav a:hover {
  color: var(--accent);
  text-decoration: none;
}

.nav a.active {
  color: var(--accent);
}

/* --- layout --- */

.layout {
  display: grid;
  grid-template-columns: var(--col-sidebar) 1fr;
  column-gap: var(--col-gap);
  min-height: 100vh;
  min-height: 100dvh; /* dvh excludes browser chrome; vh above is the fallback */
  width: 100%;
  padding: var(--pad-y) var(--pad-x);
}

/* Sidebar block (logo + nav + clock) is vertically centred in the viewport on the
   original, and stays pinned while the content column scrolls. align-self:start stops
   it stretching to the full row height, so it centres against the viewport rather
   than against a tall gallery. */
.sidebar {
  position: sticky;
  top: var(--pad-y);
  align-self: start;
  height: calc(100vh - var(--pad-2x));
  height: calc(100dvh - var(--pad-2x));
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
}

.logo-link {
  display: block;
  margin-bottom: 0.3em;
}

.logo {
  width: 100%;
  height: auto;
  display: block;
}

.nav {
  display: flex;
  flex-direction: column;
}

.clock {
  margin-top: 1.2em;
}

/* --- theme switcher --- */

.theme-switch {
  display: flex;
  margin-top: 0.9em;
  /* Cancels the leading edge of the first dot's hit-area padding, so the row of dots
     lines up with the nav text above rather than sitting indented from it. */
  margin-left: -8px;
}

/* The three themes are all near-black, so a dot showing its own background would be
   indistinguishable from the other two. Each carries its theme's accent instead. */
.theme-dot {
  appearance: none;
  border: 0;
  background: none;
  cursor: pointer;
  /* WCAG 2.2 target size: the visible circle is small, the hit area is not. */
  padding: 8px;
  min-width: 24px;
  min-height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 0.2s ease;
  opacity: 0.45;
}

.theme-dot::before {
  content: "";
  display: block;
  width: 0.6vw;
  height: 0.6vw;
  min-width: 8px;
  min-height: 8px;
  border-radius: 50%;
  background: var(--dot);
  transition: box-shadow 0.2s ease;
}

.theme-dot:hover {
  opacity: 1;
}

/* Active state is opacity plus a ring, never colour alone. */
.theme-dot[aria-pressed="true"] {
  opacity: 1;
}

.theme-dot[aria-pressed="true"]::before {
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--fg);
}

.theme-dot[data-theme-value="ink"] {
  --dot: #BE4A85;
}

.theme-dot[data-theme-value="darkroom"] {
  --dot: #E2673F;
}

.theme-dot[data-theme-value="cyanotype"] {
  --dot: #4FA8C8;
}

.content {
  min-width: 0;
  opacity: 1;
  transition: opacity 0.15s ease;
}

.content.fading {
  opacity: 0;
}

/* --- home hero --- */

/* Height-constrained to the viewport (minus page padding) on the original, with its
   width following from the image's aspect ratio. */
.hero img {
  max-width: 100%;
  max-height: calc(100vh - var(--pad-2x));
  max-height: calc(100dvh - var(--pad-2x));
  width: auto;
  height: auto;
  display: block;
  margin: 0 auto;
}

/* --- galleries --- */

.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gap-gallery);
  align-items: start;
}

.gallery-item {
  display: block;
  overflow: hidden;
}

/* Original renders photos at their natural aspect ratio - no cropping. */
.gallery-item img {
  width: 100%;
  height: auto;
  display: block;
  transition: transform 0.3s ease;
}

.gallery-item:hover img {
  transform: scale(1.04);
}

/* Gated on .js so that if scripting is unavailable the images simply render, rather
   than staying at opacity 0 forever waiting for a class that never arrives. */
.js img[loading="lazy"] {
  opacity: 0;
  transition: opacity 0.4s ease;
}

.js img[loading="lazy"].loaded {
  opacity: 1;
}

/* --- lightbox --- */

/* Deliberately not themed: a fullscreen viewer stays dark whatever the page is, or
   the surround washes out the photo it frames. */
/* Above the mobile bar (120) as well as the page, so a photo opened from a gallery
   covers the whole screen on a phone. */
.lightbox {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
}

.lightbox[hidden] {
  display: none;
}

.lightbox-img {
  max-width: 88vw;
  max-height: 88vh;
  object-fit: contain;
  display: block;
}

/* Stays #fff rather than var(--fg): these sit on the fixed-dark backdrop above, not
   on themed page chrome. */
.lightbox-close,
.lightbox-prev,
.lightbox-next {
  position: absolute;
  background: none;
  border: none;
  color: #fff;
  font-size: 2rem;
  line-height: 1;
  cursor: pointer;
  padding: 12px 18px;
  font-family: inherit;
  transition: opacity 0.15s ease;
  opacity: 0.7;
}

.lightbox-close:hover,
.lightbox-prev:hover,
.lightbox-next:hover,
.lightbox-close:focus-visible,
.lightbox-prev:focus-visible,
.lightbox-next:focus-visible {
  opacity: 1;
}

.lightbox-close {
  top: 16px;
  right: 16px;
}

.lightbox-prev {
  left: 8px;
  top: 50%;
  transform: translateY(-50%);
}

.lightbox-next {
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
}

body.lightbox-open {
  overflow: hidden;
}

/* --- carousel --- */

.slideshow {
  position: relative;
  max-width: 100%;
  overflow: hidden;
}

.slideshow-track {
  display: flex;
  will-change: transform;
}

.slideshow-slide {
  flex: 0 0 100%;
  min-width: 0;
  display: flex;
  justify-content: center;
}

/* Slides fit the viewport height (minus page padding) like the original, so a
   slideshow-only page never scrolls - width follows from the aspect ratio. */
.slideshow-slide img {
  max-width: 100%;
  max-height: calc(100vh - var(--pad-2x));
  max-height: calc(100dvh - var(--pad-2x));
  width: auto;
  height: auto;
  display: block;
  object-fit: contain;
}

/* Where the carousel sits above other content it is a fixed block rather than
   filling the viewport (measured 721px tall @1920 on the original). */
.slideshow--inline .slideshow-slide img {
  max-height: var(--carousel-inline-h);
}

/* Centred under the image rather than pinned to the container corner, so the cluster
   stays optically attached to the photo whatever width it resolves to. WCAG 2.2.2
   needs this to be persistently visible, not revealed on hover. */
.slideshow-controls {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: 0.5vw;
  display: flex;
  gap: 0.1vw;
  z-index: 2;
}

/* Also unthemed, and for the same reason as the lightbox controls: these overlay the
   photo itself. White over a black shadow is what keeps them legible on any frame. */
.slideshow-btn {
  background: none;
  border: 0;
  color: #fff;
  font-family: inherit;
  font-size: 1.1vw;
  line-height: 1;
  padding: 0.4em 0.55em;
  cursor: pointer;
  opacity: 0.55;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9);
  transition: opacity 0.15s ease;
}

.slideshow-btn:hover,
.slideshow-btn:focus-visible {
  opacity: 1;
}

/* --- embedded video --- */

/* Cargo rendered these behind the page content, centred and contained (measured on
   the original: object-fit contain, centred, z-index 1 under the copy). */
/* Sits in the normal content flow rather than behind the copy, so the player is
   fully interactive. Takes whatever vertical space the credits leave over (flex
   height, width derived from the aspect ratio), so the page fits the viewport
   without scrolling regardless of how much copy sits beneath it. */
.page-video {
  --box-w: calc(100vw - var(--pad-x) * 2 - var(--col-sidebar) - var(--col-gap));
  flex: 1 1 auto;
  min-height: 0;
  aspect-ratio: 16 / 9;
  width: auto;
  max-width: var(--box-w);
  /* The auto margin has to stay on one side: it is what stops the flex column from
     stretching this to the full column width, which would fight the aspect ratio.
     Keeping it on the right pins the player to the left edge, so the credits below
     line up with it. */
  margin: 0 auto 1.5em 0;
  position: relative;
  /* Letterbox behind a YouTube iframe, not page chrome - stays black. */
  background: #000;
}

.page-video iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* --- The Tale of Becoming; Blind --- */

/* Stacks a carousel, a block of copy, and its own photo grid. Its content is inset
   from the left compared to the plain gallery pages. */
.ttobb {
  padding-left: var(--ttobb-inset);
}

.ttobb .prose {
  font-size: 2.667vw;
  line-height: 1.1;
  text-align: justify;
  margin: 1.4em 0;
}

.ttobb .prose p {
  margin: 0 0 1.1em;
}

.ttobb .prose p:last-child {
  margin-bottom: 0;
}

.ttobb .gallery {
  gap: var(--gap-gallery-ttobb);
}

/* --- text pages --- */

/* Vertically centred in the viewport on the original, and each one has its own type
   scale (measured as a fraction of viewport width). */
.text-page {
  max-width: 100%;
  min-height: calc(100vh - var(--pad-2x));
  min-height: calc(100dvh - var(--pad-2x));
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.text-page p {
  margin: 0 0 1.2em;
}

.text-page p:last-child {
  margin-bottom: 0;
}

.text-page--music {
  font-size: 2.664vw;
  line-height: 1.1;
}

.text-page--contact {
  font-size: 0.8vw;
  line-height: 1.1;
  text-align: center;
}

/* Credits sit directly beneath the player, sharing its left edge. */
.text-page--credits {
  font-size: 1vw;
  line-height: 1.2;
  text-align: left;
}

/* Two columns under the video: the links people click on the left, the
   production credits on the right. flex-start keeps both columns hanging from
   the top of the row rather than centring the shorter one against the taller. */
.credits-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 2em;
}

.credits-meta {
  text-align: right;
}

.credits-row p:last-child {
  margin-bottom: 0;
}

.text-page.center {
  text-align: center;
}

/* --- mobile navigation bar --- */

/* Exists only below the breakpoint, where the sidebar stops being a column and
   becomes a sheet. Its layout lives in the media query, but the typography belongs
   here - a <button> otherwise falls back to the UA's 13px system font, which is not
   something a display:none rule should be hiding. */
.mobile-bar {
  display: none;
}

.mobile-bar-brand,
.mobile-bar-toggle {
  font-family: inherit;
  font-weight: inherit;
  font-size: 16px;
  line-height: 1;
  color: var(--fg);
  background: none;
  border: 0;
  cursor: pointer;
}

/* --- mobile --- */

/* Below this width the viewport-proportional scale would be unreadable, so switch to
   fixed sizes and stack the layout. Retuning the tokens here is what keeps the
   carousel and gallery gaps correct - previously more-specific selectors like
   .ttobb .gallery silently beat the overrides in this block. */
@media (max-width: 900px) {
  :root {
    --pad-y: 30px;
    --pad-x: 20px;
    --gap-gallery: 6px;
    --gap-gallery-ttobb: 6px;
    --ttobb-inset: 0px;
    --carousel-inline-h: none;

    /* The sidebar stacks above the content here, so it costs no horizontal space.
       Leaving these at their desktop values kept .page-video's --box-w subtracting a
       column that is not there, which rendered the player ~30% narrower than the
       content around it. .layout overrides both properties that use them below. */
    --col-sidebar: 0px;
    --col-gap: 0px;
  }

  body {
    font-size: 16px;
    line-height: 1.35;
  }

  /* --bar-h plus the home indicator is what every fixed thing here measures against. */
  :root {
    --bar-h: 52px;
    --bar-total: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px));
  }

  .layout {
    grid-template-columns: 1fr;
    column-gap: 0;
    row-gap: 0;
    /* The bar is fixed, so the last row of content has to be told to clear it. */
    padding-bottom: calc(var(--bar-total) + var(--pad-y));
  }

  /* The sidebar becomes the index sheet - the same element and the same nav, taken out
     of flow and parked below the fold. Taking it out of flow is the whole point: the
     grid collapses to the content column alone, so the work starts at the top of the
     page instead of ~505px down it. */
  .sidebar {
    position: fixed;
    inset: 0;
    z-index: 110;
    height: 100vh;
    height: 100dvh;
    justify-content: flex-start;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--pad-y) var(--pad-x) calc(var(--bar-total) + var(--pad-y));
    background: var(--bg);
    transform: translateY(100%);
    visibility: hidden;
    /* visibility flips only once the slide has finished, or the sheet would vanish
       mid-transition on the way out. */
    transition: transform 240ms ease, visibility 0s linear 240ms;
  }

  body.index-open .sidebar {
    transform: translateY(0);
    visibility: visible;
    transition: transform 240ms ease, visibility 0s;
  }

  body.index-open {
    overflow: hidden;
  }

  /* The bar already carries the wordmark; two of them is one too many. */
  .logo-link {
    display: none;
  }

  /* Work at a size worth tapping, everything else quieter. */
  .nav a {
    font-size: 26px;
    line-height: 1.2;
    padding: 8px 0;
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 200ms ease, transform 200ms ease, color 0.2s ease;
  }

  body.index-open .nav a {
    opacity: 1;
    transform: translateY(0);
    /* Staggered off --i, set per link in sidebar.njk. Last item lands under 300ms. */
    transition-delay: calc(var(--i) * 25ms);
  }

  .nav a[data-group="info"] {
    font-size: 16px;
    padding: 6px 0;
  }

  /* Seven bodies of work above, everything else below. */
  .nav a.group-start {
    margin-top: 22px;
    padding-top: 22px;
    border-top: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
  }

  /* --- the bar itself --- */

  .mobile-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    position: fixed;
    inset: auto 0 0 0;
    z-index: 120;
    height: var(--bar-total);
    padding: 0 var(--pad-x) env(safe-area-inset-bottom, 0px);
    background: var(--bg);
    border-top: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
  }

  /* Pads each to the full height of the bar, so both are 52px targets. */
  .mobile-bar-brand,
  .mobile-bar-toggle {
    padding: 18px 0;
  }

  .mobile-bar-toggle[aria-expanded="true"] {
    color: var(--accent);
  }

  .gallery {
    grid-template-columns: repeat(2, 1fr);
  }

  /* With the sidebar out of the flow, the home page is a single image in an otherwise
     empty column, and top-aligning it left a dead third of the screen underneath.
     Centre it in exactly the space between the top padding and the bar. */
  .hero {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: calc(100vh - var(--pad-2x) - var(--bar-total));
    min-height: calc(100dvh - var(--pad-2x) - var(--bar-total));
  }

  .hero img,
  .slideshow-slide img {
    max-height: none;
    width: 100%;
  }

  .slideshow-controls {
    bottom: 8px;
    gap: 2px;
  }

  .slideshow-btn {
    font-size: 18px;
  }

  /* Layout stacks here, so the video just takes the full column width. */
  .page-video {
    width: 100%;
    margin-bottom: 16px;
  }

  /* The one block that never got a mobile size: 2.667vw resolves to about 10px here.
     It stays a step above body copy because it is a statement, not body copy - but
     the justification has to go, since at this measure it opens rivers. */
  .ttobb .prose {
    font-size: 20px;
    line-height: 1.25;
    text-align: left;
    margin: 1.2em 0;
  }

  .text-page {
    min-height: 0;
    display: block;
  }

  .text-page--music {
    font-size: 22px;
    line-height: 1.2;
  }

  .text-page--contact {
    font-size: 16px;
    text-align: left;
  }

  /* Looser than the desktop 1.2: these pages stack runs of links one per line, and at
     19px apart they are an awkward thumb target. */
  .text-page--credits {
    font-size: 16px;
    line-height: 1.5;
  }

  /* Too narrow for two columns - stack them and let everything read left. */
  .credits-row {
    flex-direction: column;
    gap: 0;
  }

  .credits-meta {
    text-align: left;
    margin-top: 1.2em;
  }
}

@media (max-width: 480px) {
  .gallery {
    grid-template-columns: 1fr;
  }
}

/* --- reduced motion --- */

/* Auto-advance is switched off in JS as well; this covers the transitions. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .gallery-item:hover img {
    transform: none;
  }
}
