@font-face {
  font-family: "Departure Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/DepartureMono-Regular.woff2") format("woff2");
}

@font-face {
  font-family: "Newsreader";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/newsreader-latin-400-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Newsreader";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/newsreader-latin-400-italic.woff2") format("woff2");
}

/* Registered so the theme switch interpolates them (one transition on <html>). */
@property --paper {
  syntax: "<color>";
  inherits: true;
  initial-value: #faf9f6;
}

@property --ink {
  syntax: "<color>";
  inherits: true;
  initial-value: #353431;
}

@property --surface {
  syntax: "<color>";
  inherits: true;
  initial-value: #faf9f6;
}

:root {
  --z-canvas: 0;
  --z-logo: 10;
  --z-page: 100;
  --z-header-scrim: 150;
  --z-chrome: 200;

  --paper: #faf9f6;
  --surface: #faf9f6;
  --ink: #353431;

  --font-serif: "Newsreader", Georgia, serif;
  --font-mono: "Departure Mono", ui-monospace, Menlo, monospace;

  --step-0: clamp(1.0625rem, 0.98rem + 0.35vw, 1.25rem);
  --step-1: clamp(1.25rem, 1.1rem + 0.7vw, 1.625rem);
  --step-2: clamp(1.5rem, 1.2rem + 1.5vw, 2.375rem);
  --step-3: clamp(2rem, 1.2rem + 3.2vw, 3.75rem);
  /* Departure Mono is drawn on an 11px pixel grid; stay on it for crisp glyphs. */
  --mono-size: 0.6875rem;
  --lh: 1.6;
  --measure: 68ch;
  --measure-wide: 64rem;

  --frame-inline: clamp(20px, 5.2083vw, 75px);
  --frame-block: clamp(20px, 5.2083vw, 75px);
  --chrome-line: 1.5rem;
  --page-pad-block: calc(var(--frame-block) + var(--chrome-line) + 1.5rem);

  --ease: cubic-bezier(0.333333, 0, 0.666667, 1);
  --dur-theme: 900ms;

  /* Alpha-only grain (dark specks by day, light by night) over a flat colour. */
  --paper-texture: url("/paper.png");
  --tint: 97%;
  --surface-pad: clamp(1.25rem, 4vw, 3.5rem);
}

html {
  transition:
    --paper var(--dur-theme) var(--ease),
    --ink var(--dur-theme) var(--ease),
    --surface var(--dur-theme) var(--ease);
}

/* Night palette; set on <html> by the inline head script and site.js. */
.theme-night {
  --paper: #1a1a1a;
  --surface: #2e2d2b;
  --ink: #e8e6e1;
  --paper-texture: url("/paper-dark.png");
  color-scheme: dark;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-serif);
  font-size: var(--step-0);
  line-height: var(--lh);
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
}

/* Set on <html> by ocean.js when WebGL is unavailable. */
.no-webgl body {
  background-image: var(--paper-texture);
}

h1,
h2,
h3 {
  font-weight: 400;
  line-height: 1.15;
}

h1 {
  font-size: var(--step-3);
}

h2 {
  font-size: var(--step-2);
}

h3 {
  font-size: var(--step-1);
}

/* Rule (AGENTS.md): every hyperlink is underlined at rest, always 1px; hover
   and current-page states never change or remove it. */
a {
  color: inherit;
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

/* Hover: a barely-there white wash fades in behind the link. Its box is the
   padding, cancelled by an equal negative margin, so text and layout don't
   move (inline links ignore the vertical part; block-level ones need it); clone
   keeps wrapped lines tidy. Square corners, whole pixels: the aesthetic is
   pixel/mono. */
a {
  --link-pad-x: 10px;
  --link-pad-y: 5px;
  padding: var(--link-pad-y) var(--link-pad-x);
  margin: calc(-1 * var(--link-pad-y)) calc(-1 * var(--link-pad-x)); /* vertical part only matters for block-level links (logo, nav) */
  border-radius: 0;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-color 240ms var(--ease);
}

@media (hover: hover) {
  a:hover {
    background-color: rgb(255 255 255 / 0.07);
  }
}

#ocean {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: var(--z-canvas);
  pointer-events: none;
  /* Backing store is 0.35–0.4× of CSS size; let the browser smooth the upscale. */
  image-rendering: auto;
  opacity: 0;
  transition: opacity 1000ms var(--ease);
}

