/*-------- TOKENS --------*/
:root {
  --bg: #2C2832;
  --panel: #FFFFFF;
  /* sampled from the top edge of media/room.webp, which is flat across its full
     width — so the photograph dissolves into the page with no seam to hide.
     Re-sample it after any edit to that file: a WebP round trip moved this by
     two levels on its own, and a flat field meeting a flat field is exactly
     where two levels are visible. */
  --room-top: #E0E0E0;
  --ink: #000000;
  --drawer: 25%;
  --pad: 32px;
  --grid-gap: 10px;      /* the gutter between pictures; the one number to tune */
  --nav-top: 176px;

  /* ONE type size for the whole site. Hierarchy comes from case, not scale:
     headings are upper case, everything else sentence case. The only exception
     is incidental text — credits and copyright — which is smaller. */
  --type: 16px;
  --type-small: 12px;
  --nav-rhythm: calc(var(--type) * 2.75);

  /* projects open dark */
  /* Not quite opaque: the screen you clicked stays faintly present behind the
     writing rather than being shut off. */
  --project-bg: rgba(20, 19, 26, 0.96);
  --project-ink: #F2F2F0;
  --ease: cubic-bezier(.6,.05,.2,1);
  --speed: 620ms;
  --fade: 320ms;
}

/*-------- RESET --------*/
* { margin: 0; padding: 0; box-sizing: border-box; }

html, body { height: 100%; }

body {
  background: var(--bg);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--type);
  line-height: 1.5;
  color: var(--ink);
}

/* Headings carry the hierarchy by case and weight, never by size. */
h1, h2, h3 {
  font-size: inherit;
  font-weight: 400;
  text-transform: uppercase;
  margin: 0;
}

a { text-decoration: underline; text-underline-offset: 0.28em; }

a { color: var(--ink); }

/*-------- LAYOUT --------*/
/* The scene runs full bleed. The drawer lives off-canvas to the left and, when
   it opens, pushes the sculpture across rather than covering it. */
main {
  position: relative;
  height: 100%;
  overflow: hidden;
}

/*-------- PROJECT BACKDROP --------*/
/* Occupies exactly the drawer's slot and sits behind it, so it is hidden until
   the drawer slides away. Only present while a project is open. */
#backdrop {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--drawer);
  z-index: 2;
  /* Left, not centre. The slot is a tall narrow sliver, so `cover` throws the
     sides of a portrait photograph away — which took the tips off the crayons.
     Anchoring to one edge means what gets cropped is always the same edge, so
     a backdrop can be composed for it. */
  background: var(--room-top) left center / cover no-repeat;
  opacity: 0;
  transition: opacity var(--fade) var(--ease);
  pointer-events: none;
}

main[data-open] #backdrop { opacity: 1; }

/*-------- DRAWER --------*/
nav {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--drawer);
  background: var(--panel);
  padding: var(--pad);
  padding-top: var(--nav-top);
  z-index: 3;
  display: flex;
  flex-direction: column;
  translate: -100% 0;
  transition: translate var(--drawer-speed) var(--drawer-ease);
}

main[data-nav="true"] nav { translate: 0 0; }

#nav-list {
  list-style: none;
  line-height: 1.2;
}
#nav-list li { margin-bottom: calc(var(--nav-rhythm) - 1.2em); }
#nav-list a { text-decoration: none; }

#nav-list a { text-decoration: none; }
#nav-list a[data-active="true"] { font-weight: bold; }

/*-------- CREDITS --------*/
#credits { margin-top: auto; font-size: var(--type-small); line-height: 1.35; }
#credits ul { list-style: none; }
#copyright { margin-top: 1.6em; }
/* About is a page, not a thank-you. The gap is what says so: without it the
   link reads as the first name in the list underneath. */
#about-link { margin-bottom: 1.6em; }

/*-------- TOGGLES --------*/
/* Difference blending inverts against whatever sits behind, so these stay
   legible over the white panel and over any moving image alike. */
/* Above an open project (z 12) so the controls are never buried by content,
   below the signature (z 30). */
#toggle, #close {
  position: fixed;
  top: 16px;
  z-index: 20;
  font: inherit;
  font-size: 24px;
  line-height: 1;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: #FFF;
  mix-blend-mode: difference;
}

/* The same mark as the opener, turned a quarter circle into an X. It spins as
   the project opens, so closing reads as the same gesture reversed rather than
   as a different control. */
#close {
  top: var(--pad);
  right: var(--pad);
  width: var(--mark-size);
  height: var(--mark-size);
  display: block;
  opacity: 0;
  pointer-events: none;
  rotate: 0deg;
  transition:
    rotate var(--mark-speed) var(--mark-ease),
    opacity var(--fade) var(--ease);
}

main[data-open] #close {
  opacity: 1;
  pointer-events: auto;
  rotate: 45deg;
}

/*-------- PLUS / MINUS MARK --------*/
/* Measured off the Figma frames: 85/6 on desktop, 49/4 on mobile, inset 32. */
:root {
  --mark-size: 85px;
  --mark-weight: 6px;
  --mark-ease: cubic-bezier(.65, -0.15, .3, 1.2);
  --mark-speed: 520ms;
  --drawer-speed: 560ms;
  --drawer-ease: cubic-bezier(.5, 0, .18, 1);
}

#toggle {
  top: var(--pad);
  left: var(--pad);
  width: var(--mark-size);
  height: var(--mark-size);
  display: block;
  transition: rotate var(--mark-speed) var(--mark-ease);
}

.bar {
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: var(--mark-weight);
  margin-top: calc(var(--mark-weight) / -2);
  background: #FFF;
  transition: rotate var(--mark-speed) var(--mark-ease);
}

.bar-v { rotate: 90deg; }

/* ---- the mark, reused ----------------------------------------------------
   Every control on the site is the same two strokes at the same weight. The
   plus turns into a minus, or a quarter turn into an X. Arrows are the same
   stroke mitred into a chevron rather than a typeface's angle bracket. */
.mark {
  position: relative;
  width: var(--mark-size);
  height: var(--mark-size);
  background: none;
  border: 0;
  padding: 0;
  display: block;
  cursor: pointer;
  color: #FFF;
  mix-blend-mode: difference;
  rotate: 0deg;
  transition: rotate var(--mark-speed) var(--mark-ease);
}

.mark[data-kind="x"][data-on] { rotate: 45deg; }

.mark-chev {
  position: relative;
  width: var(--mark-size);
  height: var(--mark-size);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: #FFF;
  mix-blend-mode: difference;
}

.mark-chev::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 46%;
  height: 46%;
  border-left: var(--mark-weight) solid currentColor;
  border-top: var(--mark-weight) solid currentColor;
  translate: -50% -50%;
  rotate: -45deg;
}

.mark-chev[data-dir="next"]::before {
  translate: -50% -50%;
  rotate: 135deg;
}

/* The mark turns a half circle while its upright arm folds down onto the
   crossbar, so a plus resolves into a minus in one movement. */
#toggle[aria-expanded="true"] { rotate: 180deg; }
#toggle[aria-expanded="true"] .bar-v { rotate: 0deg; }

/*-------- LOGO --------*/
/* Cuts in like a signal arriving: a couple of seconds of unstable picture
   every ten, then gone. Opacity jumps in steps rather than fading, which is
   what makes it read as a screen rather than a dissolve. */
/* Inverts against whatever is behind it, exactly like the mark. The artwork is
   black, and difference against black leaves the backdrop untouched — so the
   letterforms are used as a MASK over white instead of drawn directly.
   z-index puts it above the drawer and above an open project. */
