/* roberthaverly: the site's stylesheet. Tokens first (from the Figma file
   aWpJIrYC0ljjWPTGkBD4cg), then each component and page, carried over from
   the Astro site's components. The font is declared by theme.json. */

/* Tokens from the Figma file "Untitled" (aWpJIrYC0ljjWPTGkBD4cg): color
   variables, space/* variables, and the Portfolio/* text styles. */
:root {
  color-scheme: light dark;

  --color-background: #ffffff;
  --color-text: #131313;
  --color-muted: #666666;
  --color-rule: #e0e0e0;

  --space-4: 4px;
  --space-8: 8px;
  --space-16: 16px;
  --space-24: 24px;
  --space-40: 40px;
  --space-48: 48px;
  --space-64: 64px;
  --space-80: 80px;

  --font-sans: 'Inter Variable', Inter, system-ui, -apple-system, sans-serif;

  /* Portfolio/Display */
  --display-size: 40px;
  --display-leading: 48px;
  --display-tracking: -1.6px;
  /* Portfolio/Title */
  --title-size: 24px;
  --title-leading: 32px;
  --title-tracking: -0.6px;
  /* Portfolio/Body */
  --body-size: 16px;
  --body-leading: 26px;
  --body-tracking: -0.2px;
  /* Portfolio/Link: body size and tracking, tighter leading, medium weight. */
  --link-size: var(--body-size);
  --link-leading: 24px;
  --link-weight: 500;
  --link-tracking: var(--body-tracking);
  /* Portfolio/Caption */
  --caption-size: 13px;
  --caption-leading: 20px;
  --caption-tracking: 0;
  --weight-regular: 400;

  /* Hairlines: rules between rows, the edge of a control. */
  --hairline: 1px;
  --rule: var(--hairline) solid var(--color-rule);

  /* Moodboard board: wider than the text column, laid out as a loose scatter.
     Tiles vary between roughly a third and the full column width, so the board
     reads as a scatter rather than a grid, with generous air between them.
     Below 900px there is no room to scatter: even, narrow columns. */
  --board-width: 1400px;
  --board-column: 400px;
  --board-column-narrow: 150px;
  --board-gap: 72px;
  --board-rhythm: 112px;
  --tile-hover-scale: 1.12;
  /* On an even board (the Playground) the pointed-at tile grows further and
     the others step back to this opacity. */
  --tile-hover-scale-even: 1.35;
  --tile-dim-opacity: 0.6;

  /* Snaps: index thumbnails are cropped to one shape and take half the
     column; album details put their labels in a fixed column, narrower in
     the index rows. */
  --snap-cover-ratio: 3 / 2;
  --snap-thumb-width: 348px;
  --snap-meta-label-width: 96px;
  --radius-pill: 999px;
  --filter-pad-inline: var(--space-16);
  --filter-pad-block: var(--space-8);
  --detail-label-width: 120px;

  /* Motion. Hover is a small, user-triggered state change, so it gets a short
     ease-out rather than a spring. */
  --duration-fast: 160ms;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  /* User-triggered moves that should carry some life (the Snaps filter's
     pill): a damped spring, ratio 0.72, sampled to linear(). It overshoots
     by about 4% and settles within its duration. */
  --duration-spring: 520ms;
  --ease-spring: linear(0, 0.039, 0.133, 0.257, 0.391, 0.523, 0.642, 0.746, 0.832, 0.9, 0.951, 0.988, 1.013, 1.028, 1.036, 1.038, 1.037, 1.034, 1.029, 1.024, 1.019, 1.014, 1.01, 1.007, 1.004, 1.002, 1);
  /* Lightbox zoom on touch screens, after the Photos app. Settling after a
     gesture (bounce back, momentum into place, double-tap zoom) is a spring
     that starts at the finger's speed: response is how long it takes,
     damping how much it overshoots (1 = none). Flicks travel as far as a
     scroll view's normal deceleration would carry them. */
  --zoom-spring-response: 0.42;
  --zoom-spring-damping: 0.88;
  --zoom-deceleration: 0.998;
  --zoom-double-tap-scale: 2.5;
  --zoom-min-max-scale: 3;
  /* Closing it, as Photos does: let go of a pinch below this scale, or a
     drag from fit size that has gone this share of the screen's height or
     is moving this fast (px per ms), and the photo flies back to its tile. */
  --zoom-dismiss-scale: 0.8;
  --zoom-dismiss-distance: 0.15;
  --zoom-dismiss-velocity: 0.5;
  /* How a drag feels past an edge (Apple's rubber-band constant), and at fit
     size: the photo shrinks by this share over a screen's height of travel,
     and the background has faded out this far across it. Two taps within
     this window are a double tap. */
  --zoom-rubber: 0.55;
  --zoom-drag-shrink: 0.3;
  --zoom-fade-distance: 0.6;
  --zoom-double-tap: 300ms;
  /* Turning to the next or previous photo in the lightbox. A vertical drag
     let go past this share of the screen's height, or moving this fast (px
     per ms), turns the page: the same measures as closing. A wheel or
     trackpad turns one photo per scroll: a movement of at least this many
     px starts it, and the next waits until the wheel has rested this long. */
  --page-distance: var(--zoom-dismiss-distance);
  --page-flick-velocity: var(--zoom-dismiss-velocity);
  --page-wheel-threshold: 12;
  --page-wheel-rest: 150ms;

  /* How long a control keeps its confirmation (the email's Copy reading
     "Copied") before it goes back. */
  --duration-confirm: 2000ms;

  /* The home page's section links, pointed at with a mouse: a small fan of
     the section's newest things appears by the pointer and trails it on a
     spring (response in seconds, damping 1 = no overshoot), offset from the
     pointer so it never sits under it. The cards are --peek-size square and
     fan out by --peek-tilt and --peek-spread each; the other rows step back
     to --peek-dim-opacity. */
  --peek-size: 128px;
  --peek-radius: var(--space-8);
  --peek-tilt: 7deg;
  --peek-spread: 56px;
  --peek-offset: 28px;
  --peek-hidden-scale: 0.8;
  --peek-spring-response: 0.32;
  --peek-spring-damping: 0.78;
  --peek-dim-opacity: 0.35;

  /* Showing and hiding the lightbox controls on a tap: system motion. */
  --duration-controls: 200ms;

  /* Rows leaving or reflowing when the filter changes: system motion, so an
     ease rather than a spring. */
  --duration-reflow: 360ms;

  /* Layout: 720px column, 360px sides at the 1440 frame; 24px sides on mobile. */
  --content-width: 720px;
  /* Safe areas: the parts of an iPhone screen behind the Dynamic Island, the
     rounded corners and the home indicator. They are only non-zero because
     the viewport is set to viewport-fit=cover (Base.astro), which lets the
     page, and the lightbox in particular, reach the screen's edges. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);

  --page-inset: max(var(--space-24), var(--safe-left), var(--safe-right));
  --page-top: var(--space-80);
  --page-bottom: var(--space-64);
  --section-gap: var(--space-80);
  --header-gap: var(--space-24);
  --year-width: 68px;
  --tap-target: 44px;

  /* Floating controls over images (the lightbox's hint) and the pill-shaped
     radio options: a translucent fill that frosts whatever passes behind it;
     a press shrinks the option slightly. Disabled options and the secondary
     count beside a label sit back at these opacities. */
  --control-size: var(--tap-target);
  --control-fill: color-mix(in oklab, var(--color-text) 10%, transparent);
  --control-blur: 20px;
  --control-press-scale: 0.96;
  --control-disabled-opacity: 0.4;
  --control-secondary-opacity: 0.7;
  --icon-size: 16px;
  --icon-stroke: 1.5px;
  /* A streaming service's mark beside or in place of its name (playlists). */
  --service-icon-size: 20px;

  /* Liquid glass, for the floating scroll controls (navigation layer only).
     Less blur than frosted glass and a strong saturation lift, so colour
     reads through it; a faint tint of the page; a specular rim on the top
     edge and a softer one below; a soft shadow to lift it off the page; a
     specular sheen across the rim (--glass-sheen, masked to the edge).
     Chromium also bends what is behind it (--glass-refraction, px). */
  --glass-tint: color-mix(in oklab, var(--color-background) 42%, transparent);
  --glass-blur: 10px;
  --glass-saturate: 1.9;
  --glass-rim: rgb(255 255 255 / 0.55);
  --glass-rim-soft: rgb(255 255 255 / 0.18);
  --glass-edge: rgb(0 0 0 / 0.08);
  --glass-sheen: linear-gradient(135deg, var(--glass-rim), transparent 35%, transparent 65%, var(--glass-rim-soft));
  --glass-shadow: 0 10px 30px rgb(0 0 0 / 0.14), 0 2px 6px rgb(0 0 0 / 0.08);
  --glass-refraction: 22;
  /* Pressing a glass control swells it and gathers light under the finger
     (.press-glow); hovering with a mouse shows the glow at part strength.
     A disabled button in the glass sits back. */
  --glass-press-scale: 1.06;
  --glass-press-glow: rgb(255 255 255 / 0.35);
  --glass-press-glow-spread: 70%;
  --glass-hover-glow-opacity: 0.5;
  --glass-disabled-opacity: 0.3;
  --glass-gap: var(--space-4);
  --glass-offset: var(--space-16);
  /* Appearing and leaving as the page scrolls: system motion, eased. Hidden,
     the glass is small, faded and blurred, so it takes shape as it appears. */
  --duration-glass: 320ms;
  --glass-hidden-scale: 0.9;
  --glass-hidden-blur: 6px;
  /* The scroll dock shows on pages at least this many screens tall, once
     scrolled this far down one. */
  --dock-long-page: 2.5;
  --dock-show-after: 0.6;
  --focus-ring: 2px solid var(--color-text);
  --focus-offset: 2px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --glass-rim: rgb(255 255 255 / 0.32);
    --glass-rim-soft: rgb(255 255 255 / 0.08);
    --glass-edge: rgb(255 255 255 / 0.1);
    --glass-shadow: 0 10px 30px rgb(0 0 0 / 0.45), 0 2px 6px rgb(0 0 0 / 0.3);
    --glass-press-glow: rgb(255 255 255 / 0.18);
    --color-background: #0e0e0e;
    --color-text: #f0f0f0;
    --color-muted: #a3a3a3;
    --color-rule: #383838;
  }
}