/* Set on <html> by ocean.js once a quality tier is settled and a frame drawn. */
.shader-ready #ocean {
  opacity: 1;
}

.logo {
  position: fixed;
  top: var(--frame-block);
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-logo);
  /* Same face/size as the nav items (owner decision). */
  font-family: var(--font-mono);
  font-size: var(--mono-size);
  line-height: var(--chrome-line);
  white-space: nowrap;
  /* Centred by translateX(-50%) of its own (padded) box: the link hover box's
     symmetric padding already stays centred, a negative side margin would not. */
  margin-left: 0;
  margin-right: 0;
}

.page {
  position: relative;
  z-index: var(--z-page);
  padding: var(--page-pad-block) var(--frame-inline);
  /* Let clicks through the padding to the logo underneath. */
  pointer-events: none;
}

.page > * {
  pointer-events: auto;
}

/* Home <-> Digital Sun: slow whole-page crossfade (site.js, View Transitions).
   The per-page swap fade is off meanwhile, or the new page would fade twice. */
:root {
  --sun-crossfade: 1400ms;
}

html.sun-crossfade::view-transition-old(root),
html.sun-crossfade::view-transition-new(root) {
  animation-duration: var(--sun-crossfade);
  animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); /* symmetric: 50/50 at the midpoint */
}

html.sun-crossfade .page.htmx-swapping #main > *,
html.sun-crossfade .page.htmx-added #main > * {
  opacity: 1;
  transform: none;
  transition: none;
}

/* ...but the faint sky layer keeps its own opacity (else it flashes to 100%). */
html.sun-crossfade .page.htmx-swapping #main > .sky-video,
html.sun-crossfade .page.htmx-added #main > .sky-video {
  opacity: var(--sky-video-opacity);
}

/* htmx page swap: old content fades out during the 220ms swap delay, new
   content fades in once htmx drops .htmx-added at settle. Animated below
   .page, because opacity/transform on an ancestor of .content-surface would
   break its backdrop-filter. */
#main > * {
  transition: opacity 220ms var(--ease), transform 220ms var(--ease);
}

.page.htmx-swapping #main > *,
.page.htmx-added #main > * {
  opacity: 0;
  transform: translateY(0.5rem);
}

#main:focus {
  outline: none;
}

/* Short landscape screens: tighter page padding under the header. */
@media (max-height: 30rem) {
  :root {
    --page-pad-block: calc(var(--frame-block) + var(--chrome-line) + 0.5rem);
  }
}

.chrome {
  position: fixed;
  left: 0;
  right: 0;
  z-index: var(--z-chrome);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: 0 var(--frame-inline);
  font-family: var(--font-mono);
  font-size: var(--mono-size);
  line-height: var(--chrome-line);
}

header.chrome {
  top: var(--frame-block);
  justify-content: flex-end;
  pointer-events: none;
}

header.chrome a,
header.chrome summary {
  pointer-events: auto;
}

.nav-inline {
  display: flex;
  gap: 1.5rem;
}

.menu {
  display: none;
}

footer.chrome {
  bottom: 0;
  padding-top: 2rem;
  padding-bottom: var(--frame-block);
  /* Soft paper scrim so the footer reads over dark water troughs. */
  background: linear-gradient(to top, color-mix(in srgb, var(--paper) 85%, transparent) calc(var(--frame-block) + var(--chrome-line)), transparent);
  pointer-events: none;
}

/* Content pages: paper scrim under the header (like the footer scrim, but
   opaque behind the text line) so scrolled panel text never shows through the
   logo and nav. */