#logo {
  position: fixed;
  z-index: 30;
  background: #FFF;
  mask-image: url("media/logo-v.webp");
  mask-size: contain;
  mask-repeat: no-repeat;
  mask-position: center;
  -webkit-mask-image: url("media/logo-v.webp");
  -webkit-mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mix-blend-mode: difference;
  pointer-events: none;
  opacity: 0;
  left: -1.2%;
  top: 11%;
  height: 44%;
  aspect-ratio: 596 / 2600;
  animation: logo-flicker 10s steps(1, end) infinite;
}

@keyframes logo-flicker {
  0%    { opacity: 0; }
  0.6%  { opacity: .85; }
  1.3%  { opacity: .05; }
  2%    { opacity: 1; }
  2.7%  { opacity: .25; }
  3.4%  { opacity: .95; }
  4%    { opacity: 1; }
  12%   { opacity: .4; }
  12.7% { opacity: 1; }
  14.5% { opacity: .1; }
  15.4% { opacity: .8; }
  16.5% { opacity: 0; }
  17.6% { opacity: .45; }
  18.6% { opacity: 0; }
  100%  { opacity: 0; }
}

/* A hard flicker is a photosensitivity risk, so hold it steady instead. */
@media (prefers-reduced-motion: reduce) {
  #logo { animation: none; opacity: .9; }
}

.page { display: none; }

/*-------- PROJECT --------*/
/* Sits over the media the television grew into, filling the same area: the
   whole window on a phone, the space beside the drawer on desktop. */
#project {
  position: fixed;
  inset: 0 0 0 var(--drawer);
  z-index: 12;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--project-bg);
  color: var(--project-ink);
  padding: calc(var(--pad) * 2.6) var(--pad) calc(var(--pad) * 3);
  opacity: 0;
  pointer-events: none;
  transition: opacity 520ms var(--ease);
}

/* Hidden while the screen plays; revealed once the preview is over. */
main[data-reading] #project { opacity: 1; pointer-events: auto; }

/* Blocks rise into place one after another, so the page assembles upward
   rather than appearing all at once. */
#project > * {
  opacity: 0;
  translate: 0 2.2rem;
  transition:
    opacity 700ms var(--ease),
    translate 700ms var(--ease);
  transition-delay: calc(var(--i, 0) * 80ms);
}

main[data-reading] #project > * {
  opacity: 1;
  translate: 0 0;
}

@media (prefers-reduced-motion: reduce) {
  #project > * { transition: none; translate: none; }
}

#project a { color: inherit; }

#project > * + * { margin-top: calc(var(--type) * 3); }

/* The work shows in its own colour. It used to open monochrome and only come
   back on hover, which made every page read the same and made the pictures
   look like an archive of something rather than the thing itself. */
#project img {
  /* Alt text is for screen readers and for search, not for the seconds a
     photograph spends arriving. Left visible it fills every waiting box with
     its own description, which on a phone reads as a page of failed images.
     Transparent hides the words while keeping them in the markup; the box
     underneath already has a colour, so a loading picture is just a dark
     rectangle until it lands. */
  color: transparent;
}

#project h1 { margin-bottom: calc(var(--type) * 1.5); }

/* A heading that is read but not shown. The drawer already names the channel,
   and on Just for fun a title over the field is a label on a window. */
#project h1.quiet {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.block-text p + p { margin-top: 1em; }

/* name against role, in two columns the monospace already lines up */
.pairs {
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: 2ch;
  margin-top: 1.6em;
}

.pairs dd { margin: 0; }
.block-text { max-width: 62ch; }

/*-------- TABS --------*/
/* One type size site-wide, hierarchy from case, so these are not styled as
   buttons: they are words with a rule under the one you are reading. */
.tab-bar {
  display: flex;
  gap: calc(var(--type) * 2);
  margin-bottom: calc(var(--type) * 2.5);
}

.tab {
  font: inherit;
  text-transform: uppercase;
  color: var(--project-ink);
  background: none;
  border: 0;
  padding: 0 0 0.4em;
  cursor: pointer;
  opacity: 0.45;
  border-bottom: 1px solid transparent;
  transition: opacity 160ms var(--ease);
}

.tab:hover { opacity: 0.75; }
.tab[aria-selected="true"] {
  opacity: 1;
  border-bottom-color: currentColor;
}

/* The first block inside a pane must not push away from the tabs as if it
   were following a block of writing. */
.tab-pane > :first-child { margin-top: 0; }
.tab-pane > * + * { margin-top: calc(var(--type) * 3); }

/*-------- A FOLDED BLOCK --------*/
/* Documentation for a show: the entry reads as an entry, the pictures are one
   click away. `details` so it opens without JavaScript and find-in-page can
   still reach the alt text inside. */
.block-collapse > summary {
  cursor: pointer;
  text-transform: uppercase;
  opacity: 0.55;
  padding: 0.4em 0;
  list-style: none;
  transition: opacity 160ms var(--ease);
}

.block-collapse > summary::-webkit-details-marker { display: none; }
.block-collapse > summary:hover { opacity: 1; }

/* A mark that turns, rather than the browser's triangle, so it matches the
   two-stroke vocabulary the rest of the controls are drawn from. */
.block-collapse > summary::before {
  content: "+";
  display: inline-block;
  width: 1.6ch;
  transition: transform 200ms var(--ease);
}

.block-collapse[open] > summary::before { transform: rotate(45deg); }
.block-collapse[open] > summary { opacity: 1; }
.block-collapse > :not(summary) { margin-top: calc(var(--type) * 1.5); }

/*-------- VIDEO --------*/
/* A real grid rather than the column flow the pictures use: these are all the
   same shape and a flow would stagger them for no reason. */
.video-grid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 3), 1fr);
  gap: var(--grid-gap);
  columns: auto;
}

.video-grid > figure { margin: 0; }

.video-grid video {
  width: 100%;
  display: block;
  aspect-ratio: 540 / 960;
  background: #000;
}

/* The pictures sit in the same margin as the writing above them. Run to the
   edges they read as a wall of image with nothing holding them; one page
   width, kept by everything, is what makes the page read as a page. */
.block-grid h2 {
  margin-bottom: calc(var(--type) * 1.2);
}

/* A sentence standing between a heading and the thing it introduces. Held to
   the same measure as the writing blocks so its lines break where theirs do.
   Only the last one carries the gap down to the pictures, so a block without
   any intro keeps exactly the spacing it had. */
.block-intro { max-width: 62ch; }
.block-intro + .block-intro { margin-top: 1em; }
.block-intro:last-of-type { margin-bottom: calc(var(--type) * 1.2); }

/* Columns, not a grid: every photograph keeps its own proportions and the next
   one starts where it ends, so nothing is cropped to make a tidy row. The
   gutter is deliberately much tighter than the page's padding — at this
   spacing the pictures read as one field rather than as separate cards. */
.grid-items {
  columns: var(--cols, 3);
  column-gap: var(--grid-gap);
}

.grid-items > figure {
  margin: 0 0 var(--grid-gap);
  break-inside: avoid;
}

.block-grid img {
  width: 100%;
  height: auto;
  display: block;
  background: #211F28;
}

/* A picture that has not arrived has no size of its own, and `height: auto`
   makes that literal: a nought pixel tall box. Lazy loading then never fires,
   because a box with no height never comes into view — so it stays empty, so
   it stays nought tall. Every image below the first screen was stuck in that
   loop. A square holds the space until the real proportions land. */
.block-grid img:not([data-loaded]) { aspect-ratio: var(--ratio, 1); }

/* An empty slot has no picture to take its height from, so it keeps the
   block's declared ratio. */
.grid-items[data-ratio] .slot { aspect-ratio: var(--ratio, 1); }

/* A slot with no artwork yet. Reads as a space held open on purpose rather
   than a photograph that failed to arrive. */
.slot {
  width: 100%;
  aspect-ratio: var(--ratio, 1);
  background: #211F28;
  border: 1px solid rgba(255, 255, 255, 0.14);
  display: grid;
  place-items: center;
  font-size: var(--type-small);
  opacity: 0.45;
}