/* Home / Mobile frame: 390 wide, 24px sides, 40px top, 64px section gap,
   Display mobile 32/40 with -1px tracking. */
@media (max-width: 767px) {
  :root {
    --display-size: 32px;
    --display-leading: 40px;
    --display-tracking: -1px;
    --page-top: var(--space-40);
    --section-gap: var(--space-64);
    --header-gap: var(--space-8);
  }
}

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

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background-color: var(--color-background);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--body-size);
  line-height: var(--body-leading);
  letter-spacing: var(--body-tracking);
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
}

p,
h1,
h2,
h3,
ul,
ol {
  margin: 0;
}

ul,
ol {
  padding: 0;
  list-style: none;
}

a {
  color: inherit;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

/* Every focusable element gets the same ring. Scoped to interactive elements so
   the lightbox dialog, which takes focus when it opens, is not outlined. */
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* Text styles */
.display {
  font-size: var(--display-size);
  line-height: var(--display-leading);
  letter-spacing: var(--display-tracking);
  font-weight: var(--weight-regular);
}

.title {
  font-size: var(--title-size);
  line-height: var(--title-leading);
  letter-spacing: var(--title-tracking);
  font-weight: var(--weight-regular);
}

.body {
  font-size: var(--body-size);
  line-height: var(--body-leading);
  letter-spacing: var(--body-tracking);
}

.link {
  font-size: var(--link-size);
  line-height: var(--link-leading);
  letter-spacing: var(--link-tracking);
  font-weight: var(--link-weight);
}

/* The ways to get in touch, in a line in every footer: the email address on
   the left, the links elsewhere on the right. Without room for both, the
   links wrap to a row of their own. */
.contact-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-24);
}

.contact-elsewhere {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-24);
}

.caption {
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  letter-spacing: var(--caption-tracking);
  font-weight: var(--weight-regular);
}

.muted {
  color: var(--color-muted);
}

/* Page shell */
.page {
  display: flex;
  flex-direction: column;
  gap: var(--section-gap);
  width: min(var(--content-width), 100% - 2 * var(--page-inset));
  margin-inline: auto;
  padding-block: max(var(--page-top), calc(var(--safe-top) + var(--space-16)))
    calc(var(--page-bottom) + var(--safe-bottom));
}