body:has(.content-surface)::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-header-scrim);
  height: calc(var(--frame-block) + var(--chrome-line) + 2rem);
  background: linear-gradient(to bottom, var(--paper) calc(var(--frame-block) + var(--chrome-line)), transparent);
  pointer-events: none;
}

body:has(.content-surface) footer.chrome {
  background: linear-gradient(to top, var(--paper) calc(var(--frame-block) + var(--chrome-line)), transparent);
}

/* Keep focused/skip-link targets clear of the opaque header and footer scrims. */
html {
  scroll-padding-block: calc(var(--frame-block) + var(--chrome-line) + 2rem);
}

body:has(.content-surface) .logo {
  z-index: calc(var(--z-header-scrim) + 1);
}

footer.chrome > * {
  pointer-events: auto;
}

footer.chrome small {
  font-size: inherit;
}

@media (max-width: 39.99rem) {
  .nav-inline {
    display: none;
  }

  .menu {
    display: block;
  }

  .menu summary {
    position: relative;
    z-index: 1;
    list-style: none;
    cursor: pointer;
  }

  .menu summary::-webkit-details-marker {
    display: none;
  }

  .menu-panel {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.75rem;
    padding: calc(var(--frame-block) + var(--chrome-line) + 1rem) var(--frame-inline) 4rem;
    background: var(--paper) var(--paper-texture);
    -webkit-mask-image: linear-gradient(#000 calc(100% - 3rem), transparent);
    mask-image: linear-gradient(#000 calc(100% - 3rem), transparent);
    font-size: calc(var(--mono-size) * 2);
    line-height: 1.5;
    pointer-events: auto;
  }
}

@media (max-width: 39.99rem) and (prefers-reduced-motion: no-preference) {
  .menu[open] .menu-panel {
    animation: menu-in 220ms var(--ease);
  }
}

@media (prefers-reduced-motion: reduce) {
  .page.htmx-swapping #main > *,
  .page.htmx-added #main > * {
    transform: none;
  }
}

@keyframes menu-in {
  from {
    opacity: 0;
    transform: translateY(-0.5rem);
  }
}

img {
  max-width: 100%;
  height: auto;
}

/* Content pages: paper panel; the ocean behind it is slowed by site.js. */
.content-surface {
  box-sizing: content-box;
  max-width: var(--measure);
  margin-inline: auto;
  padding: var(--surface-pad);
  background-color: color-mix(in srgb, var(--surface) var(--tint), transparent);
  background-image: var(--paper-texture);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  overflow-wrap: break-word;
}

.content-surface.wide {
  max-width: var(--measure-wide);
}

.content-surface > :first-child,
.content-surface > header > :first-child {
  margin-top: 0;
}

.content-surface > :last-child {
  margin-bottom: 0;
}

.content-surface h1 {
  margin: 0 0 0.75em;
}

/* 404 page: mono, on the 11px pixel grid. */
.content-surface .not-found {
  font-family: var(--font-mono);
  font-size: calc(var(--mono-size) * 4);
}

.content-surface h2 {
  margin: 2em 0 0.5em;
}

.content-surface h3 {
  margin: 1.75em 0 0.5em;
}

.content-surface p,
.content-surface ul,
.content-surface ol,
.content-surface blockquote,
.content-surface pre {
  margin: 0 0 1.25em;
}

.content-surface ul,
.content-surface ol {
  padding-left: 1.25em;
}

.content-surface li + li {
  margin-top: 0.25em;
}

.content-surface blockquote {
  margin-inline: 0;
  padding-left: 1em;
  border-left: 1px solid currentColor;
  font-style: italic;
}

.content-surface hr {
  margin: 2em 0;
  border: 0;
  border-top: 1px solid color-mix(in srgb, currentColor 25%, transparent);
}

.content-surface code,
.content-surface pre,
.content-surface time,
.content-surface .meta {
  font-family: var(--font-mono);
  font-size: var(--mono-size);
}

.content-surface pre {
  overflow-x: auto;
  line-height: 1.5;
}

.content-surface .meta {
  opacity: 0.8;
}

.content-surface > header {
  margin-bottom: 2em;
}

.content-surface > header h1 {
  margin-bottom: 0.25em;
}

.content-surface > header .meta {
  margin: 0;
}

.content-surface .post-list {
  padding: 0;
  list-style: none;
}

.post-list li + li {
  margin-top: 1.5em;
}

.post-list a {
  font-size: var(--step-1);
}

.post-list .meta {
  margin: 0.25em 0 0;
}


/* Video page: full-viewport loop on black over its poster; site.js starts
   playback and pauses the ocean. Light chrome in both themes, dimmed until
   hovered or focused. */
.video-page {
  position: fixed;
  inset: 0;
  background: #000;
}

/* Opacity-only swap fade: a transformed ancestor would break position: fixed. */
.page.htmx-swapping #main > .video-page,
.page.htmx-added #main > .video-page {
  transform: none;
}