.block-grid figcaption { margin-top: 0.6em; padding-inline: 2px; }
.fig-title, .fig-note, .fig-role { display: block; }
/* materials read as the note on the label, not a second title */
.fig-note { opacity: 0.6; }
/* what was played on the record. Set off from the title and the note by a
   gap, because it is a different kind of fact from either of them. */
.fig-role { opacity: 0.45; margin-top: 0.5em; }

/* Uniform sets keep a real grid: every cell the same, read across not down.
   Photographs flow; drawings and record covers do not. */
.grid-items[data-even] {
  columns: auto;
  display: grid;
  grid-template-columns: repeat(var(--cols, 3), 1fr);
  gap: var(--grid-gap);
}
.grid-items[data-even] > figure { margin-bottom: 0; }
/* A thumbnail is cropped to its square. Nobody should be precious about how a
   contact sheet crops: the point of it is that there is more to look at, and
   the lightbox is where the whole picture lives. */
.grid-items[data-even] img { aspect-ratio: var(--ratio, 1); object-fit: cover; }
/* Except a drawing, which is the one thing that must not be cut into. */
.grid-items[data-even][data-fit="contain"] img { object-fit: contain; }

/* A flip pair standing in a row of ordinary thumbnails is a thumbnail too. It
   carried a hardcoded square and a contained fit, which it needs on the marks
   grids and nowhere else — so among photographs it sat shorter than its
   neighbours with its caption riding up above theirs. It takes the block's
   ratio now, and crops the way the pictures either side of it do. */
.grid-items[data-even] .flicker-still { aspect-ratio: var(--ratio, 1); }
.grid-items[data-even]:not([data-fit="contain"]) .flicker-still img {
  object-fit: cover;
}

/* `fit: contain` used to mean "do not crop this". Nothing is cropped any more,
   so it now marks what it was only ever used for: a DRAWING, which wants air
   around it rather than running to its edges. */
.grid-items[data-fit="contain"] img { padding: 8%; }

/*-------- A FEATURED CELL --------*/
/* Two columns wide AND two rows tall, so the picture keeps its proportions.
   Spanning only the width was the first try and it cropped each one to a
   letterbox, which took the head off a portrait and the wheels off a bus.

   The arithmetic has to be checked by hand when items are added. Cells must
   fill whole rows: at four columns each feature costs four cells, so the
   total (features x 4 + the rest) has to divide by four, or a hole opens in
   the middle of the grid where auto-placement could not fit the next one.
   Sixteen pieces with four featured comes to twenty-eight, which is seven
   rows exactly.

   The figure becomes its own little grid so the picture fills the track the
   ordinary cells beside it set, rather than taking a height from its own
   proportions and leaving the caption floating short of the bottom. The few
   pixels of crop that costs are two caption lines across a tile this size. */
.grid-items[data-even] > figure[data-span] {
  grid-column: span 2;
  grid-row: span 2;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
}
.grid-items[data-even] > figure[data-span] img {
  aspect-ratio: auto;
  height: 100%;
}

/*-------- A PICTURE THAT MOVES WHILE A TRACK RUNS --------*/
/* `data-dance` is opt-in per block and only fires while the open project has
   something playing. Deliberately NOT a strobe: the cut is roughly two a
   second, well under the rate that carries a photosensitivity risk, and the
   picture never fully disappears. It shifts, tips and dims, which reads as
   the thing moving to the track rather than as a fault in the screen. */
.block-grid[data-dance] img {
  transform-origin: 50% 60%;
  will-change: translate, rotate, scale;
}

#project[data-playing] .block-grid[data-dance] img {
  animation: pyramid-dance 1.9s steps(1, end) infinite;
}

@keyframes pyramid-dance {
    0% { translate:  0    0;   rotate:  0deg;   scale: 1;     opacity: 1;   }
   12% { translate: -2%  -3%;  rotate: -1.6deg; scale: 1.04;  opacity: .72; }
   25% { translate:  3%   1%;  rotate:  1.2deg; scale: .97;   opacity: 1;   }
   37% { translate: -1%   3%;  rotate:  2.1deg; scale: 1.06;  opacity: .8;  }
   50% { translate:  2%  -2%;  rotate: -0.8deg; scale: 1;     opacity: 1;   }
   62% { translate: -3%   0;   rotate:  1.7deg; scale: 1.03;  opacity: .68; }
   75% { translate:  1%   2%;  rotate: -2deg;   scale: .98;   opacity: 1;   }
   87% { translate:  0   -3%;  rotate:  0.9deg; scale: 1.05;  opacity: .85; }
  100% { translate:  0    0;   rotate:  0deg;   scale: 1;     opacity: 1;   }
}

/* Motion is the whole point of it, so under reduced motion it simply does not
   happen rather than being slowed down. */
@media (prefers-reduced-motion: reduce) {
  #project[data-playing] .block-grid[data-dance] img { animation: none; }
}

/* Anything on a transparent ground belongs ON the page, not on a panel. Once a
   picture has landed it covers its own box, so the colour behind it is either
   invisible or a grey rectangle showing through the transparency. The loading
   colour is still wanted before that, or the box has nothing to hold it open
   with. Narrower than this before, and the wide people mark sat on a panel. */
.block-grid img[data-loaded] { background: transparent; }

/* Grey until you look at it. Scoped to a block that asks, so it reads as a
   set behaving differently rather than as the way everything looks: the site
   stopped being monochrome by default on purpose. */
.block-grid[data-mono] img { filter: grayscale(1); transition: filter 320ms var(--ease); }
.block-grid[data-mono] figure:hover img { filter: none; }
.block-grid[data-mono] figure:focus-within img { filter: none; }

/* A label standing where a link will go. Not underlined, because it is not
   clickable yet, and dimmed so it reads as pending rather than broken. */
.link-pending { opacity: 0.55; }

/*-------- A RECORD --------*/
/* Sleeve on the left, tracks on the right, split down the middle and gapped
   exactly as `.block-feature` is — a record and a zine are the same kind of
   thing on this page, one picture with its account beside it, so they are laid
   out to one measure rather than two that nearly match.
   The minmax keeps a long track title from widening its column: `1fr` is a
   floor of auto, so the tracklist would push the sleeve narrower. */
.block-album {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: calc(var(--pad) * 1.4);
  align-items: start;
}

.album-cover {
  width: 100%;
  aspect-ratio: 1;
  display: block;
  background: #211F28;
  object-fit: cover;
}

.album-record { opacity: 0.6; }
.album-role   { opacity: 0.45; margin-top: 0.5em; }

/* The transport sits under the record's own lines. It used to be introduced by
   the track's name, which repeated the first row of the list directly beneath
   it and put a second title under the album's heading. */
.album-transport { margin-top: calc(var(--type) * 1.6); }

.album-list {
  list-style: none;
  margin: calc(var(--type) * 1.4) 0 0;
  padding: 0;
}

.album-pick {
  width: 100%;
  display: grid;
  grid-template-columns: 2.4em 1fr auto;
  gap: 0.8em;
  align-items: baseline;
  padding: 0.42em 0;
  background: none;
  border: 0;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.album-track:last-child .album-pick { border-bottom: 1px solid rgba(255, 255, 255, 0.12); }

.album-n, .album-d { opacity: 0.45; font-variant-numeric: tabular-nums; }
.album-d { text-align: right; }

.album-pick:hover .album-t { opacity: 0.6; }

/*-------- PLAY AND PAUSE, ON A TRACK --------*/
/* The number and the icon occupy the same cell, so nothing shifts sideways
   when one replaces the other. Both are built from the site's own materials:
   the pause is two strokes at the mark's weight, and the play is the only
   triangle on the site, sized so its bounding box matches them. */
/* Stacked with grid rather than absolute positioning: the number stays IN
   flow, so it still gives the row its baseline and the whole line cannot
   drift when the icon takes over. */
.album-cue { display: grid; }
.album-n, .album-icon {
  grid-area: 1 / 1;
  transition: opacity 160ms var(--ease);
}
.album-icon { position: relative; opacity: 0; }

/* Play: a triangle drawn with borders, so it needs no file and takes its
   colour from the row like everything else. */
.album-icon::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  translate: 0 -50%;
  border-style: solid;
  border-width: 0.36em 0 0.36em 0.58em;
  border-color: transparent transparent transparent currentColor;
}

