:root {
  color-scheme: light dark;
  --gap: 4px;
  --bg-base: #f7f7f5;
  --fg: #111;
  --muted: #666;

  /* One tint for every wave layer. The light-to-deep gradient down the field
     is not painted — it accumulates, because each layer covers everything
     below its own crest, so the further down you look the more layers have
     stacked. Sixteen layers at this alpha land around 0.78 at the bottom;
     changing the layer count means re-deriving the alpha or the field shifts
     weight.

     Split into channels and alpha so the easter-egg palettes below can
     restate the hue without knowing which theme is showing — the alpha is not
     shared between them, since the tint sits over a near-white page in one
     and a near-black one in the other. --wave-ink re-resolves on its own
     whenever either part changes. */
  --wave-rgb: 64 116 196;
  --wave-alpha: 0.09;
  --wave-ink: rgb(var(--wave-rgb) / var(--wave-alpha));
}

/* Tapping the masthead cycles these; js/waves.js counts, the colours live
   here. data-waves="0" is deliberately undeclared so the cycle returns to the
   default above rather than to a copy of it. */
:root[data-waves="1"] {
  --wave-rgb: 38 138 140;
}

:root[data-waves="2"] {
  --wave-rgb: 120 96 200;
}

:root[data-waves="3"] {
  --wave-rgb: 206 116 60;
}

:root[data-waves="4"] {
  --wave-rgb: 196 76 120;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg-base: #111113;
    --fg: #eee;
    --muted: #999;

    /* Lighter and weaker than the light-theme values: this tint accumulates
       over a near-black page, so it reads as light on dark rather than dark
       on light. Every palette needs its own value here for the same reason. */
    --wave-rgb: 96 150 225;
    --wave-alpha: 0.058;
  }

  :root[data-waves="1"] {
    --wave-rgb: 74 178 178;
  }

  :root[data-waves="2"] {
    --wave-rgb: 150 130 230;
  }

  :root[data-waves="3"] {
    --wave-rgb: 226 146 84;
  }

  :root[data-waves="4"] {
    --wave-rgb: 222 112 152;
  }
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: var(--bg-base);
  color: var(--fg);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}

/* Wave field --------------------------------------------------------------
   Twelve stacked layers of the same sine crest, each one stepped down and
   nudged sideways from the one before. The sideways step is what braids them
   — without it the layers sit in phase and the whole thing collapses into one
   shape with stripes on it.

   The crests are inline SVG rather than CSS gradients because a gradient edge
   is always soft; a path edge is crisp, which is the whole point. Inline SVG
   is DOM, not an image load, so the `img-src 'self'` CSP does not apply — a
   data: URI in a background or mask WOULD be blocked, which is why this isn't
   one. Fills come from the stylesheet, so both themes work off one variable.

   The layers are split across two <svg> elements that drift at different
   speeds and in opposite directions, so the braid keeps rearranging itself.
   The split is also what keeps this cheap: a transform on an <svg> element
   composites on the GPU, while animating the twelve <use> nodes individually
   would repaint the whole field on the main thread every frame, forever. */

/* Absolute, not fixed: the field belongs to the masthead, so it should scroll
   away with it. Pinned to the viewport it stayed on screen the whole way down
   the page, and any thumbnail that had not faded in yet left it showing
   through mid-scroll.

   The crests only occupy the top of this box. The rest is the solid colour
   they accumulate to, which exists purely to give the mask below something to
   fade out over — hence the height being roughly double the wave zone. */