/* Frame centre is shifted by --video-shift (positive = right, negative =
   left) to centre the lamp, and widened by twice |shift| so it still covers
   the viewport: one edge sits on the viewport edge, the other overflows. */
.video-page {
  --video-shift: 3%;
}

/* Matches the portrait encodes' breakpoint in video.html. */
@media (orientation: portrait) {
  .video-page {
    --video-shift: -1.5%;
  }
}

.video-poster img,
.video-page video {
  position: fixed;
  inset: 0 auto 0 min(0%, 2 * var(--video-shift));
  display: block;
  width: calc(100% + 2 * max(var(--video-shift), -1 * var(--video-shift)));
  max-width: none;
  height: 100%;
  object-fit: cover;
}

/* Autoplaying: no poster, the video fades in from black (see html.video-ok). */
.video-ok .video-poster {
  visibility: hidden;
}

/* The faint home sun keeps its poster until the video really plays (the
   colour step is invisible at 10%): if iOS defers play() (e.g. after the
   back crossfade) the sun stays, at worst still, instead of vanishing. */
.video-ok .sky-video:has(video.playing):not(.video-failed) picture {
  visibility: hidden;
  transition: visibility 0s 900ms; /* hide once the video has faded in */
}

/* Except while crossfading in from home: the incoming page must show the sun
   from its first frame (the poster is cached from home), not black while the
   video starts; the poster/video colour step then falls inside the blend. */
html.sun-crossfade.video-ok .video-page .video-poster,
.video-ok .video-failed .video-poster {
  visibility: visible;
}

.video-page video {
  /* Not 0: iOS won't start a muted video that isn't visible, and this one
     only becomes opaque once it plays. 0.001 is invisible but counts. */
  opacity: 0.001;
  transition: opacity 900ms var(--ease);
}

.video-page video.playing {
  opacity: 1;
}

/* iOS Low Power Mode: hide the start-playback overlay; the poster stays. */
.video-page video::-webkit-media-controls,
.video-page video::-webkit-media-controls-start-playback-button {
  display: none !important;
  -webkit-appearance: none;
}

.video-caption {
  position: fixed;
  left: var(--frame-inline);
  bottom: calc(var(--frame-block) + var(--chrome-line) + 0.5rem);
  z-index: 1;
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--mono-size);
  font-weight: 400;
  line-height: var(--chrome-line);
}

body:has(.video-page) {
  --paper: #1a1a1a;
  --ink: #e8e6e1;
  --paper-texture: url("/paper-dark.png");
  color-scheme: dark;
  background: #000;
}

body:has(.video-page) footer.chrome {
  background: none;
}

body:has(.video-page) .logo {
  z-index: calc(var(--z-page) + 1);
}

body:has(.video-page) :is(.logo, .chrome, .video-caption) {
  opacity: 0.55;
  text-shadow: 0 0 0.75rem #000;
  transition: opacity 220ms var(--ease), background-color 240ms var(--ease);
}