/* Pause: the same two strokes the rest of the site is built from. The row
   already knows whether it is running, so the icon reads it from there rather
   than being told twice. */
.album-track[data-playing="true"] .album-icon::before {
  border: 0;
  width: 0.19em;
  height: 0.72em;
  background: currentColor;
  box-shadow: 0.3em 0 0 currentColor;
}

/* Hovering a row offers to play it; the row already loaded keeps its icon
   showing, because paused in the middle of a record is a place you are. */
.album-pick:hover .album-n,
.album-track[data-on="true"] .album-n { opacity: 0; }
.album-pick:hover .album-icon,
.album-track[data-on="true"] .album-icon { opacity: 1; }

/* The track that is loaded reads as chosen even when paused. */
.album-track[data-on="true"] .album-t,
.album-track[data-on="true"] .album-d,
.album-track[data-on="true"] .album-icon { opacity: 1; }

@media (max-width: 749px) {
  /* One column, and the sleeve takes the whole of it. Held to 60% it sat in a
     column of its own with the page's edge a long way off to the right, which
     read as a thumbnail of a record rather than the record. */
  .block-album { grid-template-columns: 1fr; gap: var(--grid-gap); }
}

/* a release is its art, a title, and where to hear it */
.release ul { list-style: none; margin-top: 0.4em; }
.release li { display: inline; }
.release li + li::before { content: " / "; }

/* The player is the site's mark at a smaller gauge. The 85:6 ratio the plus
   was drawn at is held exactly, so it reads as the same two strokes rather
   than a different control that happens to be near them. */
.track {
  --mark-size: 40px;
  --mark-weight: 3px;
  margin-top: 1.2em;
  max-width: 520px;
  display: grid;
  grid-template-columns: var(--mark-size) 1fr auto;
  grid-template-areas: "title title title" "play line time";
  align-items: center;
  column-gap: 16px;
  row-gap: 0.4em;
}

.track audio { display: none; }
.track-title { grid-area: title; }

.track-play {
  grid-area: play;
  mix-blend-mode: normal;          /* it sits on the project's own dark ground */
  color: inherit;
  flex: none;
}
.track-play .bar { background: currentColor; }

/* plus while stopped, minus while running — the drawer's move, reused */
.track-play[data-playing="true"] { rotate: 180deg; }
.track-play[data-playing="true"] .bar-v { rotate: 0deg; }

/* the minus, stretched: played at full weight, the rest of the track faint */
.track-line {
  grid-area: line;
  position: relative;
  height: var(--mark-size);
  cursor: pointer;
  touch-action: none;
}
.track-line::before,
.track-fill {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  height: var(--mark-weight);
  margin-top: calc(var(--mark-weight) / -2);
  background: currentColor;
}
.track-line::before { width: 100%; opacity: 0.25; }
.track-fill { width: 0; }
.track-line:focus-visible { outline: var(--mark-weight) solid currentColor; outline-offset: 6px; }

.track-time {
  grid-area: time;
  font-variant-numeric: tabular-nums;
}

@media (max-width: 749px) {
  .track { --mark-size: 32px; --mark-weight: 2px; column-gap: 12px; }
}

.block-links ul { list-style: none; }
.block-links li + li { margin-top: 0.5em; }

/*---- feature: carousel one half, writing the other ----*/
/*-------- TWO CAROUSELS IN A ROW --------*/
/* A cassette is small, so two of them fit where one photograph of a zine needs
   the whole width. Each is named underneath rather than beside, which keeps
   the two pictures level with one another. */
.block-pair h2 { margin-bottom: calc(var(--type) * 1.2); }

.pair-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: calc(var(--pad) * 1.4);
  align-items: start;
}

.pair-one figcaption { margin-top: 0.6em; padding-inline: 2px; }

@media (max-width: 749px) {
  /* Stacked on a phone, where half a column is too small to read a J-card in. */
  .pair-row { grid-template-columns: 1fr; gap: calc(var(--type) * 2); }
}

.block-feature {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: calc(var(--pad) * 1.4);
  align-items: start;
}

.carousel-main {
  width: 100%;
  display: block;
  background: #211F28;
  cursor: pointer;
}

/* The tape shots are a mix of flat artwork and photographs of an object, so
   their proportions are all over the place and the row jumped every time the
   picture changed. The frame is a square and holds it: what is inside can be
   any shape it likes, and the lightbox has the whole of it. */
.block-pair .carousel-main {
  aspect-ratio: 1;
  object-fit: cover;
}

/* Four across, always at that size. Fewer than four and they simply end where
   they end rather than stretching to fill the row; more and the strip scrolls,
   which a trackpad can do on its own and a mouse gets arrows for. */
.carousel-strip { position: relative; margin-top: 8px; }

.carousel-thumbs {
  --thumbs: 4;
  --thumb-gap: 8px;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns:
    calc((100% - (var(--thumbs) - 1) * var(--thumb-gap)) / var(--thumbs));
  gap: var(--thumb-gap);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
}
.carousel-thumbs::-webkit-scrollbar { display: none; }
.thumb { scroll-snap-align: start; }

/* The arrows only exist when there is somewhere to go. Difference blending so
   they read over a light thumbnail and a dark one alike. */
.strip-step {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  --mark-size: 30px;
  --mark-weight: 3px;
  color: #FFF;
  mix-blend-mode: difference;
  z-index: 2;
}
.strip-step[data-dir="prev"] { left: 2px; }
.strip-step[data-dir="next"] { right: 2px; }
.carousel-strip:not([data-more="true"]) .strip-step { display: none; }
.strip-step[disabled] { opacity: .2; cursor: default; }

@media (max-width: 749px) {
  /* Four across here too. They come out small on a phone, which is what a
     thumbnail is for: it says how many there are and where you are, and the
     picture above it is the one you are actually looking at. */
  .carousel-thumbs {
    --thumbs: 4;
    --thumb-gap: 6px;
    gap: var(--thumb-gap);
    grid-auto-columns:
      calc((100% - (var(--thumbs) - 1) * var(--thumb-gap)) / var(--thumbs));
  }
}

.thumb {
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  opacity: 0.45;
  transition: opacity 220ms var(--ease);
}

.thumb[data-on="true"], .thumb:hover { opacity: 1; }
.thumb img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; }

.kicker { opacity: 0.6; }
.inline-links { list-style: none; margin-top: 1.2em; }
.inline-links li { display: inline; }
.inline-links li + li::before { content: " / "; }

/* enlarge one image without leaving the project */
#lightbox {
  position: fixed;
  inset: 0;
  z-index: 40;
  /* fully opaque: at 94% the page beneath showed through and copy sat on copy */
  background: #0A090E;
  display: grid;
  place-items: center;
  /* clear of the chevrons at the sides and the close mark at the top */
  padding: calc(var(--pad) * 2 + var(--mark-size)) calc(var(--pad) * 2 + var(--mark-size));
  cursor: zoom-out;
}

.lightbox-frame {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: calc(var(--pad) * 1.4);
  align-items: center;
  /* the parent centres its child, so without a width the frame shrinks to its
     content — and a flicker pair has no intrinsic width to shrink to */
  width: 100%;
  max-width: 1400px;
  cursor: default;
}