/* Sub-pages (work, playground, moodboard) run a tighter rhythm than the home page. */
.page.sub {
  --section-gap: var(--space-64);
}

.section {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
}

/* Liquid glass: the material for floating controls on the navigation layer
   (the scroll dock, the lightbox's close button). A light blur with a
   saturation lift and a faint tint, rims top and bottom, a hairline edge,
   a soft shadow, and a specular highlight round the rim (::before). The
   element using it sets its own shape and position. */
.glass {
  position: relative;
  background: var(--glass-tint);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  box-shadow:
    inset 0 var(--hairline) 0 var(--glass-rim),
    inset 0 calc(-1 * var(--hairline)) 0 var(--glass-rim-soft),
    inset 0 0 0 var(--hairline) var(--glass-edge),
    var(--glass-shadow);
}

.glass::before {
  content: '';
  position: absolute;
  inset: 0;
  padding: var(--hairline);
  border-radius: inherit;
  background: var(--glass-sheen);
  mask:
    linear-gradient(#000 0 0) content-box exclude,
    linear-gradient(#000 0 0);
  pointer-events: none;
}

/* The press glow on a glass button: light gathered under the finger
   (--px/--py, set by the button's script; the centre otherwise). The button
   positions itself and clips to its shape. */
.press-glow::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(circle at var(--px, 50%) var(--py, 50%), var(--glass-press-glow), transparent var(--glass-press-glow-spread));
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out);
  pointer-events: none;
}

.press-glow:active:not(:disabled)::after {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .press-glow::after {
    transition: none;
  }
}

/* Anything the lightbox opens. */
[data-zoom] {
  display: block;
  cursor: zoom-in;
}

.rule {
  height: var(--hairline);
  background-color: var(--color-rule);
}

@media (max-width: 767px) {
  .page.sub {
    --section-gap: var(--space-48);
  }

  /* Desktop-only line breaks inside a paragraph. */
  .br-desktop {
    display: none;
  }
}


/* ---- WordPress shell ------------------------------------------------- */
/* The block templates render into .wp-site-blocks: that is the page shell
   (.page in the Astro site), one flex column with the section gap. The
   template parts and the post content are wrappers the templates need but
   the layout does not, so their children take part in that column. */
.wp-site-blocks {
  display: flex;
  flex-direction: column;
  gap: var(--section-gap);
  width: min(var(--content-width), 100% - 2 * var(--page-inset));
  margin-inline: auto;
  padding-block: max(var(--page-top), calc(var(--safe-top) + var(--space-16)))
    calc(var(--page-bottom) + var(--safe-bottom));
}

body.sub .wp-site-blocks {
  --section-gap: var(--space-64);
}

.wp-block-template-part,
.entry-content {
  display: contents;
}

/* Blocks the content may carry, in the site's own styles. */
.entry-content > .wp-block-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
}

/* ---- components/SiteHeader --------------------------------------- */
.site-header {
  display: flex;
  gap: var(--header-gap);
  align-items: flex-start;
}
/* The name takes what the links leave, so the links stay on one line. */
.name {
  flex: 1 1 auto;
  min-width: 0;
}
.side {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-24);
}
@media (max-width: 767px) {
  .site-header {
    flex-direction: column;
  }
  .name,
  .side {
    flex: none;
  }
  /* Four links do not fit across a phone: two by two. */
  .side {
    display: grid;
    grid-template-columns: repeat(2, max-content);
    column-gap: var(--space-24);
    row-gap: var(--space-8);
    justify-content: flex-start;
  }
}


/* ---- components/ProjectRow --------------------------------------- */
.row-item {
  border-bottom: var(--rule);
}
.row {
  display: flex;
  gap: var(--space-24);
  align-items: flex-start;
  padding-block: var(--space-16);
  min-height: var(--tap-target);
}
.row:hover {
  text-decoration: none;
}
.row:hover .row-title {
  text-decoration: underline;
}
.row .info {
  display: flex;
  flex: 1 0 0;
  min-width: 0;
  flex-direction: column;
  gap: var(--space-4);
}
.year {
  flex: none;
  width: var(--year-width);
  text-align: right;
}


/* ---- components/CoverRow ----------------------------------------- */
.cover-row {
  position: relative;
  display: flex;
  gap: var(--space-24);
  align-items: flex-start;
  padding-block: var(--space-24);
}

/* The title's link covers the row, and carries the focus ring there. */
.cover-row .row-title::after {
  content: '';
  position: absolute;
  inset: 0;
}

.cover-row .row-title:focus-visible {
  outline: none;
}

.cover-row .row-title:focus-visible::after {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.cover-row:hover .row-title {
  text-decoration: underline;
}

/* The row's own links sit above the title's reach. */
.actions {
  position: relative;
  z-index: 1;
  align-self: flex-start;
}

.cover-row:has(.actions:hover) .row-title {
  text-decoration: none;
}

/* The cover is the page's own element, so outside this component's scope. */
.cover-row > img {
  display: block;
  flex: none;
  width: var(--snap-thumb-width);
  height: auto;
  aspect-ratio: var(--snap-cover-ratio);
  object-fit: cover;
}

.cover-row.square > img {
  aspect-ratio: 1;
}

.cover-row .info {
  display: flex;
  flex: 1 0 0;
  min-width: 0;
  flex-direction: column;
  gap: var(--space-8);
}

.cover-row .meta {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding-top: var(--space-8);
}

.cover-row .meta-row {
  display: flex;
  gap: var(--space-16);
}

.cover-row .meta-row dt {
  flex: none;
  width: var(--snap-meta-label-width);
}

.cover-row .meta-row dd {
  flex: 1 0 0;
  min-width: 0;
  margin: 0;
}

/* No room for two columns on a phone: the cover goes full width above. */
@media (max-width: 767px) {
  .cover-row {
    flex-direction: column;
    gap: var(--space-16);
  }

  .cover-row > img {
    width: 100%;
  }
}


/* ---- components/Email -------------------------------------------- */
.email {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-4) var(--space-8);
}

/* A text control, like the links around it: underlined when pointed at. */
.copy {
  padding: 0;
  border: 0;
  background: none;
  font-family: inherit;
  cursor: pointer;
}