body:has(.video-page) :is(.logo, .chrome, .video-caption):is(:hover, :focus-within),
body:has(.video-page) .chrome:has(details[open]) {
  opacity: 1;
}

/* Digital Sun, faint in the home sky: the whole layer is 10% opaque above the
   sea line and fades out just below it. Same box as #ocean (fixed, inset 0),
   so the shader horizon is a fixed fraction of this box too: 38.2% from the
   bottom (CAMERA_PITCH in ocean.frag.glsl; change both together). The water
   just below that line is flat haze (waves show from ~34.5%), so the sun
   carries on through it and fades out where the waves begin; cutting it at
   the line itself leaves a sky-coloured gap above the sea.
   A second, round mask keeps only the orb itself: its disc has a sharp edge at
   0.175 of the video frame's height, centred 0.06 below the frame's middle
   (horizontally by --video-shift). Everything outside it (a dark red ring,
   the black room, the red square frame) is darker than the sky and would show
   as a black halo, so the mask fades out by 0.18. Sizes follow the frame's
   on-screen height under object-fit: cover. */
.sky-video {
  --sky-video-opacity: 0.1;
  --sky-horizon: 38.2%;
  --sky-haze: 3.7%; /* misty water below the horizon line, before waves show */
  --video-shift: 3%;
  --frame-h: max(100vh, 100vw * 1.06 * 9 / 16); /* 16:9 frame, widened by 2 × shift */
  --orb-mask: radial-gradient(circle at 50% calc(50% + var(--frame-h) * 0.06),
    #000 calc(var(--frame-h) * 0.165),
    transparent calc(var(--frame-h) * 0.18));
  --horizon-mask: linear-gradient(to top,
    transparent calc(var(--sky-horizon) - var(--sky-haze)),
    #000 calc(var(--sky-horizon) - 0.5%));
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  opacity: var(--sky-video-opacity);
  -webkit-mask-image: var(--horizon-mask), var(--orb-mask);
  -webkit-mask-composite: source-in;
  mask-image: var(--horizon-mask), var(--orb-mask);
  mask-composite: intersect;
}

/* Matches the portrait encodes' breakpoint in index.html. */
@media (orientation: portrait) {
  .sky-video {
    --video-shift: -1.5%;
    --frame-h: max(100vh, 100vw * 1.03 * 5 / 4); /* 4:5 portrait encodes */
  }
}

/* Opacity-only swap fade: a transformed ancestor would break position: fixed. */
.page.htmx-swapping #main > .sky-video,
.page.htmx-added #main > .sky-video {
  transform: none;
}

/* Frame centre is shifted by --video-shift (positive = right, negative =
   left) to centre the lamp, and widened by twice |shift| so it still covers
   the viewport: one edge sits on the viewport edge, the other overflows. */
.sky-video img,
.sky-video video {
  position: absolute;
  inset: 0 auto 0 min(0%, 2 * var(--video-shift));
  display: block;
  width: calc(100% + 2 * max(var(--video-shift), -1 * var(--video-shift)));
  max-width: none;
  height: 100%;
  object-fit: cover;
}

.sky-video video {
  opacity: 0.001; /* not 0, see .video-page video */
  transition: opacity 900ms var(--ease);
}

.sky-video video.playing {
  opacity: 1;
}

/* iOS Low Power Mode: hide the start-playback overlay; the poster stays. */
.sky-video video::-webkit-media-controls,
.sky-video video::-webkit-media-controls-start-playback-button {
  display: none !important;
  -webkit-appearance: none;
}

:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

.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.5rem;
  left: 0.5rem;
  z-index: calc(var(--z-chrome) + 1);
  margin: 0; /* not the link hover box's negative margin */
  padding: 0.5rem 0.75rem;
  background: var(--paper);
  color: var(--ink);
  transform: translateY(-200%);
}

.skip-link:focus {
  transform: none;
}