.lightbox-media { min-width: 0; }
.lightbox-media img { width: 100%; max-height: 78vh; object-fit: contain; display: block; }
/* the pair is absolutely positioned inside, so it needs a width to size from */
.lightbox-media .flicker-still {
  width: min(100%, 78vh);
  aspect-ratio: 1;
  margin-inline: auto;
}
.lightbox-media .flicker-still img { position: absolute; inset: 0; height: 100%; }
.lightbox-copy { color: var(--project-ink); }
.lightbox-copy > * + * { margin-top: 1em; }
.lightbox-count { opacity: 0.45; }

.lightbox-step {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  opacity: 0.7;
  transition: opacity 200ms var(--ease);
}

.lightbox-step:hover { opacity: 1; }
.lightbox-step.prev { left: var(--pad); }
.lightbox-step.next { right: var(--pad); }

#lightbox .mark[data-kind="x"] {
  position: absolute;
  top: var(--pad);
  right: var(--pad);
}

/* The strip belongs to the phone for now. On a wide screen the picture and its
   writing already stand side by side with room around both. */
.lightbox-strip { display: none; }

@media (max-width: 749px) {
  .block-feature { grid-template-columns: 1fr; }

  /* ---- THE LIGHTBOX ON A PHONE ----
     Edge to edge. It used to sit inside 113px of padding on each side — room
     kept clear for the arrows and the close mark — which on a 390px screen
     left the picture 164px wide, smaller than the thumbnail it was opened
     from.

     So the frame dissolves (display: contents) and the picture, the writing,
     the thumbs and the two arrows all become rows of one grid on the screen
     itself. The picture takes whatever is left after the band at the bottom,
     and the arrows share its row rather than centring on a screen whose lower
     third is text. */
  #lightbox {
    padding: 0;
    grid-template-columns: 100%;
    grid-template-rows: minmax(0, 1fr) auto auto;
    place-items: stretch;
  }
  .lightbox-frame { display: contents; }

  /* The picture fills its row and no more. It used to be sized by
     `max-height: 100%`, which inside this grid resolves against nothing, so a
     tall picture took its natural height and ran down over its own title and
     the thumbnails. Pinned to the row's edges and contained, it can run to the
     top of the screen and the sides, under the arrows and the close mark, but
     never past the line where the writing starts. */
  .lightbox-media {
    grid-row: 1;
    grid-column: 1;
    position: relative;
    min-height: 0;
  }
  .lightbox-media img,
  .lightbox-media .flicker-still {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    max-height: none;
    aspect-ratio: auto;
    object-fit: contain;
  }

  /* Over the picture, difference blended, so they read on a white sleeve and a
     black one alike — which is the whole reason they can stop reserving a
     margin of their own. */
  .lightbox-step {
    position: static;
    grid-row: 1;
    grid-column: 1;
    align-self: center;
    translate: 0;
    opacity: 1;
    z-index: 2;
  }
  .lightbox-step.prev { justify-self: start; margin-left: 6px; }
  .lightbox-step.next { justify-self: end; margin-right: 6px; }

  /* The same place every time, whatever shape the picture turns out to be.
     The band is deliberately tight: every pixel it takes comes off the
     picture, which is the thing being looked at. */
  .lightbox-copy {
    grid-row: 2;
    padding: 14px 16px 0;
  }
  .lightbox-copy > * + * { margin-top: 0.4em; }
  .lightbox-strip {
    display: block;
    grid-row: 3;
    margin-top: 0;
    padding: 10px 16px 14px;
  }
  /* The strip keeps its own arrows on a page, where it is the only way
     through the pictures. In here it is not: the big arrows move the picture
     and the strip follows them, so a second smaller pair would be two ways to
     do one thing, six inches apart. Dragging it still works. */
  .lightbox-strip .strip-step { display: none; }
}

@media (max-width: 749px) {
  /* The writing takes the whole screen once the preview is over, the same as
     it does on desktop. The phone used to keep a band of the screen showing
     above it, which meant the media never got the screen to itself. The top
     padding clears the mark and the signature. */
  #project {
    inset: 0;
    padding: calc(var(--pad) + var(--mark-size) + 24px) var(--pad) calc(var(--pad) * 3);
  }
  .grid-items:not([data-single]) { --cols: 2 !important; }


  /* `captions: 'wide'`. A title and its materials run one line each in a
     desktop column and four or five in half a phone, which pushes the next
     row of pictures off the bottom of the screen. The lightbox still names
     every piece, so nothing is lost — it just has to be asked for. */
  .grid-items[data-captions="wide"] figcaption { display: none; }

  /* `strip`: the photographs under a zine, three across in one row that your
     thumb pushes sideways. When there are more than three, the row is cut a
     little short of the fourth, so the edge of the next one shows and says
     there is more to the right. */
  .grid-items[data-strip] {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: calc((100% - 2 * var(--grid-gap)) / 3);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .grid-items[data-strip]:has(> :nth-child(4)) {
    grid-auto-columns: calc((100% - 3 * var(--grid-gap)) / 3.3);
  }
  .grid-items[data-strip]::-webkit-scrollbar { display: none; }
  .grid-items[data-strip] > figure { scroll-snap-align: start; }

  /* `mobileColumns: 1`: one to a row, each at its own shape. A jewel case is
     wide and a layout sheet is tall, and forced into the same square on a
     phone both came out small with a band of nothing around them. */
  .grid-items[data-mobile-one] { --cols: 1 !important; }
  .grid-items[data-mobile-one] img { padding: 0; }
  /* Only once it has arrived. A picture still loading keeps the placeholder
     square: given its own shape before it has one, it is nought pixels tall,
     never scrolls into view, and so never loads. */
  .grid-items[data-mobile-one] img[data-loaded] {
    aspect-ratio: auto;
    height: auto;
  }

  /* Two columns is already the whole screen, so a feature only needs the
     width. It goes back to taking its height from its own proportions,
     because alone on a row it has no neighbour to take one from. */
  .grid-items[data-even] > figure[data-span] {
    grid-row: auto;
    display: block;
  }
  .grid-items[data-even] > figure[data-span] img {
    aspect-ratio: var(--ratio, 1);
    height: auto;
  }
}

/*-------- JUST FOR FUN --------*/
/* The section is tall and the field inside it is pinned, so scrolling past
   the section leaves the drawings on screen and stirs them instead. It runs
   to the panel's edges rather than sitting inside the page's padding: things
   that float should be able to float off the side. */
.block-float {
  /* Long, because the scroll bar is the handle that stirs the field. A short
     section ran out of stirring after a flick and a half. */
  height: 560vh;
  margin-inline: calc(var(--pad) * -1);
}

/* The title and the line under it read over the field, and take no clicks, so
   a drawing behind them can still be tapped. */
/* Fully black on this page. Everywhere else the panel is not quite opaque, so
   the screen you clicked stays faintly present behind the writing. Neon wants
   nothing behind it at all. */
#project[data-float] { background: #000; }

#project[data-float] > *:not(.block-float) {
  position: relative;
  z-index: 2;
  pointer-events: none;
}
#project[data-float] .block-text p { max-width: 34ch; }

/* Fixed to the panel, not pinned inside the section. Pinned, it let go the
   moment the section's end came past — which is what scrolling hard to the
   bottom did, sliding the field up and showing a band of page beneath it. The
   section above is now only a spacer: it exists to give the scroll bar
   something to travel, and the field never moves at all. */
.float-field {
  position: fixed;
  inset: 0 0 0 var(--drawer);
  overflow: hidden;
  /* a finger dragging across the field still scrolls the page */
  touch-action: pan-y;
  /* NOT decoration. iOS Safari will not dispatch a tap to a plain div whose
     handler came from addEventListener unless it looks interactive, and this
     is what makes it look interactive. Without it every tap on a phone went
     nowhere and the drawings read as broken. The JS uses pointer events now,
     which is the other half of the same fix. */
  cursor: pointer;
}