.copy:hover {
  text-decoration: underline;
}


/* ---- components/ServiceLinks ------------------------------------- */
.services {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  margin: 0;
  padding: 0;
  list-style: none;
}

.services.labels {
  gap: var(--space-8) var(--space-24);
}

.service {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
}

.service img,
.service svg {
  flex: none;
  width: var(--service-icon-size);
  height: var(--service-icon-size);
  object-fit: contain;
  fill: currentColor;
}

/* A mark on its own: a full-size tap target around a small mark, pulled
   left so the first mark lines up with the text above it. */
.services:not(.labels) {
  margin-left: calc((var(--service-icon-size) - var(--tap-target)) / 2);
}

.services:not(.labels) .service {
  justify-content: center;
  min-width: var(--tap-target);
  height: var(--tap-target);
}


/* ---- components/Gallery ------------------------------------------ */
.gallery {
  columns: 2;
  column-gap: var(--space-24);
}
.gallery li {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  break-inside: avoid;
  margin-bottom: var(--space-24);
  column-span: all;
}
.gallery li.portrait {
  column-span: none;
}
.gallery img {
  display: block;
  width: 100%;
  height: auto;
}


/* ---- components/Board -------------------------------------------- */
/* The page's copy stays in the 720px column; only the board breaks out
   wider. */
.board-wrap {
  --board-w: min(var(--board-width), calc(100vw - 2 * var(--page-inset)));
  width: var(--board-w);
  margin-inline: calc((100% - var(--board-w)) / 2);
}

/* Without the script: CSS columns, by column width. */
.board {
  columns: var(--board-column);
  column-gap: var(--board-gap);
}

/* An even board: no scatter (its tiles carry no place in the cycle), and
   on wide screens the spacing of a grid rather than a loose spread. */
@media (min-width: 900px) {
  .board.even {
    --board-gap: var(--space-24);
    --board-rhythm: var(--space-40);
  }
}

/* A formatting root, so the inner box's margins count in the tile's
   measured height. */
.tile {
  position: relative;
  display: flow-root;
  break-inside: avoid;
}

/* With it: each tile placed by the script, one column wide. */
.board.is-placed {
  position: relative;
  columns: auto;
}

.board.is-placed > .tile {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--lane-w);
}

/* The inner box carries the scatter: its width, indent and spacing are
   shares of the column. */
.tile-inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  margin-bottom: var(--board-rhythm);
}

.board a {
  position: relative;
  transition: transform var(--duration-fast) var(--ease-out);
}

.board img {
  display: block;
  width: 100%;
  height: auto;
}

/* The clip lies over its own poster and fades in once it starts playing, so
   a video that never loads simply stays a still. */
.board video {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.board .is-playing video {
  opacity: 1;
}

/* Hovering lifts a tile above its neighbours as it grows. */
.tile:hover,
.tile:focus-within {
  z-index: 2;
}

.tile:hover a,
.tile:focus-within a {
  transform: scale(var(--tile-hover-scale));
}

/* An even board singles the tile out more: it grows further and the rest
   fade back a little. Pointer only, so a tap on a phone does not leave
   the board dimmed; keyboard focus does the same. */
.board.even .tile {
  transition: opacity var(--duration-fast) var(--ease-out);
}

.board.even .tile:focus-within a {
  transform: scale(var(--tile-hover-scale-even));
}

.board.even:has(.tile:focus-within) .tile:not(:focus-within) {
  opacity: var(--tile-dim-opacity);
}

/* In an outer column it grows from the page's edge inward, so it is not
   pushed off the screen. */
.board.even .tile[data-edge='start'] a {
  transform-origin: left center;
}

.board.even .tile[data-edge='end'] a {
  transform-origin: right center;
}

@media (hover: hover) {
  .board.even .tile:hover a {
    transform: scale(var(--tile-hover-scale-even));
  }

  .board.even:has(.tile:hover) .tile:not(:hover) {
    opacity: var(--tile-dim-opacity);
  }
}

@media (prefers-reduced-motion: reduce) {
  .board a,
  .board.even .tile {
    transition: none;
  }
}

/* Narrow screens have no room to scatter: even columns, tighter rhythm. */
@media (max-width: 899px) {
  .board {
    --board-column: var(--board-column-narrow);
    --board-gap: var(--space-16);
    --board-rhythm: var(--space-24);
  }
}

/* A nine-tile cycle of widths and offsets, wide screens only, by a tile's
   place in the order. Tiles land at different sizes and indents in each
   column, and each carries its own top padding, so the columns begin at
   different heights. Padding rather than margin, which browsers may
   truncate at a column break in the CSS-columns fallback. */
@media (min-width: 900px) {
  .tile[data-scatter='1'] .tile-inner {
    width: 100%;
    margin-bottom: var(--board-rhythm);
  }
  .tile[data-scatter='2'] .tile-inner {
    width: 42%;
    margin-left: 52%;
    padding-top: calc(var(--board-rhythm) * 0.9);
    margin-bottom: calc(var(--board-rhythm) * 1.4);
  }
  .tile[data-scatter='3'] .tile-inner {
    width: 78%;
    margin-left: 4%;
    padding-top: calc(var(--board-rhythm) * 0.15);
    margin-bottom: calc(var(--board-rhythm) * 0.8);
  }
  .tile[data-scatter='4'] .tile-inner {
    width: 54%;
    margin-left: 22%;
    padding-top: calc(var(--board-rhythm) * 0.55);
    margin-bottom: calc(var(--board-rhythm) * 1.3);
  }
  .tile[data-scatter='5'] .tile-inner {
    width: 92%;
    margin-left: 8%;
    margin-bottom: calc(var(--board-rhythm) * 0.9);
  }
  .tile[data-scatter='6'] .tile-inner {
    width: 36%;
    padding-top: calc(var(--board-rhythm) * 1.1);
    margin-bottom: calc(var(--board-rhythm) * 1.5);
  }
  .tile[data-scatter='7'] .tile-inner {
    width: 68%;
    margin-left: 30%;
    padding-top: calc(var(--board-rhythm) * 0.3);
    margin-bottom: var(--board-rhythm);
  }
  .tile[data-scatter='8'] .tile-inner {
    width: 48%;
    margin-left: 6%;
    padding-top: calc(var(--board-rhythm) * 0.8);
    margin-bottom: calc(var(--board-rhythm) * 1.25);
  }
  .tile[data-scatter='9'] .tile-inner {
    width: 84%;
    margin-left: 14%;
    padding-top: calc(var(--board-rhythm) * 0.1);
    margin-bottom: calc(var(--board-rhythm) * 0.95);
  }
}


/* ---- components/Lightbox ----------------------------------------- */
.lightbox {
  /* Room above the image for the controls' row, and below it for the
     home indicator. */
  /* The close button is glass, the height of the scroll dock. */
  --close-size: calc(var(--control-size) + var(--glass-gap) * 2);
  --stage-top: calc(var(--safe-top) + var(--space-16) * 2 + var(--close-size));
  --stage-bottom: calc(var(--safe-bottom) + var(--space-24));
  inset: 0;
  width: 100vw;
  height: 100dvh;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--color-text);
}