.waves {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: max(68vh, 34rem);
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  /* Fades the field out rather than ending it on a cut. This masks the waves
     to transparent instead of painting the page colour over them: an overlay
     has to match --bg-base exactly, and an opaque patch of it sitting in this
     stacking context still came out about one value off the canvas behind it,
     which is enough to leave a hairline. Fading to real transparency means
     there is no colour to match, in either theme. */
  -webkit-mask-image: linear-gradient(to bottom, #000 50%, transparent 88%);
  mask-image: linear-gradient(to bottom, #000 50%, transparent 88%);
}

/* Holds the one <path> both bands point at. Not drawn. */
.wave-defs {
  position: absolute;
  width: 0;
  height: 0;
}

.wave-band {
  position: absolute;
  top: 0;
  left: -50%;
  width: 200%;
  height: 100%;
  will-change: transform;
  animation: wave-drift var(--drift-time) linear infinite;
}

/* fill is animatable where the custom property behind it is not, so the
   palette change cross-fades instead of snapping. */
.wave-band use {
  fill: var(--wave-ink);
  transition: fill 0.6s ease;
}

/* The path repeats every 150 user units and the viewBox is 1200 wide across
   an element of 200% container width, so one period is 12.5% of that element
   — four crests across the viewport. Drifting by exactly one period lands the
   next crest where the last one was and the loop is invisible; any other
   distance jumps on restart, so this figure and the path's period in the HTML
   have to be changed together. */
@keyframes wave-drift {
  from {
    transform: translate3d(0, 0, 0);
  }
  to {
    transform: translate3d(var(--drift), 0, 0);
  }
}

/* Both bands travel the same way, and all the life comes from the difference
   in their speeds: the braid between them shifts by a full period every ~68s.
   The times are shorter than they look — a period is now half the width it
   used to be, so the same duration would have read as half the pace. */
.wave-band-a {
  --drift: -12.5%;
  --drift-time: 24s;
}

.wave-band-b {
  --drift: -12.5%;
  --drift-time: 37s;
}

.site-header {
  padding: 1.5rem 1rem 1rem;
}

.site-header h1 {
  margin: 0.25rem 0;
  font-size: 1.5rem;
  font-weight: 600;
}

/* Home page only: the masthead is the stage for the wave field, so it gets
   the height to show it. The post pages keep the compact header — there the
   photo grid is the subject. */
.site-header--hero {
  padding: clamp(3.5rem, 13vh, 7rem) 1rem clamp(2rem, 6vh, 3.5rem);
}

.site-header--hero h1 {
  margin: 0;
  font-size: clamp(2.75rem, 10vw, 5.5rem);
  line-height: 0.95;
  letter-spacing: -0.04em;
}

.back-link {
  color: var(--muted);
  text-decoration: none;
  font-size: 0.9rem;
}

.back-link:hover {
  text-decoration: underline;
}

.post-date {
  margin: 0;
  color: var(--muted);
  font-size: 0.9rem;
}

.cover-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--gap);
  padding: 0 var(--gap) var(--gap);
}

.thumb-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  grid-auto-rows: var(--gap);
  grid-auto-flow: dense;
  gap: var(--gap);
  padding: 0 var(--gap) var(--gap);
}

.cover {
  position: relative;
  display: block;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  color: #fff;
  text-decoration: none;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.55s ease, transform 0.55s cubic-bezier(0.2, 0.7, 0.3, 1);
  /* Must stay after the shorthand above, which resets delay to 0. main.js
     gives each cover its own --reveal-delay so the grid assembles in no
     particular order; the range is kept short so the page still feels quick. */
  transition-delay: var(--reveal-delay, 0s);
}

.cover.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scale(1);
  transition: transform 0.5s ease;
}

.cover:hover img,
.cover:focus-visible img {
  transform: scale(1.06);
}

/* The same swell as hover, out and back in one pass. An animation rather than
   a transition because it has to return by itself, with nothing to trigger the
   way back.

   Both halves must match the hover transition above, which means the same
   duration AND the same curve. An animation's timing function applies to each
   keyframe segment, so `ease` here runs the swell out and back exactly as
   hover runs it in and out: 6% in half a second, on the same curve.

   ease-in-out is the trap. It reads as much slower than hover despite an
   identical duration, because it leaves near-zero velocity at both ends — a
   tenth of a second in it has moved 8% of the way against ease's 30% — and
   those crawling tails look like dropped frames rather than easing.

   Duration also sets how many tiles are mid-swell at once, together with the
   gap in main.js: roughly this duration divided by that gap. */
@keyframes cover-drift {
  50% {
    transform: scale(1.06);
  }
}

.cover.is-drifting img {
  animation: cover-drift 1.4s ease;
}