/* No drawer to sit beside on a phone. This has to come after the rule it
   overrides, not in the mobile block higher up the file, or it loses. */
@media (max-width: 749px) {
  .float-field { inset: 0; }
}

/* The blocks on a page rise into place, and that movement is a transform,
   which would trap the fixed field inside this section and undo all of the
   above. This one arrives without it. */
/* Everything on this page arrives already in place. The rise is a transform,
   and a transform on an ancestor traps the fixed field inside it; it also
   meant the writing was measured mid-flight, so the drawings were kept clear
   of where the words had been a moment earlier rather than where they are. */
#project[data-float] > *,
main[data-reading] #project[data-float] > *,
#project > .block-float,
main[data-reading] #project > .block-float {
  /* `none`, not `0 0`: a translate of nothing is still a transform, and a
     transform on an ancestor is enough to trap a fixed child inside it. That
     is exactly what was happening — the field measured five thousand pixels
     tall, the height of this section, rather than the height of the window. */
  translate: none;
  opacity: 1;
  transition: none;
}

/* The glow is drawn here rather than baked into the file. Two shadows of the
   tube's own colour: a tight one that makes the tube look lit, and a wide one
   that is the light it throws. Each sprite is its own layer, so moving it
   does not make the browser draw the glow again. */
.float-sprite {
  --s: clamp(78px, 8vw, 132px);
  position: absolute;
  left: 0;
  top: 0;
  width: var(--s);
  height: var(--s);
  will-change: transform;
  /* The field decides what a tap meant, so the pictures stay out of it. */
  pointer-events: none;
  /* Two halos, and no more: a tight one that makes the tube look lit and a
     wider one that is the light it throws. A third, wider still, looked
     marvellous and cost double — 33ms a frame on a desktop and 50 on a phone,
     against 16.7 for two. The warmth lives in the colour and the tube, which
     are baked in and free. */
  filter:
    drop-shadow(0 0 2px var(--c))
    drop-shadow(0 0 14px var(--c));
  cursor: pointer;
  user-select: none;
  -webkit-user-drag: none;
  transition: filter 380ms var(--ease);
}
.float-sprite[data-big] { --s: clamp(140px, 14.5vw, 240px); }

/* a tap turns it up for a moment */
.float-sprite[data-lit] {
  filter:
    drop-shadow(0 0 3px var(--c))
    drop-shadow(0 0 20px var(--c))
    brightness(1.3);
  transition-duration: 60ms;
}

/* Thrown off when a fed drawing bursts and drops back to its own size. */
.float-spark {
  position: absolute;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--c);
  box-shadow: 0 0 6px var(--c), 0 0 16px var(--c);
  pointer-events: none;
  will-change: transform, opacity;
}

/* A tube that is on its way out. Two short dips in a quarter of a second,
   then nothing for six — nowhere near the rate that carries a
   photosensitivity risk, and only ever dimming, never going dark. */
@keyframes neon-catch {
  0%, 90%, 100% { opacity: 1; }
  91% { opacity: .5; }
  92% { opacity: 1; }
  94% { opacity: .7; }
  95% { opacity: 1; }
}
.float-sprite[data-flicker] {
  animation: neon-catch 7s linear infinite;
  animation-delay: var(--fd, 0s);
}

@media (prefers-reduced-motion: reduce) {
  .block-float { height: auto; }
  .float-field { position: absolute; inset: auto; height: 100vh; width: 100%; }
  .float-sprite[data-flicker] { animation: none; }
}

/*-------- STAGE --------*/
/* The room and the sculpture never move. The drawer simply slides over them,
   and no screen is ever placed where it would be covered. */
#stage {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--room-top);
}

.layer { position: absolute; inset: 0; }

/* The whole installation is one fixed-aspect artwork. fitScene() in app.js
   sizes it to cover the stage, so the gallery always bleeds to the edges and
   nothing inside it distorts. Everything else is positioned as a percentage
   of this box, which is what lets the sets sit on the photographed floor. */
/* The sky is built and wired but deliberately hidden: it pulled attention away
   from the work, and the ceiling reads better as clean white. Delete this one
   line to bring it back — nothing else needs changing, app.js notices it is
   visible and loads the file then. Hidden, it costs no download. */
#sky { display: none; }

#sky {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
  pointer-events: none;
}

/* Anchored to the bottom, full width. fitScene() sets the height. */
#scene {
  z-index: 1;
  position: absolute;
  left: 0;
  bottom: 0;
  translate: none;
}

/*-------- CABLE --------*/
/* Empty layers, in front of and behind the sets, waiting on real artwork.
   tools/make-wires.py still generates the drawn SVG version if wanted. */
.wire {
  position: absolute;
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

#wires-back, #wires-front { pointer-events: none; }

#stage:not([data-expanded=""]) #wires-back,
#stage:not([data-expanded=""]) #wires-front {
  opacity: 0;
  transition: opacity var(--fade) var(--ease);
}

/* Layer 1 — gallery background, the white room */
#room {
  background: url("media/room.webp") center / cover no-repeat;
}

/* Layer 2 — wires and detritus */
#wires { background: none; }

/* The room recedes once a project takes over the screen */
#stage:not([data-expanded=""]) #room {
  opacity: 0;
  transition: opacity var(--fade) var(--ease);
}

/*-------- TELEVISIONS --------*/
.tv {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--w);
  height: var(--h);
  transform: rotate(var(--r));
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
  transition:
    left var(--speed) var(--ease),
    top var(--speed) var(--ease),
    width var(--speed) var(--ease),
    height var(--speed) var(--ease),
    transform var(--speed) var(--ease),
    scale 200ms var(--ease);
}

/* Something has to say a television is a thing you can press. `scale` is its
   own property here rather than part of the transform, so it can move at
   hover speed while the transform keeps the slow curve the takeover needs.
   Only while nothing is expanded, and only where there is a real pointer:
   a touch screen reports no hover and would otherwise leave a set stuck
   larger after a tap. */
@media (hover: hover) and (pointer: fine) {
  #stage[data-expanded=""] .tv:hover { scale: 1.035; }
}

/* Layer 3 — the media, sitting in the screen aperture */
.tv-media {
  position: absolute;
  left: var(--sx);
  top: var(--sy);
  width: var(--sw);
  height: var(--sh);
  overflow: hidden;
  background: #111;
  transition:
    left var(--speed) var(--ease),
    top var(--speed) var(--ease),
    width var(--speed) var(--ease),
    height var(--speed) var(--ease);
}

.tv-media img,
.tv-media video { width: 100%; height: 100%; object-fit: cover; display: block; }

/* A pointer, wandering the laptop's screen. Its height is a fraction of the
   screen it sits on, so it stays in proportion at every viewport — and the
   fraction is small, because a pointer on a real 13 inch machine is small.
   The image is cropped to the arrow, so its top left corner IS the tip and
   left/top can be read as where the pointer is. */
.cursor {
  position: absolute;
  height: var(--cursor-size, 8.5%);
  aspect-ratio: 210 / 330;
  background: url("media/cursor.webp") center / contain no-repeat;
  pointer-events: none;
  animation: wander 34s var(--ease) infinite;
}

/* Long holds with short moves between them. A pointer spends most of its life
   parked, and moves the rest of it in well under a second. */
@keyframes wander {
  0%,  100% { left: 12%; top: 78%; }
  4%,   14% { left: 58%; top: 26%; }
  18%,  28% { left: 30%; top: 50%; }
  32%,  41% { left: 82%; top: 64%; }
  45%,  55% { left: 46%; top: 16%; }
  59%,  68% { left: 18%; top: 36%; }
  72%,  82% { left: 74%; top: 82%; }
  86%,  96% { left: 52%; top: 54%; }
}

/* Layer 4 — cathode ray texture, rides along with the media */
.tv-crt {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(
      to bottom,
      rgba(0,0,0,.22) 0 1px,
      transparent 1px 3px
    ),
    radial-gradient(120% 120% at 50% 50%, transparent 55%, rgba(0,0,0,.45) 100%);
}