/* The viewer's background is its own layer, so a dismissing gesture can
   fade it and let the page show through (--scrim, set by the script). */
.lightbox::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  background: var(--color-background);
  opacity: var(--scrim, 1);
}
/* The dialog holds focus while open so the close button does not show a
   ring on open; a ring around the whole screen says nothing, so it has
   none. Tabbing reaches the close button, which has the site's ring. */
.lightbox:focus {
  outline: none;
}

.lightbox::backdrop {
  background: transparent;
}

/* The page beneath holds still while the viewer is open: a wheel or the
   keyboard would otherwise scroll it through the dialog, and it would be
   somewhere else on close. The scroll offset is kept, so a dismissed photo
   still finds its tile. */
html:has(.lightbox[open]) {
  overflow: hidden;
}
/* The controls' row: below the top safe area, inside the side ones. It
   lets clicks through between the controls, to the stage. */
.bar {
  position: fixed;
  top: calc(var(--safe-top) + var(--space-16));
  left: max(var(--space-16), var(--safe-left));
  right: max(var(--space-16), var(--safe-right));
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-16);
  pointer-events: none;
  transition: opacity var(--duration-controls) var(--ease-out);
}

.hint,
.close {
  pointer-events: auto;
}

.hint {
  background: var(--control-fill);
  backdrop-filter: blur(var(--control-blur));
  -webkit-backdrop-filter: blur(var(--control-blur));
  margin: 0;
  padding: var(--space-4) var(--space-16);
  border-radius: var(--radius-pill);
  cursor: default;
}

/* The same liquid glass as the scroll dock (.glass), and the same press:
   it swells on a spring and gathers light under the finger. */
.close {
  display: grid;
  place-items: center;
  flex: none;
  width: var(--close-size);
  height: var(--close-size);
  margin-left: auto;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  color: inherit;
  cursor: pointer;
  transition: transform var(--duration-spring) var(--ease-spring);
}

.close svg {
  width: var(--icon-size);
  height: var(--icon-size);
  fill: none;
  stroke: currentColor;
  stroke-width: var(--icon-stroke);
  stroke-linecap: round;
}

@media (hover: hover) {
  .close:hover::after {
    opacity: var(--glass-hover-glow-opacity);
  }
}

.close:active {
  transform: scale(var(--glass-press-scale));
}

/* The photo's date (feed photos), small and centred under it. The photo
   leaves room for it at fit size; zoomed in, the photo covers that space,
   so the date steps aside. */
.lightbox.has-date {
  --stage-bottom: calc(var(--safe-bottom) + var(--space-16) * 2 + var(--caption-leading));
}

.date {
  position: fixed;
  left: 50%;
  bottom: calc(var(--safe-bottom) + var(--space-16));
  margin: 0;
  translate: -50% 0;
  white-space: nowrap;
  pointer-events: none;
  transition: opacity var(--duration-controls) var(--ease-out);
}

.lightbox:not(.has-date) .date,
.lightbox.is-video .date {
  display: none;
}

.lightbox.is-chrome-hidden .date,
.lightbox.is-dragging .date,
.lightbox.is-paging .date,
.lightbox.is-zoomed .date {
  opacity: 0;
}

/* The stage fits the media between the controls and the bottom edge. The
   photo and the one arriving on a turn share its one cell. */
.stage {
  width: 100%;
  height: 100%;
  overflow: hidden;
  display: grid;
  place-items: center;
  padding: var(--stage-top) max(var(--space-24), var(--safe-right)) var(--stage-bottom)
    max(var(--space-24), var(--safe-left));
}

.stage img,
.stage video {
  display: block;
  grid-area: 1 / 1;
  max-width: 100%;
  max-height: calc(100dvh - var(--stage-top) - var(--stage-bottom));
  width: auto;
  height: auto;
  cursor: zoom-in;
}

/* The photo arriving on a turn: until it has loaded, its tile's copy
   stands in for it (set by the script). */
.stage [data-neighbour] {
  background-size: 100% 100%;
  pointer-events: none;
}

.stage img[hidden] {
  display: none;
}

.stage video {
  cursor: default;
}

/* Only one of the two is ever in play. */
.lightbox:not(.is-video) [data-player],
.lightbox.is-video [data-image],
.lightbox.is-video .hint {
  display: none;
}
.lightbox.is-full .stage {
  display: block;
  overflow: auto;
}

.lightbox.is-chrome-hidden .bar,
.lightbox.is-dragging .bar {
  opacity: 0;
}

/* Closing with no tile to return to: the photo fades as it goes. */
.lightbox.is-fading [data-image] {
  opacity: 0;
  transition: opacity var(--duration-glass) var(--ease-out);
}

.lightbox.is-chrome-hidden .bar > * {
  pointer-events: none;
}

/* Touch: the script places and transforms the image itself, and the stage
   takes every gesture so the browser does not zoom the page instead. */
.lightbox.is-touch:not(.is-video) .stage {
  position: relative;
  display: block;
  overflow: hidden;
  touch-action: none;
}

.lightbox.is-touch:not(.is-video) .stage img {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--fit-w);
  height: var(--fit-h);
  max-width: none;
  max-height: none;
  transform-origin: 0 0;
  cursor: default;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.lightbox.is-full .stage img {
  max-width: none;
  max-height: none;
  margin: 0 auto;
  cursor: zoom-out;
}

/* Reduced motion: every transition is instant and the press does not
   swell. */