.cover-title {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 2rem 0.6rem 0.6rem;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0.5) 55%, transparent);
  color: rgba(255, 255, 255, 0.8);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
  font-size: 1rem;
  font-weight: 600;
}

/* Only ever rendered with JS off, where the grid script never runs. Plain
   column: without measurement there is nothing to build a masonry from. */
.noscript-grid {
  list-style: none;
  margin: 0;
  padding: 0 var(--gap) var(--gap);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--gap);
}

.noscript-grid img {
  width: 100%;
  height: auto;
  display: block;
}

/* Foot of a post. The older link is pushed right by margin-left:auto rather
   than space-between, so it stays on the right even when it is the only one. */
.post-nav {
  display: flex;
  gap: var(--gap);
  padding: 1.5rem var(--gap) 3rem;
}

.post-nav[hidden] {
  display: none;
}

.post-nav-link {
  color: var(--fg);
  text-decoration: none;
  font-weight: 600;
  max-width: 45%;
}

.post-nav-older {
  margin-left: auto;
  text-align: right;
}

.post-nav-label {
  display: block;
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

.post-nav-title {
  display: block;
  margin-top: 0.15rem;
}

.post-nav-link:hover .post-nav-title,
.post-nav-link:focus-visible .post-nav-title {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Failure and empty states. Spans the grid so it centres properly whether it
   is dropped into the cover grid or the thumbnail grid. */
.notice {
  grid-column: 1 / -1;
  max-width: 34rem;
  margin: 4rem auto;
  padding: 0 1rem;
  text-align: center;
  font-size: 1.05rem;
  font-weight: 600;
}

.notice-detail,
.notice-link {
  display: block;
  margin-top: 0.5rem;
  font-size: 0.9rem;
  font-weight: 400;
  color: var(--muted);
}

.notice-link {
  margin-top: 1.25rem;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.notice-link:hover,
.notice-link:focus-visible {
  color: var(--fg);
}

/* Sits under the title inside the same overlay, quiet enough that the title
   still reads first. */
.cover-count {
  display: block;
  margin-top: 0.1rem;
  font-size: 0.78rem;
  font-weight: 400;
  letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.62);
}

.thumb-cell {
  display: block;
  overflow: hidden;
  cursor: zoom-in;
  background: #0001;
  border: 0;
  padding: 0;
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.thumb-cell.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.thumb-cell img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scale(1);
  transition: transform 0.5s ease;
}

.thumb-cell:hover img,
.thumb-cell:focus-visible img {
  transform: scale(1.06);
}

.lightbox {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}

.lightbox[hidden] {
  display: none;
}

.lightbox img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  opacity: 0;
  transform: scale(0.98);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.lightbox img.is-loaded {
  opacity: 1;
  transform: scale(1);
}

.lightbox-close {
  position: absolute;
  top: 1rem;
  right: 1.25rem;
  background: none;
  border: none;
  color: #fff;
  font-size: 2.5rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.25rem 0.5rem;
}

.lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  color: #fff;
  font-size: 3rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.5rem 0.75rem;
  opacity: 0.7;
  transition: opacity 0.2s ease;
}

.lightbox-nav:hover,
.lightbox-nav:focus-visible {
  opacity: 1;
}

.lightbox-prev {
  left: 0.5rem;
}

.lightbox-next {
  right: 0.5rem;
}

.lightbox-counter {
  position: absolute;
  bottom: 1rem;
  left: 50%;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, 0.75);
  font-size: 0.85rem;
  letter-spacing: 0.03em;
}

@media (prefers-reduced-motion: reduce) {
  /* The wave field stays, it just holds still. */
  .wave-band {
    animation: none;
  }

  /* Belt and braces: main.js also never starts the timer under this setting. */
  .cover.is-drifting img {
    animation: none;
  }

  .cover,
  .cover img,
  .thumb-cell,
  .thumb-cell img,
  .lightbox img {
    transition: none;
  }

  .cover,
  .thumb-cell {
    opacity: 1;
    transform: none;
  }

  .lightbox img {
    opacity: 1;
    transform: none;
  }
}