/* Layer 5 — the television itself, screen removed, transparent WebP */
.tv-frame {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--frame);
  background-size: 100% 100%;
  background-repeat: no-repeat;
  transition: opacity var(--fade) var(--ease);
  /* drop-shadow follows the cutout's alpha, so the shadow is the set's real
     silhouette rather than its bounding box */
  filter: drop-shadow(0 1.2% 14px rgba(0, 0, 0, .42));
}

/* Stand-in for the real cutout artwork. Sits behind the media rather than
   on top of it, since a placeholder has no transparent aperture to see through.
   Delete once .tv-frame has a real --frame WebP. */
.tv-body {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: #9A948C;
  border-radius: 3% / 4%;
  box-shadow:
    inset 0 0 0 1px rgba(0,0,0,.3),
    0 2px 14px rgba(0,0,0,.25);
  transition: opacity var(--fade) var(--ease);
}

.tv[data-frame="image"] .tv-body { display: none; }

.tv[data-state="expanded"] .tv-body { opacity: 0; }

/*-------- SCREEN TAKEOVER --------*/
/* The media escapes the aperture and fills the stage; the set fades away. */
/* not yet coupled to a project, so it should not invite a click */
.tv[data-scenery] { cursor: default; }

/* the target is measured by lockToStage() and written as these variables */
.tv[data-state="expanded"] {
  left: var(--el, 0%);
  top: var(--et, 0%);
  width: var(--ew, 100%);
  height: var(--eh, 100%);
  transform: none;
  z-index: 20;
  cursor: default;
}

.tv[data-state="expanded"] .tv-media {
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
}

.tv[data-state="expanded"] .tv-frame { opacity: 0; }

#stage:not([data-expanded=""]) .tv:not([data-state="expanded"]) {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--fade) var(--ease);
}

/*-------- MOBILE --------*/
@media (max-width: 749px) {
  :root {
    --mark-size: 49px;
    --mark-weight: 4px;
    /* Thirteen channels, not ten. At the desktop rhythm the list ran off the
       bottom of a phone and took the credits with it. */
    --nav-rhythm: calc(var(--type) * 2);
    /* The list starts just under the menu mark and the signature rather than
       at the desktop's depth, which on a phone was a hand's width of empty
       drawer above fourteen channels crowding the credits at the bottom. */
    --nav-top: calc(var(--pad) + var(--mark-size) + 44px);
  }

  html, body { height: 100%; overflow: hidden; }
  body[data-locked] { overflow: hidden; }

  main { position: static; height: 100%; overflow: visible; }

  /* The same sculpture as desktop, enlarged until one screen frames a slice of
     it, panned across freely. Deliberately NOT snapped: the sculpture reads as
     one continuous object, so stopping between two sets has to be allowed
     rather than sprung away from. */
  #stage {
    position: relative;
    height: 100dvh;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    background: var(--room-top);
  }

  /* An open project fills the phone, so there is nothing left to pan to. The
     scroller has to be shut off or a flick drags the opened screen aside. */
  #stage[data-frozen] {
    overflow-x: hidden;
    touch-action: none;
    overscroll-behavior: none;
  }

  /* fitScene() sets width, height and the left offset; the scene keeps its
     desktop proportions and stays anchored to the floor. */
  #scene {
    position: absolute;
    left: 0;
    bottom: 0;
    translate: none;
  }

  /* the portrait photograph is full bleed, so there is no wall to continue */

  /* Width spacers only. No pixels of their own; they exist so the scroll
     container knows how far the sculpture extends. */
  .page {
    display: block;
    position: absolute;
    top: 0;
    height: 1px;
    pointer-events: none;
  }



  /* Fixed so the drawer covers the screen whatever the pager is doing, and
     ABOVE an open project. On a phone the drawer is the only navigation there
     is, so at z-index 3 it opened behind the project and you were stuck on
     whatever page you were already reading. Below the toggles at 20, which
     have to stay reachable to close it again. */
  /* Scrolls as a safety net: whatever the rhythm, a list long enough to pass
     the bottom must stay reachable rather than being quietly cut off.
     `overscroll-behavior` keeps that scroll from chaining to the wall behind. */
  nav {
    position: fixed;
    width: 100%;
    z-index: 15;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  #backdrop { display: none; }   /* the drawer covers the screen on a phone */


  /* Laid across the top, filling whatever room is left between the mark and
     the right margin and never more, and running off the top edge so the
     letterforms clip against it. */
  #logo {
    mask-image: url("media/logo-h.webp");
    -webkit-mask-image: url("media/logo-h.webp");
    left: calc(var(--pad) + var(--mark-size) + 20px);
    right: 8px;
    top: -8px;
    width: auto;
    height: auto;
    aspect-ratio: 2600 / 596;
  }
}

/*-------- FLICKERING STILLS --------*/
/* Two images stacked; the top one is cut in and out on a stepped animation so
   the change reads as a channel flipping rather than a dissolve. Slightly
   uneven timings stop it feeling metronomic. */
.flicker {
  position: absolute;
  inset: 0;
}

.flicker img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* in a grid the pair needs its own box; on a screen it fills the aperture */
.flicker-still {
  position: relative;
  aspect-ratio: 1;
}

.flicker-still img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }

/* In a list of still pictures a pair is a HOVER, not a flicker. Cutting
   between two designs on its own reads as a fault when nothing else on the
   page is moving, and a hard flicker is a photosensitivity risk. The white
   face is what you see; the black one is the same design turned over, and you
   get it by asking. On the SCREENS the flicker stays, because there it is a
   television changing channel. */
.flicker-still img:last-child {
  opacity: 0;
  transition: opacity 200ms var(--ease);
}
/* Kept as separate rules on purpose: one bad selector in a comma list throws
   the WHOLE rule away, so pairing :hover with a :focus pseudo-class means a
   browser that dislikes the second silently loses the first as well. */
.flicker-still:hover img:last-child { opacity: 1; }
.flicker-still:focus-within img:last-child { opacity: 1; }

/* slides hold, then cross into one another. A slideshow is a flicker for
   layout only — the cut animation below must never reach it, so the pair is
   selected as :not(.slides) rather than left to source order. */
.slides img { opacity: 0; transition: opacity 900ms var(--ease); }
.slides img[data-on] { opacity: 1; }

/* Ken Burns, on the screens that ask for it. The move starts when the slide
   becomes the live one, because that is when the selector begins to match, so
   it stays in step with the timer without being driven by it. Neighbouring
   slides push the opposite way and off a different corner, which stops four
   works in a row reading as one drift. */
/* Scaling away from a corner is what makes it drift rather than just swell,
   so each slide takes a different one and alternates in and out. */
.slides[data-pan] img[data-on] {
  animation: kb-in var(--pan-time, 7s) linear both;
}
.slides[data-pan] img { transform-origin: 0% 0%; }
.slides[data-pan] img:nth-child(2n) { transform-origin: 100% 0%; }
.slides[data-pan] img:nth-child(4n + 3) { transform-origin: 0% 100%; }
.slides[data-pan] img:nth-child(4n) { transform-origin: 100% 100%; }
.slides[data-pan] img:nth-child(2n)[data-on] { animation-name: kb-out; }

/* One image alone is never replaced, so a single pass would finish and then
   sit still for as long as the set is on screen. It breathes in and back out
   instead, eased at both ends so the turn is not a jolt. */
.slides[data-pan] img:only-child[data-on] {
  transform-origin: 35% 30%;
  animation: kb-in var(--pan-time, 7s) var(--ease) infinite alternate;
}

@keyframes kb-in  { from { transform: scale(1);     } to { transform: scale(1.13); } }
@keyframes kb-out { from { transform: scale(1.13);  } to { transform: scale(1);    } }