@media (prefers-reduced-motion: reduce) {
  .close,
  .date,
  .bar {
    transition: none;
  }

  .close:active {
    transform: none;
  }
}


/* ---- components/ScrollDock --------------------------------------- */
.defs {
  position: absolute;
  width: 0;
  height: 0;
}

.dock {
  position: fixed;
  left: 50%;
  bottom: calc(var(--safe-bottom) + var(--glass-offset));
  z-index: 10;
  display: flex;
  gap: var(--glass-gap);
  padding: var(--glass-gap);
  border-radius: var(--radius-pill);
  color: var(--color-text);
  translate: -50% 0;
  /* Hidden: small, faded and blurred, so it takes shape as it appears. */
  opacity: 0;
  scale: var(--glass-hidden-scale);
  filter: blur(var(--glass-hidden-blur));
  transition:
    opacity var(--duration-glass) var(--ease-out),
    scale var(--duration-glass) var(--ease-out),
    filter var(--duration-glass) var(--ease-out),
    transform var(--duration-spring) var(--ease-spring);
}

.dock.is-shown {
  opacity: 1;
  scale: 1;
  filter: none;
}

/* Chromium bends the backdrop before blurring it. */
.dock.is-refracting {
  backdrop-filter: url(#dock-refract) blur(var(--glass-blur)) saturate(var(--glass-saturate));
}

/* Pressing swells the glass on a spring, as a touch does on iOS. */
.dock:has(.dock-button:active:not(:disabled)) {
  transform: scale(var(--glass-press-scale));
}

.dock-button {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--control-size);
  height: var(--control-size);
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: inherit;
  cursor: pointer;
  overflow: hidden;
  transition:
    background-color var(--duration-fast) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out);
}

.dock-button svg {
  width: var(--icon-size);
  height: var(--icon-size);
  fill: none;
  stroke: currentColor;
  stroke-width: var(--icon-stroke);
  stroke-linecap: round;
  stroke-linejoin: round;
}

@media (hover: hover) {
  .dock-button:hover:not(:disabled) {
    background: var(--control-fill);
  }
}

.dock-button:disabled {
  opacity: var(--glass-disabled-opacity);
  cursor: default;
}

@media (prefers-reduced-motion: reduce) {
  .dock,
  .dock-button {
    transition: none;
  }

  .dock:has(.dock-button:active:not(:disabled)) {
    transform: none;
  }
}


/* ---- components/Segmented ---------------------------------------- */
.segmented {
  position: relative;
  display: inline-flex;
  align-self: flex-start;
  max-width: 100%;
  margin: 0;
  padding: var(--space-4);
  border: var(--rule);
  border-radius: var(--radius-pill);
}

.segmented:not(.is-ready) {
  visibility: hidden;
}

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

.option {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  min-height: var(--tap-target);
  padding: var(--filter-pad-block) var(--filter-pad-inline);
  border-radius: var(--radius-pill);
  color: var(--color-muted);
  font-size: var(--link-size);
  line-height: var(--link-leading);
  letter-spacing: var(--link-tracking);
  font-weight: var(--link-weight);
  white-space: nowrap;
  cursor: pointer;
  transition:
    color var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

/* The radio covers its option: native keyboard and screen reader behaviour,
   drawn by the option around it. */
.option input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: inherit;
}

.option:hover {
  color: var(--color-text);
}

.option:active {
  transform: scale(var(--control-press-scale));
}

.option:has(input:focus-visible) {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.option:has(input:disabled) {
  opacity: var(--control-disabled-opacity);
  cursor: default;
  transform: none;
}

/* One pill for the whole track, drawn under the options (they follow it
   in the DOM). */
.pill {
  position: absolute;
  top: var(--space-4);
  bottom: var(--space-4);
  left: 0;
  width: var(--pill-w, 0);
  translate: var(--pill-x, 0) 0;
  border-radius: var(--radius-pill);
  background: var(--color-text);
  pointer-events: none;
  transition:
    translate var(--duration-spring) var(--ease-spring),
    width var(--duration-spring) var(--ease-spring);
}

.segmented.is-instant .pill {
  transition: none;
}

.option-label,
.count {
  position: relative;
}

.count {
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  font-weight: var(--weight-regular);
  font-variant-numeric: tabular-nums;
  opacity: var(--control-secondary-opacity);
}

/* The checked option's text inverts over the pill. It eases across the
   pill's travel, so mid-slide it is a grey that reads on the pill and the
   track alike. */
.option:has(input:checked) {
  color: var(--color-background);
  transition:
    color var(--duration-spring) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .pill,
  .option,
  .option:has(input:checked) {
    transition: none;
  }
}

/* On a phone the options share the track evenly, in caption type. */
@media (max-width: 767px) {
  .segmented {
    display: flex;
    align-self: stretch;
  }

  .option {
    flex: 1 1 auto;
    justify-content: center;
    gap: var(--space-4);
    padding-inline: var(--space-8);
    font-size: var(--caption-size);
    line-height: var(--caption-leading);
    letter-spacing: var(--caption-tracking);
  }
}


/* ---- components/Prose -------------------------------------------- */
.post {
  gap: var(--space-16);
}

.post h2,
.post h3 {
  padding-top: var(--space-8);
  color: var(--color-muted);
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  letter-spacing: var(--caption-tracking);
  font-weight: var(--weight-regular);
}

.post a {
  text-decoration: underline;
}

.post strong {
  font-weight: var(--link-weight);
}

.post ul,
.post ol {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  padding-left: var(--space-24);
}

.post ul {
  list-style: disc;
}

.post ol {
  list-style: decimal;
}

.post blockquote {
  margin: 0;
  padding-left: var(--space-24);
  border-left: var(--rule);
  color: var(--color-muted);
}

.post hr {
  height: var(--hairline);
  margin: 0;
  border: 0;
  background-color: var(--color-rule);
}

.post img,
.post iframe {
  display: block;
  width: 100%;
  border: 0;
}

.post img {
  height: auto;
}


/* ---- layouts/Base ------------------------------------------------ */
/* Measures the top safe area for the script above. */
.status-probe {
  position: fixed;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  padding-top: var(--safe-top);
  visibility: hidden;
  pointer-events: none;
}


/* ---- pages/index ------------------------------------------------- */
/* The sections: the name, and a line about it under that. The whole row
   is the link. */
.jump {
  display: flex;
  flex-direction: column;
  /* The rules between rows, off for now:
  border-top: var(--rule); */
}

.jump-row {
  display: flex;
  gap: var(--space-24);
  align-items: center;
  padding-block: var(--space-24);
  /* border-bottom: var(--rule); */
}

.jump-info {
  display: flex;
  flex: 1 0 0;
  min-width: 0;
  flex-direction: column;
  gap: var(--space-4);
}

/* Pointing at a row: no underline. An arrow eases in beside the name, the
   other rows step back, and a fan of the section's newest things trails
   the pointer (the script below moves it). */
.jump-row:hover {
  text-decoration: none;
}

.arrow {
  display: inline-block;
  margin-left: var(--space-8);
  opacity: 0;
  translate: calc(-1 * var(--space-8)) 0;
  transition:
    opacity var(--duration-fast) var(--ease-out),
    translate var(--duration-spring) var(--ease-spring);
}

.jump-row:hover .arrow,
.jump-row:focus-visible .arrow {
  opacity: 1;
  translate: 0 0;
}

.jump-row {
  transition: opacity var(--duration-fast) var(--ease-out);
}

/* The fan: fixed to the screen and placed by the script at the pointer.
   Hidden, its cards sit stacked and small; shown, they spread and tilt on
   the site's spring. A phone has no pointer to follow, so it never shows. */
.peek {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 5;
  width: var(--peek-size);
  height: var(--peek-size);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.peek img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: var(--peek-radius);
  background: var(--color-rule);
  box-shadow: var(--glass-shadow);
  scale: var(--peek-hidden-scale);
  transition:
    translate var(--duration-spring) var(--ease-spring),
    rotate var(--duration-spring) var(--ease-spring),
    scale var(--duration-spring) var(--ease-spring);
}

@media (hover: hover) and (pointer: fine) {
  .jump:has(.jump-row:hover) .jump-row:not(:hover) {
    opacity: var(--peek-dim-opacity);
  }

  .peek.is-shown {
    opacity: 1;
  }

  .peek.is-shown img {
    scale: 1;
  }

  /* First on top, the rest fanned out to the right behind it. */
  .peek img:nth-child(1) {
    z-index: 3;
  }

  .peek img:nth-child(2) {
    z-index: 2;
  }

  .peek.is-shown img:nth-child(1) {
    rotate: calc(-1 * var(--peek-tilt));
  }

  .peek.is-shown img:nth-child(2) {
    translate: var(--peek-spread) 0;
  }

  .peek.is-shown img:nth-child(3) {
    translate: calc(var(--peek-spread) * 2) 0;
    rotate: var(--peek-tilt);
  }
}

@media (prefers-reduced-motion: reduce) {
  .arrow,
  .jump-row,
  .peek,
  .peek img {
    transition: none;
  }
}

/* The pictures, off for now with their markup:
.thumbs {
  display: flex;
  flex: none;
  gap: var(--space-4);
}

.thumbs img {
  display: block;
  width: var(--space-80);
  height: var(--space-80);
  object-fit: cover;
  background: var(--color-rule);
}

On a phone the pictures go under the name, sharing the row's width.
@media (max-width: 767px) {
  .jump-row {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-16);
  }

  .thumbs img {
    flex: 1 0 0;
    min-width: 0;
    width: auto;
    height: auto;
    aspect-ratio: 1;
  }
} */


/* ---- pages/work/index -------------------------------------------- */
.index {
  display: flex;
  flex-direction: column;
}
.indie {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
}
.indie li {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
}
.indie li.sub {
  padding-left: var(--space-24);
  border-left: var(--rule);
}
.tag {
  margin-left: var(--space-8);
}


/* ---- pages/work/[slug] ------------------------------------------- */
.prose {
  gap: var(--space-16);
}


/* ---- pages/moodboard --------------------------------------------- */
/* Random: a large glass pill at the bottom centre, on the navigation layer
   with the scroll dock. It rests where the dock does and steps up out of
   the dock's way when that appears; system motion, so it eases. Pressing
   swells it on a spring, like the dock. */
.random {
  --dock-height: calc(var(--control-size) + var(--glass-gap) * 2);
  position: fixed;
  left: 50%;
  bottom: calc(var(--safe-bottom) + var(--glass-offset));
  z-index: 10;
  height: calc(var(--space-64) * 2);
  padding: 0 var(--space-80);
  border: 0;
  border-radius: var(--radius-pill);
  color: var(--color-text);
  font-family: inherit;
  cursor: pointer;
  translate: -50% 0;
  transition:
    translate var(--duration-glass) var(--ease-out),
    transform var(--duration-spring) var(--ease-spring);
}

body:has(.dock.is-shown) .random {
  translate: -50% calc(-1 * (var(--dock-height) + var(--space-24)));
}

.random:active {
  transform: scale(var(--glass-press-scale));
}

/* On a phone it is half the size: the height of the old button, with the
   title's type in place of the display's. */
@media (max-width: 767px) {
  .random {
    height: var(--space-64);
    padding: 0 var(--space-40);
    font-size: var(--title-size);
    line-height: var(--title-leading);
    letter-spacing: var(--title-tracking);
  }
}

@media (hover: hover) {
  .random:hover::after {
    opacity: var(--glass-hover-glow-opacity);
  }
}

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

  .random:active {
    transform: none;
  }
}

/* The board's narrow layout starts at 900px, so this page's tighter
   rhythm does too. */
@media (max-width: 899px) {
  .sub {
    --section-gap: var(--space-48);
  }
}


/* ---- pages/snaps/index ------------------------------------------- */
.snaps {
  gap: var(--space-40);
}

.controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-16);
}

/* Albums and feed are two views of the same section; the film filter only
   applies to the albums, the order only to the feed. */
.snaps[data-view='feed'] .albums,
.snaps[data-view='feed'] .filter,
.snaps[data-view='albums'] .feed,
.snaps[data-view='albums'] .order {
  display: none;
}

.filter,
.order {
  display: contents;
}

.snaps[data-filter='digital'] .album-item:not([data-kind='digital']),
.snaps[data-filter='film'] .album-item:not([data-kind='film']),
.snaps[data-filter='expired'] .album-item:not([data-kind='expired']) {
  display: none;
}