/* `:not(.flicker-still)` is named rather than left to specificity: the hover
   rules for a grid pair are a lower weight than this selector, and a tie
   decided by source order has silently broken this pair once already. */
.flicker:not(.slides):not(.flicker-still) img:last-child {
  animation: flick 2.4s steps(1, end) infinite;
}

@keyframes flick {
  0%    { opacity: 0; }
  46%   { opacity: 0; }
  48%   { opacity: 1; }
  52%   { opacity: 0; }
  54%   { opacity: 1; }
  94%   { opacity: 1; }
  96%   { opacity: 0; }
  97.5% { opacity: 1; }
  99%   { opacity: 0; }
  100%  { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  /* the grid pairs are excluded: a hover swap is not motion, and forcing
     opacity here would pin every one of them to its black face */
  .flicker:not(.flicker-still) img:last-child { animation: none; opacity: 1; }
  /* the single-image rule outweighs the one above it, so it has to be named */
  .slides[data-pan] img[data-on],
  .slides[data-pan] img:only-child[data-on] { animation: none; }
  .cursor { animation: none; }
}

/*-------- PLACEHOLDER --------*/
.placeholder {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  color: rgba(255,255,255,.5);
  font-size: 11px;
}

/*-------- THE POINTER --------*/
/* The cursor is the site's own mark: the same two strokes at the same weight
   as the drawer's toggle, the close and every play button. Difference blending
   means it never needs a second colour, inverting against the white drawer, a
   photograph and a dark project page alike, exactly as the signature does.

   The system cursor is only hidden once app.js has confirmed a real mouse, so
   no JavaScript or a touch screen leaves the ordinary one alone. */
#pointer {
  --mark-size: 22px;
  --mark-weight: 2px;
  position: fixed;
  left: 0;
  top: 0;
  width: var(--mark-size);
  height: var(--mark-size);
  margin: calc(var(--mark-size) / -2) 0 0 calc(var(--mark-size) / -2);
  z-index: 60;
  pointer-events: none;
  color: #FFF;
  mix-blend-mode: difference;
  opacity: 0;
  rotate: 0deg;
  scale: 1;
  transition:
    opacity 160ms var(--ease),
    rotate var(--mark-speed) var(--mark-ease),
    scale 180ms var(--ease);
}

/* Nothing is drawn until the first movement, so it cannot flash in a corner
   on load. */
#pointer[data-on] { opacity: 1; }

/* Over something that opens, the mark grows: the same gesture as the
   television lifting under it, at the same moment. */
#pointer[data-kind="open"] { scale: 1.5; }

/* Over the close, it turns the quarter circle the close button itself turns. */
#pointer[data-kind="close"] { rotate: 45deg; scale: 1.5; }

/* Over a book, the mark becomes the direction the page would turn: the same
   mitred stroke as the lightbox arrows, drawn from the same two borders. */
#pointer-chev {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 140ms var(--ease);
}
#pointer-chev::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 46%;
  height: 46%;
  border-left: var(--mark-weight) solid currentColor;
  border-top: var(--mark-weight) solid currentColor;
  translate: -50% -50%;
  rotate: -45deg;
}
#pointer[data-kind="next"] #pointer-chev::before { rotate: 135deg; }

#pointer[data-kind="prev"] #pointer-chev,
#pointer[data-kind="next"] #pointer-chev { opacity: 1; }
#pointer[data-kind="prev"] .bar,
#pointer[data-kind="next"] .bar { opacity: 0; }
#pointer[data-kind="prev"],
#pointer[data-kind="next"] { scale: 1.8; }

/* Dimmed at the first spread and the last, so the arrow says the page will not
   turn before you press and find out. */
#pointer[data-dim] { opacity: .3; }

#pointer-word {
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  font-size: var(--type-small);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 140ms var(--ease);
}

/* A word replaces the strokes rather than joining them, so the pointer is
   either a mark or a verb and never both at once. */
#pointer[data-word] .bar { opacity: 0; }
#pointer[data-word] #pointer-word { opacity: 1; }
#pointer[data-word] { scale: 1; rotate: 0deg; }

/* Scoped to the pointer: the toggle and the close keep the transition they
   were given, which this would otherwise have replaced site-wide. */
#pointer .bar { transition: opacity 140ms var(--ease), rotate var(--mark-speed) var(--mark-ease); }

/* `!important` because several pictures carry an inline cursor of their own,
   and this has to beat them. Scoped to the flag app.js sets, so it can only
   ever apply on a machine that has been checked. */
html[data-pointer] ,
html[data-pointer] * { cursor: none !important; }

/* Except the drawer, which keeps the ordinary cursor: a list of links is where
   a hand is expected, and the mark read as "add a channel" over it. */
html[data-pointer] nav,
html[data-pointer] nav * { cursor: auto !important; }
html[data-pointer] nav a { cursor: pointer !important; }

/*-------- A ZINE, READ AS A BOOK --------*/
/* Each file is a whole spread, so a page is half an image: the background is
   twice the width of its box and slid to one end. Flat throughout, like the
   rest of the site. The fold is a hairline at the weight the tracklist rules
   use, not a gradient pretending there is a lamp overhead. */
.block-book h2 { margin-bottom: calc(var(--type) * 1.2); }

.book {
  width: 100%;
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* one page is half of a 2376x1836 spread, so the pair is that twice over */
  aspect-ratio: calc(1188 / 1836 * 2);
  position: relative;
  perspective: 2400px;
  background: #0E0D12;
  cursor: pointer;
}

.book-page {
  position: relative;
  background-color: #0E0D12;
  background-repeat: no-repeat;
  background-size: 200% 100%;
}
.book-left  { background-position: 0 0; }
.book-right { background-position: 100% 0; }

.book-gutter {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 50%;
  pointer-events: none;
  border-left: 1px solid rgba(255, 255, 255, 0.12);
}

/* The leaf exists only while a page is turning. */
.book-leaf {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50%;
  transform-style: preserve-3d;
  transition: transform 620ms cubic-bezier(.62,.02,.34,1);
  z-index: 3;
}
.book-leaf[data-dir="next"] { left: 50%; transform-origin: left center; }
.book-leaf[data-dir="prev"] { left: 0;   transform-origin: right center; }
.book-leaf[data-dir="next"][data-go] { transform: rotateY(-180deg); }
.book-leaf[data-dir="prev"][data-go] { transform: rotateY(180deg); }

.book-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  background-repeat: no-repeat;
  background-size: 200% 100%;
  background-color: #0E0D12;
}
.book-back { transform: rotateY(180deg); }

.book-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.4em;
  margin-top: calc(var(--type) * 1.2);
}
.book-step {
  --mark-size: 34px;
  --mark-weight: 3px;
  mix-blend-mode: normal;      /* it sits on the project's own dark ground */
  color: var(--project-ink);
}
.book-step[disabled] { opacity: .25; cursor: default; }
.book-count { font-variant-numeric: tabular-nums; opacity: .6; }

/* The turn is the whole of it, so under reduced motion the page simply
   changes rather than being slowed down. */
@media (prefers-reduced-motion: reduce) {
  .book-leaf { transition: none; }
}

/*-------- A SLIDESHOW --------*/
/* Photographs holding and crossing into one another on the shared ticker. No
   arrows and no thumbnails: it changes on its own, and anyone who wants to
   stop and look opens the lightbox. */
.slideshow {
  position: relative;
  width: 100%;
  aspect-ratio: var(--ratio, 3 / 2);
  overflow: hidden;
  background: #211F28;
  cursor: pointer;
}

.slideshow img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 900ms var(--ease);
}
.slideshow img[data-on] { opacity: 1; }

/* The crossfade is the whole of it, so under reduced motion the picture simply
   changes rather than dissolving. */
@media (prefers-reduced-motion: reduce) {
  .slideshow img { transition: none; }
}