.albums {
  display: flex;
  flex-direction: column;
  border-top: var(--rule);
}

/* One album per row (CoverRow), ruled like the home index. */
.album-item {
  border-bottom: var(--rule);
  view-transition-class: snap-row;
}

.feed {
  view-transition-name: snaps-feed;
  view-transition-class: snap-row;
}

/* On a phone the two controls stack. */
@media (max-width: 767px) {
  .controls {
    flex-direction: column;
  }
}


/* The rows' view transition is drawn at the document root, outside any
   component's scope. Reflowing rows are system motion, so they ease. The
   page itself shows live rather than crossfading from a snapshot, so the
   pill and the options keep moving underneath; the footer is named so it
   glides with the rows instead of jumping. Rows present before and after
   move as one solid snapshot; only those leaving or arriving fade. Clicks
   pass through, so the filter can be changed again mid-transition. */
::view-transition {
  pointer-events: none;
}

::view-transition-old(root) {
  animation: none;
  opacity: 0;
}

::view-transition-new(root) {
  animation: none;
}

main:has([data-snaps]) > footer {
  view-transition-name: snaps-footer;
  view-transition-class: snap-row;
}

::view-transition-group(.snap-row) {
  animation-duration: var(--duration-reflow);
  animation-timing-function: var(--ease-out);
}

::view-transition-old(.snap-row):not(:only-child) {
  animation: none;
  opacity: 0;
}

::view-transition-new(.snap-row):not(:only-child) {
  animation: none;
}

::view-transition-old(.snap-row):only-child,
::view-transition-new(.snap-row):only-child {
  animation-duration: var(--duration-reflow);
  animation-timing-function: var(--ease-out);
}


/* ---- pages/snaps/[slug] ------------------------------------------ */
.details-section {
  gap: var(--space-16);
}

/* The index's ruled rows, with the label in a fixed column like the year. */
.details {
  margin: 0;
  border-top: var(--rule);
}

.detail {
  display: flex;
  gap: var(--space-24);
  align-items: baseline;
  padding-block: var(--space-8);
  border-bottom: var(--rule);
}

.detail dt {
  flex: none;
  width: var(--detail-label-width);
}

.detail dd {
  flex: 1 0 0;
  min-width: 0;
  margin: 0;
}


/* ---- pages/playlists/index --------------------------------------- */
.playlists {
  display: flex;
  flex-direction: column;
  border-top: var(--rule);
}

.playlist-item {
  border-bottom: var(--rule);
}


/* ---- pages/playlists/[slug] -------------------------------------- */
.details-section {
  gap: var(--space-16);
}

/* The index's ruled rows, with the label in a fixed column like the year. */
.details,
.tracks {
  margin: 0;
  padding: 0;
  border-top: var(--rule);
  list-style: none;
}

.detail {
  display: flex;
  gap: var(--space-24);
  align-items: baseline;
  padding-block: var(--space-8);
  border-bottom: var(--rule);
}

.detail dt {
  flex: none;
  width: var(--detail-label-width);
}

.detail dd {
  flex: 1 0 0;
  min-width: 0;
  margin: 0;
}

/* A track is a home index row: its number where nothing was, the title
   over the artist and album, and the length in the year's column. */
.track {
  display: flex;
  gap: var(--space-16);
  align-items: baseline;
  padding-block: var(--space-8);
  border-bottom: var(--rule);
}

.number {
  flex: none;
  width: var(--space-40);
  font-variant-numeric: tabular-nums;
}

.track .info {
  display: flex;
  flex: 1 0 0;
  min-width: 0;
  flex-direction: column;
}

.length {
  flex: none;
  width: var(--year-width);
  text-align: right;
  font-variant-numeric: tabular-nums;
}


/* ---- blocks/contact-form ----------------------------------------- */
/* The form on the Contact page: each field is its label over the value
   with a rule under it, in the rhythm of the site's ruled rows; the submit
   is a filled pill, as the segmented control draws its checked option. The
   note under a field and the status line say what went wrong in words, in
   the text colour, since the palette has no colour for errors. */
/* The form and the thanks take turns; the hidden one must stay hidden under
   the flex display below. */
.contact-form .form[hidden],
.contact-form .sent[hidden] {
  display: none;
}

.contact-form .form,
.contact-form .sent {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
}

.contact-form .field {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.contact-form input,
.contact-form textarea {
  width: 100%;
  margin: 0;
  padding: var(--space-8) 0;
  border: 0;
  border-bottom: var(--rule);
  border-radius: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  transition: border-color var(--duration-fast) var(--ease-out);
}

.contact-form textarea {
  min-height: calc(var(--body-leading) * 4 + var(--space-8) * 2);
  resize: none;
  field-sizing: content;
}

.contact-form input:focus-visible,
.contact-form textarea:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-bottom-color: var(--color-text);
}

.contact-form .is-invalid input,
.contact-form .is-invalid textarea {
  border-bottom-color: var(--color-text);
}

/* A browser's autofill paints its own background over the field; the only
   way to keep the page's is to cover it with an inset shadow. */
.contact-form input:autofill {
  -webkit-text-fill-color: var(--color-text);
  box-shadow: inset 0 0 0 100vh var(--color-background);
}

.contact-form .note,
.contact-form .status {
  color: var(--color-text);
}

.contact-form .button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-target);
  padding: var(--filter-pad-block) var(--space-24);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--color-text);
  color: var(--color-background);
  font-family: inherit;
  font-size: var(--link-size);
  line-height: var(--link-leading);
  letter-spacing: var(--link-tracking);
  font-weight: var(--link-weight);
  cursor: pointer;
  transition:
    transform var(--duration-fast) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out);
}

.contact-form .button:active:not(:disabled) {
  transform: scale(var(--control-press-scale));
}

.contact-form .button:disabled {
  opacity: var(--control-disabled-opacity);
  cursor: default;
}

/* The thanks takes focus when it appears, so a reader is told; a ring
   around a paragraph says nothing. */
.contact-form .sent:focus {
  outline: none;
}

@media (prefers-reduced-motion: reduce) {
  .contact-form input,
  .contact-form textarea,
  .contact-form .button {
    transition: none;
  }

  .contact-form .button:active:not(:disabled) {
    transform: none;
  }
}
