/* ── The page that opens underneath the cards ─────────────────────────────
   Restrained on purpose: the video and the cards carry the expression.
   Dark, readable, and translucent enough that the carwash keeps moving behind.
   ------------------------------------------------------------------------ */

/* Extra air under the hand, so the open module sits clear of the docked card
   rather than crowding it. Paired with the same subtraction from the inner's
   max-height, so the panel moves down without its bottom edge moving.

   On :root because the deck needs it too: cards.css centres the docked row in
   the space this opens up, and that calculation is wrong the moment the two
   disagree. One number, read by both. */
/* 44px, up from 30px. The module grew by a tenth and the artist asked for it to
   sit lower with it — "move the window a bit towards bottom to maintain the
   distance to the cards as is". The deck reads the same number and recentres in
   the band above, so the two move together by construction. */
:root { --lower: 44px; }

.panel {
  position: relative; z-index: 2;
  flex: 1 1 auto;
  margin: calc(clamp(1.4rem, 4vh, 2.6rem) + var(--lower)) auto 0;
  /* A TENTH WIDER, on the artist's word. 1080 -> 1188, and the viewport floor
     goes 92vw -> 94vw rather than the ten per cent that would have taken it past
     the screen. */
  width: min(1188px, 94vw);
  animation: rise .5s var(--ease-out) both;
}
.panel[hidden] { display: none; }

/* ── the circling glow, continued onto the module ──────────────────────────
   The same travelling ring the chosen card carries, so the card and the panel
   below it are lit by one moving edge rather than two unrelated treatments.

   DELIBERATELY SHARED, not copied: `--ring-a` and the `ringSpin` keyframes are
   declared in cards.css and reused verbatim here. Duplicating them under new
   names would let the two rings drift apart on some later edit, which is the
   one thing this is for. The stops, the mask trick and the black travelling
   gap are the card's — see the long note in cards.css for why the highlight is
   black and why the period is absurd.

   Square, where the card's is rounded: this edge is the panel's, and a radius
   here would read as a second, smaller card rather than as the module. */
.panel__inner::before {
  content: "";
  position: absolute; inset: -3px;
  padding: 2px;
  background: conic-gradient(from var(--ring-a),
    transparent 0 54%,
    rgba(216,31,38,.55) 66%,
    #ff3b40 76%,
    #000 81%,
    #ff3b40 86%,
    rgba(216,31,38,.5) 93%,
    transparent 100%);
  /* keep the fill, drop the middle — the ring is what is left */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  opacity: var(--ring-o, .9);
  filter: drop-shadow(0 0 5px rgba(216,31,38,.85));
  pointer-events: none;
  z-index: 4;
  animation: ringSpin .85ms linear infinite;
}

/* The ring is the one moving thing here, and motion is exactly what this asks
   to be spared. The edge stays, lit but still. */
@media (prefers-reduced-motion: reduce) {
  .panel__inner::before { animation: none; }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(26px); }
  to   { opacity: 1; transform: translateY(0); }
}

.panel__inner {
  /* Opaque on purpose. The video behind is ungraded now, so a translucent
     panel would change readability as the loop brightens and darkens. */
  background: #08080a;

  /* THE OPEN MODULE IS THE CHOSEN CARD'S OTHER HALF, and the border and the
     glow are what say so. The red top stroke is gone: it marked the panel as a
     thing with a lid, and a single red edge reads as a different object from
     the card rather than a continuation of it. A closed 2px white frame states
     the module as one surface, the way the card's own face is framed.

     NO GLOW. A red bloom was tried here, lifted from `.card__aura`, and it read
     as a second light source rather than as the same one: the card's aura works
     because it is small and bounded, while the same falloff around a
     1080px-wide box washes into the backdrop and softens the edge the ring is
     drawing. The travelling ring alone carries the connection to the card, and
     it does it on the edge itself instead of around it.

     The border is kept at 2px and made TRANSPARENT rather than deleted. A drawn
     white frame stated an edge harder than the card's own, and read as a box
     around the content instead of the same object continuing; the ring below is
     what describes this edge now. Keeping the 2px reserves the identical box so
     nothing reflows, and `position: relative` gives that ring something to
     anchor to. */
  position: relative;
  border: 2px solid transparent;
  box-shadow: 0 -6px 40px rgba(0,0,0,.7);
  /* EXACTLY THE OFFSET, TAKEN BACK. Pushing the panel down without this made it
     grow past the bottom of a short screen, and the page itself does not
     scroll, so that lower edge became unreachable. Subtracting the same
     `--lower` it was moved by lands the bottom precisely where it always was,
     at every viewport height — no guessing at how much room the deck takes,
     which is not a constant. */
  max-height: min(calc(68vh - var(--lower)), calc(792px - var(--lower)));
  max-width: 1300px;
  margin: 0 auto;
  display: flex; flex-direction: column;
}

.panel__bar {
  display: flex; align-items: baseline; gap: 1rem;
  padding: .85rem clamp(.9rem, 2.4vw, 1.6rem);
  border-bottom: 1px solid rgba(242,240,236,.1);
  flex: 0 0 auto;
}

.panel__title {
  margin: 0;
  font-size: .82rem;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--red);
  font-weight: 700;
}

.panel__count {
  font-size: .68rem; letter-spacing: .16em;
  color: var(--grey); margin-left: auto;
}

.panel__close {
  background: none; border: 1px solid rgba(242,240,236,.25);
  color: var(--bone); width: 30px; height: 30px; line-height: 1;
  font-size: 1.1rem; cursor: pointer; border-radius: 2px; padding: 0;
  transition: border-color .2s, color .2s;
}
.panel__close:hover, .panel__close:focus-visible {
  border-color: var(--red); color: var(--red);
}

.panel__body {
  /* ABOUT's portrait is pulled past this box's content edge on purpose, so that
     the photograph's centre lands on the panel border. Without a clip that
     overhang becomes a horizontal scrollbar on a page that has nothing to
     scroll sideways to. `clip` rather than `hidden`: it does not create a
     scroll container, so the vertical `auto` below is unaffected. Verified that
     no other section overflows horizontally, so nothing else changes. */
  overflow-x: clip;
  overflow-y: auto; overscroll-behavior: contain;
  padding: clamp(1rem, 2.6vw, 1.7rem);
  flex: 1 1 auto;
  /* No visible scrollbar. It still scrolls — by wheel, drag, touch, and by
     keyboard, since the body is focusable through its content — the track and
     thumb are simply not drawn. A red bar down the right edge was competing
     with the module's own edge for attention. */
  scrollbar-width: none;
}
.panel__body::-webkit-scrollbar { display: none; }

/* ── the project photograph ────────────────────────────────────────────────
   One image, at the head of the detail, before the project even names itself.
   `alt=""` because the caption carries what can honestly be said about it and
   the picture adds no fact the panel does not already state — an invented
   description would be worse than none.

   Capped in height and left to find its own width. What survives is a mixed
   bag — a 3:2 performance photograph, a 3.2:1 photographed wordmark, a 0.3:1
   logotype — so a fixed box would either crop some badly or letterbox them.
   Filling the width with `object-fit: contain` did the latter: the photograph
   sat left with a black band beside it the width of half the panel. Sizing to
   the image means each asset is simply whatever shape it is, and there is no
   filler to explain. */
/* ── the project photograph, as the modal's own weather ────────────────────
   It used to sit at the top of the column at 260px, a picture with the text
   beneath it. It is now the ground the text is written on: turned a quarter
   clockwise, anchored to the top right, larger than the window, and dissolving
   leftward into the panel.

   SQUARE, SO THE ROTATION IS FREE. A 90deg rotation swaps a box's width and
   height, and a rotated element inside a differently-shaped container either
   overflows it or leaves it short — you end up sizing the image in terms of its
   container's OTHER axis, which CSS cannot express. A square region has no such
   problem: it is its own rotation. `object-fit: cover` crops whatever shape the
   source happens to be into it, and every project's photo is a different shape.

   `screen`, SO BLACK IS THE PANEL. These are dark photographs on a near-black
   ground, and under `screen` a black pixel leaves the backdrop untouched while
   a highlight comes through — the picture's own shadows become the panel rather
   than sitting on it as a grey rectangle. That is what makes the edges vanish
   without a vignette drawn around them. `.modal__box` isolates so the blend
   cannot reach the page behind it.

   The mask does the fade and never reaches full strength: the left gradient is
   long and slow, and the strongest the image ever gets is .82, so text laid
   over the right-hand side stays readable rather than depending on where the
   picture happens to be bright. A second, softer gradient intersects it to take
   the top and bottom edges off, so the image meets the panel border on all
   sides instead of being cut by it. */
.pj-bg {
  position: absolute; top: -9%; right: 0;
  height: 118%; aspect-ratio: 1;
  overflow: hidden;              /* a transformed child is clipped by nothing else */
  pointer-events: none;
  mix-blend-mode: screen;
  -webkit-mask-image:
    linear-gradient(to left, rgba(0,0,0,.82) 0%, rgba(0,0,0,.52) 34%,
                             rgba(0,0,0,.2) 64%, transparent 93%),
    linear-gradient(to bottom, transparent 0%, #000 14%, #000 84%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(to left, rgba(0,0,0,.82) 0%, rgba(0,0,0,.52) 34%,
                             rgba(0,0,0,.2) 64%, transparent 93%),
    linear-gradient(to bottom, transparent 0%, #000 14%, #000 84%, transparent 100%);
  mask-composite: intersect;
  animation: pjBgIn 2.8s var(--ease-out) both;
}
.pj-bg img {
  display: block; width: 100%; height: 100%;
  object-fit: cover;
  transform: rotate(90deg);
  filter: grayscale(1) contrast(1.22) brightness(.92);
}
/* Several photographs behind one panel: the same cross-fade the tile uses, on
   the same cycle, so a project's pictures keep time with themselves whether you
   are looking at the grid or at the detail. Layers after the first are laid over
   it — the first still sizes the box. */
.pj-bg[data-shots] img + img { position: absolute; inset: 0; }
/* the same addition the tile uses, for the same reason — see `.tile__pic--morph` */
.pj-bg[data-shots]:not([data-shots="1"]) img { mix-blend-mode: plus-lighter; }
.pj-bg[data-shots="2"] img { animation: shotFade2 var(--morph, 26s) linear infinite both; }
.pj-bg[data-shots="3"] img { animation: shotFade3 var(--morph, 26s) linear infinite both; }
.pj-bg[data-shots="4"] img { animation: shotFade4 var(--morph, 26s) linear infinite both; }
.pj-bg[data-shots="5"] img { animation: shotFade5 var(--morph, 26s) linear infinite both; }
.pj-bg[data-shots="6"] img { animation: shotFade6 var(--morph, 26s) linear infinite both; }
@media (prefers-reduced-motion: reduce) {
  .pj-bg img { animation: none !important; }
  .pj-bg[data-shots] img + img { display: none; }
}
/* it arrives after the panel does, slowly, so the modal opens on its text */
@keyframes pjBgIn {
  from { opacity: 0; transform: translateX(2.5%); }
  to   { opacity: 1; transform: none; }
}

/* the credit, now at the foot of the content instead of under a picture */
.pj-credit {
  margin: 1.6rem 0 0; padding-top: .7rem;
  border-top: 1px solid rgba(242,240,236,.08);
  color: var(--grey); font-size: .68rem; letter-spacing: .04em; line-height: 1.55;
}

@media (prefers-reduced-motion: reduce) {
  .pj-bg { animation: none; }
}

/* ── press, embedded in a project or a release ─────────────────────────────
   Quieter than the PRESS section itself: this is the trace of what was written
   about the thing you are looking at, not the place the whole of it is read. */
.det__press li { padding: .5em 0; border-bottom: 1px solid rgba(242,240,236,.06); }
.dp__pub { color: var(--white); }
.dp__y { color: var(--grey); font-size: .74rem; margin-left: .5em; }
.dp__q {
  display: block; margin: .35em 0 .3em; padding-left: .8em;
  border-left: 1px solid var(--red-d);
  color: var(--bone); font-style: italic;
}
.dp__by, .dp__na { color: var(--grey); font-size: .74rem; }

/* ── ABOUT: the two drawers ────────────────────────────────────────────────
   BIO and CV. Deliberately the same vocabulary as the project archive and the
   LIVE year groups — a drawn marker that turns, small uppercase grey label, red
   when open or hovered — so the site folds things away one way rather than
   three. The values were lifted from the archive toggle that used to sit here.
   ------------------------------------------------------------------------ */
.ab + .ab { margin-top: 1.6rem; border-top: 1px solid rgba(242,240,236,.14); }
.ab__t {
  display: flex; align-items: baseline; gap: .8rem; cursor: pointer;
  padding: .95rem 0 .9rem; list-style: none;
  color: var(--grey); font-size: .65rem; letter-spacing: .2em;
  text-transform: uppercase;
}
.ab__t::-webkit-details-marker { display: none; }
.ab__t::before {
  content: ""; flex: none; width: 0; height: 0; margin-right: .1rem;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  transition: transform .18s ease;
}
.ab[open] .ab__t::before { transform: rotate(90deg); }
.ab__t:hover, .ab[open] .ab__t { color: var(--red); }
.ab__t:focus-visible { outline: 1px solid var(--red); outline-offset: 3px; }
.ab__n { color: var(--grey); letter-spacing: .1em; text-transform: none; }

/* ── the CV ────────────────────────────────────────────────────────────────
   A record, so it is set as one: a date column and a line, nothing centred and
   nothing dressed. The date is allowed to be empty — several entries carry no
   date in the source and inventing one would be worse than the gap. */
.cv { padding-bottom: .8rem; max-width: 78ch; }
.cv__s + .cv__s { margin-top: 2rem; }
.cv__h {
  margin: 0 0 .7rem; font-weight: 400;
  color: var(--grey); font-size: .62rem; letter-spacing: .2em;
  text-transform: uppercase;
  padding-bottom: .45rem; border-bottom: 1px solid rgba(242,240,236,.12);
}
.cv__n { color: var(--red-d); letter-spacing: .1em; margin-left: .3rem; }
.cv__l { list-style: none; margin: 0; padding: 0; }
.cv__i {
  display: grid;
  grid-template-columns: 11.5em 1fr;
  gap: .1rem 1.1rem;
  padding: .42em 0;
  border-bottom: 1px solid rgba(242,240,236,.05);
}
.cv__d { color: var(--grey); font-size: .78rem; letter-spacing: .04em; }
.cv__t { color: var(--bone); }

/* Narrow: the date stops being a column and becomes a line above its entry. */
@media (max-width: 620px) {
  .cv__i { grid-template-columns: 1fr; }
  .cv__d:empty { display: none; }
}

/* ── prose (ABOUT) ─────────────────────────────────────────────────────── */
/* ABOUT is the only section using `.prose`, and its portrait has to reach the
   panel's own right border — so the column fills the body rather than sitting
   centred inside it with a gap the float could never cross. The reading measure
   is now set by what the portrait leaves, which is the point of the layout. */
.prose { max-width: none; margin-inline: 0; }
.prose p { margin: 0 0 1.05em; color: var(--bone); }
.prose p:first-child::first-line { color: var(--white); }
/* Not scoped to `.prose`. The marker began in the biography and now appears in
   project summaries too; scoped, the word rendered in body colour there and the
   substitution silently did nothing visible. Wherever the archive writes the
   name as the coloured word, it is the coloured word. */
.unknown { color: var(--red); font-style: normal; }

/* ── ABOUT: the portrait ───────────────────────────────────────────────────
   A FLOAT, not a background. It sits at the top left of the prose and the
   biography wraps down its right side, then continues underneath it. Nothing
   is ever printed over the photograph, which is what lets it go to full
   strength: there is no text legibility to protect.

   HOW IT STOPS BEING A RECTANGLE. `mix-blend-mode: screen` against the panel's
   near-black `#08080a` maps the photograph's own black to nothing at all — the
   frame, the unlit half of the face and the whole left of the image are not
   faded, they are ARITHMETICALLY absent. Screen is `1-(1-a)(1-b)`, so against a
   backdrop of 0.031 a source pixel of 1.0 still resolves to 1.0: the lit face
   and hand reach FULL source strength. Darkness disappears, light does not.
   That is the entire reason this works, and why nothing here reduces opacity to
   hide the crop.

   The mask is therefore a boundary treatment ONLY. It is fully opaque across
   the middle two thirds and dissolves solely at the outer edge, where the lit
   hair and the shoulder are bright enough to reach the crop and would otherwise
   end on a straight line. It must not — and does not — dim the picture.

   THE VISUAL DESIGN IS FROZEN AT `final-subsurface-design`. This is scoped to
   the ABOUT panel by the artist's explicit request. It touches no shader, no
   carrier, no card motion and no aesthetics anywhere else — `carrier.js`,
   `subsurface.js`, `base.css` and `cards.css` are all untouched.
   ------------------------------------------------------------------------ */
.about__portrait {
  float: right;
  /* THE RENDERED IMAGE, and it is the thing that is sized — the box is derived
     from it, not the other way round.

     NO PERCENTAGES IN THIS CHAIN. A percentage resolves against the containing
     block in `width` but against the background positioning area in
     `background-size`, so the same `clamp(340px, 59%, 640px)` meant 596px in
     one and 354px in the other: the image rendered smaller than its own box and
     left a hard black edge short of the panel border. `vw` resolves against the
     viewport in both, so the two can no longer disagree. */
  --s: clamp(660px, 73.7vw, 1210px);
  /* THE BOX IS A WINDOW ONTO THE IMAGE — but only horizontally. Vertically the
     box IS the image: same height, no offset, so the top is never cut and the
     whole picture stands beside the text and scrolls with it rather than being
     fitted into the panel's viewport.

     --win    how wide the window is, as a fraction of the image. Narrower means
              a longer line of text beside it, because the right edge is pinned
              to the panel border and only the left edge can move.
     --shift  how far INTO the image the window starts. This is what slides the
              picture left under a fixed window, independently of how wide that
              window is.

     --top    where the HAIRLINE sits in the image, measured at y 0.183.

              NOTHING IS CROPPED HERE. Lifting the picture by this amount did
              align the hairline, but it also sliced off whatever faint hair
              sits above the row the measurement could detect — a cut, which is
              exactly what it looked like. The whole image is kept instead: the
              box is pulled UP by `--top` with a negative margin so the hairline
              still lands on the first line of text, and the frame above it is
              taken out by the mask rather than by a crop. Strands that fade in
              above the hairline fade in; they are no longer amputated.

     Together they set where the crop falls: the window spans .32–.70 of the
     image, so its right edge lands just past the eye between the fingers and
     leaves a pinch of it showing, with everything beyond running off the panel
     border. The measured subject starts at x 0.43, which is 29% into the box —
     that 29% is the lit room, and the mask below is what removes it. */
  --win: .38;
  --shift: .32;
  --top: .183;
  width: calc(var(--s) * var(--win));
  /* The image's FULL height. Nothing is cropped at either end; what happens at
     both edges is the mask's job. */
  height: var(--s);
  /* The top margin is the sync. The box holds the whole image, so the hairline
     sits `--top` down inside it; pulling the box up by exactly that puts the
     hairline on the first line of the biography. The frame above rides up out
     of the panel's scroll area, where it is both masked to nothing and clipped
     — it can never be seen, and it was never cut off.

     `--drop` then lets the whole thing sit lower against the text without
     disturbing that: the hairline stays tied to the first line and simply meets
     it further down the page. */
  --drop: 50px;
  margin: calc(var(--drop) - var(--top) * var(--s)) 0 .8rem 1.8rem;
  /* Cancels the panel body's own padding so the box's right edge lands on the
     panel border rather than inside it, which is what carries the bleed.

     There used to be a further 10px here for the scrollbar gutter. The bar is
     hidden now, so that gutter is gone and the same 10px became an overshoot —
     the picture crossed the border and lost the last of its crop to the clip.
     Removed with it, rather than left to rot as a magic number. */
  margin-right: calc(-1 * clamp(1rem, 2.6vw, 1.7rem));
  pointer-events: none;
  /* X slides the window `--shift` into the image. Y is ZERO — the picture is
     shown whole from its true top edge, and the hairline is brought to the
     first line of text by the negative margin above, not by cutting. No
     transform anywhere, so the float's wrap box and the visible picture stay
     the same rectangle and the text cannot disagree with what it is flowing
     around. */
  background: url("../assets/img/about/bloodymargaret.jpg") no-repeat;
  background-size: var(--s) var(--s);
  background-position: calc(-1 * var(--shift) * var(--s)) 0;
  mix-blend-mode: screen;

  /* THE BLACK CRUSH, and the rectangle lives or dies on it. The photograph's
     surround measures [5,7,6] — near-black, but not zero — and screen is
     ADDITIVE, so those few levels lifted the panel's [8,8,10] to about
     [13,15,16] across the whole square. Six levels is nothing on its own; six
     levels bounded by a straight edge is a visible rectangle.

     `contrast(1.12)` pivots on mid-grey, so everything below ~14/255 resolves to
     true black and screens to exactly the panel colour — the surround stops
     existing rather than being covered up. The same curve lifts the brightest
     pixel (207) to about 216, so the lit face and hand come out STRONGER, not
     dimmer. This deepens the darkness; it does not fade the picture. */
  filter: contrast(1.12);

  /* MASK THE ROOM, KEEP THE FACE. A vertical fade only ever treated the top and
     bottom, which left the lit room either side of the head — the window frame
     at the left, the wall behind the hair — sitting there as legible
     background. Screen cannot remove those: they are genuinely lit, not black,
     so subtracting them is the mask's job.

     The gradient is therefore centred ON THE FACE rather than on the box. The
     face sits at 87% across (computed from the crop: 59% of the image width
     divided by the .68 reveal) and just under half way down.

     TWO EDGES, NOT FOUR. A radial mask took every side and ate into the head.
     Only two edges actually need treating.

     LEFT, where the lit room sits. The stop is computed from the crop: the
     subject begins at x 0.43 of the image, so it begins `(.43 - --shift) /
     --win` into the box. Opaque a little before that and gone at the box edge,
     which removes the room without touching anything lit. Change the crop and
     the fade moves with it.

     TOP, which is now a fade rather than a crop, and it is the reason the
     picture is no longer cut there. It runs from nothing at the box's top edge
     to solid AT the hairline, so the empty frame is removed, any faint strands
     above the measured hairline fade in instead of being sliced, and the head
     arrives exactly where the first sentence does.

     BOTTOM, because the photograph does not end in darkness — the lowest row
     measures a mean of 41 and peaks at 161, so it is still lit at the very edge
     and a hard stop there reads as a cut-off rectangle. The fade carries it
     into the panel's shadow instead.

     RIGHT is left alone: it runs off the panel border, and softening a bleed
     only makes it look like a failed one.

     Top and bottom are one vertical gradient, since they are the same axis.
     Two layers intersected, so each edge is treated once and the corner where
     they meet is simply the product of the two. */
  --room: calc((.43 - var(--shift)) / var(--win) * 100%);
  --hair: calc(var(--top) * 100%);
  -webkit-mask-image:
    linear-gradient(to right, transparent 0%,
                    rgba(0,0,0,.45) calc(var(--room) * .55), #000 var(--room)),
    linear-gradient(to bottom, transparent 0%,
                    rgba(0,0,0,.5) calc(var(--hair) * .6), #000 var(--hair),
                    #000 72%, rgba(0,0,0,.5) 89%, transparent 100%);
          mask-image:
    linear-gradient(to right, transparent 0%,
                    rgba(0,0,0,.45) calc(var(--room) * .55), #000 var(--room)),
    linear-gradient(to bottom, transparent 0%,
                    rgba(0,0,0,.5) calc(var(--hair) * .6), #000 var(--hair),
                    #000 72%, rgba(0,0,0,.5) 89%, transparent 100%);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;

  /* SIMPLY PRESENT. No fade, no cycle, no animation at all — the photograph is
     there the moment ABOUT opens and does not move again. Everything temporal
     that was tried here (a failing bulb, a slow breath, lights coming up) has
     been removed rather than tuned down: the picture is part of the page, not
     an event on it. */
  opacity: 1;
}


/* The current-projects list always starts below the photograph rather than
   wrapping beside it — it is a list, and a list narrowed by a float reads as a
   caption for the picture. */
.prose__h { clear: both; }
/* The list itself is a reading measure again, not the full panel width. */
.now { max-width: 68ch; }

/* No reduced-motion rule for the portrait: it has no motion to reduce. */

/* Narrow screens: half a portrait bled off the border leaves three or four
   words per line beside it, which is worse than not wrapping at all. Stack, and
   show the WHOLE square rather than the half — the half only exists to sit on a
   border that is no longer being crossed. */
@media (max-width: 620px) {
  .about__portrait {
    float: none;
    width: min(100%, 340px);
    height: auto;
    aspect-ratio: 1;
    margin: 0 auto 1.3rem;
    background-size: cover;
    background-position: center;
    -webkit-mask-image: radial-gradient(50% 50% at 50% 46%, #000 0%, #000 58%,
                        rgba(0,0,0,.8) 74%, rgba(0,0,0,.3) 88%, transparent 100%);
            mask-image: radial-gradient(50% 50% at 50% 46%, #000 0%, #000 58%,
                        rgba(0,0,0,.8) 74%, rgba(0,0,0,.3) 88%, transparent 100%);
  }
}

/* The artist's own current-projects list: six names and nothing else. It sits
   under the description rather than inside it, because it is a list the artist
   wrote, not a sentence they wrote. */
.prose__h {
  margin: 2.2em 0 .9em; font-weight: 400; color: var(--grey);
  font-size: .65rem; letter-spacing: .2em; text-transform: uppercase;
  padding-bottom: .5rem; border-bottom: 1px solid rgba(242,240,236,.14);
}
.now { list-style: none; margin: 0; padding: 0; }
.now li { color: var(--white); padding: .34em 0; letter-spacing: .04em; }

/* ── LIVE: filter bar and year accordion ───────────────────────────────────
   Same vocabulary as the archive drop-down below: small uppercase grey labels,
   a drawn marker that turns, red on hover and when open. */
/* The bar is deliberately quiet: black ground, white type, no fill and no
   accent until something is actually switched on. It sits above the whole
   chronology, so it should read as a label rather than a control panel. */
.lv__bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem;
  padding-bottom: .85rem; margin-bottom: .2rem;
  border-bottom: 1px solid rgba(242,240,236,.14);
}
.lv__tag, .lv__search {
  font: inherit; font-size: .78rem;
  color: var(--white); background: #000;
  border: 1px solid rgba(242,240,236,.24); border-radius: 0;
  padding: .46rem .7rem;
}
.lv__tag {
  display: inline-flex; align-items: baseline; gap: .55rem;
  cursor: pointer; text-align: left;
}
.lv__tag-k {
  color: var(--grey); font-size: .62rem; letter-spacing: .2em;
  text-transform: uppercase;
}
.lv__tag-v { color: var(--white); }
.lv__tag:hover, .lv__search:hover { border-color: rgba(242,240,236,.5); }
.lv__tag:focus-visible, .lv__search:focus-visible {
  outline: none; border-color: var(--red);
}
/* the one place the accent appears: a filter is actually on */
.lv__tag.is-on { border-color: var(--red-d); }
.lv__tag.is-on .lv__tag-v { color: var(--red); }
.lv__search { flex: 0 1 20rem; min-width: 10rem; }
.lv__search::placeholder { color: var(--grey); }
.lv__n {
  margin: 0 0 0 auto; color: var(--grey);
  font-size: .65rem; letter-spacing: .14em; text-transform: uppercase;
}

/* ── LIVE: the year jump ───────────────────────────────────────────────────
   A line of years under the filter bar, in the same quiet vocabulary: grey
   until you touch one, red when you do. No boxes and no fills — nineteen
   outlined chips would read as a second control panel and compete with the
   filter, which is the control that actually changes what the page contains.
   This one only moves you.

   Tabular numerals so the years form a column-less grid rather than a ragged
   line, and a disabled year keeps its place instead of vanishing: which years
   a filter leaves empty is information about the filter. */
.lv__jump {
  display: flex; flex-wrap: wrap; gap: .15rem .75rem;
  padding: .7rem 0 .75rem;
  border-bottom: 1px solid rgba(242,240,236,.14);
  /* IT STAYS. A jump you can only reach at the top of the list is half a
     control: one use scrolls it away and the way back to it is the scrolling it
     was there to save. Sticky against the panel's own scrollport, on a solid
     ground because 619 rows pass underneath it. The filter bar above is not
     sticky — it is used once at the start of a search, not repeatedly on the way
     down. */
  position: sticky; top: 0; z-index: 2;
  background: #000;
}
/* SCROLL ANCHORING HAS TO BE OFF HERE, and this is the whole reason the jump
   works. Opening a year's drawer changes the height of content above the
   viewport, so Chrome silently adds the difference to `scrollTop` to keep what
   you were looking at where it was. That correction lands ON TOP of the jump's
   own scroll: measured 2026-08-18, a jump to 2024 asked to move −1360px and
   ended up +2078px away, off by exactly the 3438px the anchor had shifted. The
   drawer opens and the page moves — anchoring is right about every other kind of
   reflow and wrong about this one, which is a scroll the visitor asked for. */
.lv__years { overflow-anchor: none; }
.jy {
  font: inherit; font-size: .7rem; font-variant-numeric: tabular-nums;
  letter-spacing: .1em;
  color: var(--grey); background: none;
  border: 0; border-radius: 0; padding: .18rem .05rem;
  cursor: pointer;
}
.jy:hover { color: var(--red); }
.jy:focus-visible { outline: 1px solid var(--red); outline-offset: 3px; }
.jy:disabled { color: rgba(242,240,236,.18); cursor: default; }

/* ── the project picker ────────────────────────────────────────────────────
   Centred over the page, same furniture as the release/project popups. */
.fdlg {
  position: fixed; inset: 0; z-index: 70;
  display: grid; place-items: center;
  padding: clamp(.5rem, 3vh, 2.5rem) clamp(.5rem, 3vw, 2.5rem);
  background: rgba(6,6,7,.55);
}
/* must follow the rule above — see the note on .modal[hidden] */
.fdlg[hidden] { display: none; }
.fdlg__box {
  display: flex; flex-direction: column;
  width: min(560px, 100%); max-height: min(78vh, 760px);
  background: #000;
  border: 1px solid rgba(242,240,236,.2); border-top: 2px solid var(--red);
  border-radius: 3px; box-shadow: 0 30px 80px rgba(0,0,0,.72);
  padding: 1.3rem clamp(1rem, 3vw, 1.6rem) 1.1rem;
}
.fdlg__title {
  margin: 0 0 .3rem; color: var(--white); font-weight: 400;
  font-size: .7rem; letter-spacing: .24em; text-transform: uppercase;
}
.fdlg__hint { margin: 0 0 1rem; color: var(--grey); font-size: .74rem; }
/* the padding keeps the count column clear of the scrollbar thumb */
.fdlg__scroll {
  overflow-y: auto; flex: 1 1 auto; padding-right: .7rem;
  scrollbar-width: none;
}
.fdlg__scroll::-webkit-scrollbar { display: none; }
.fdlg__list { list-style: none; margin: 0; padding: 0; }
.fdlg__grp {
  margin: 1.1rem 0 .45rem; padding-top: .8rem;
  border-top: 1px solid rgba(242,240,236,.14);
  color: var(--grey); font-size: .62rem; letter-spacing: .2em;
  text-transform: uppercase;
}
.fopt {
  display: flex; align-items: baseline; gap: .7rem; width: 100%;
  background: none; border: 0; cursor: pointer; font: inherit; font-size: .82rem;
  color: var(--bone); text-align: left;
  padding: .4rem .3rem; border-bottom: 1px solid rgba(242,240,236,.055);
}
.fopt:hover { color: var(--white); background: rgba(242,240,236,.045); }
.fopt:focus-visible { outline: 1px solid var(--red); outline-offset: -1px; }
/* the X IS the state: red when chosen, a ghost of itself when not */
.fopt__x {
  flex: none; width: 1em; color: var(--red);
  opacity: .16; transition: opacity .14s ease;
}
.fopt[aria-pressed="true"] { color: var(--white); }
.fopt[aria-pressed="true"] .fopt__x { opacity: 1; }
.fopt__n { flex: 1 1 auto; overflow-wrap: anywhere; }
.fopt__c {
  flex: none; color: var(--grey); font-size: .7rem;
  font-variant-numeric: tabular-nums;
}
.fdlg__foot {
  display: flex; justify-content: flex-end; gap: .6rem;
  margin-top: 1.1rem; padding-top: .9rem;
  border-top: 1px solid rgba(242,240,236,.14);
}
.fbtn {
  font: inherit; font-size: .68rem; letter-spacing: .18em; text-transform: uppercase;
  color: var(--bone); background: #000; cursor: pointer;
  border: 1px solid rgba(242,240,236,.26); border-radius: 0;
  padding: .5rem 1.1rem;
}
.fbtn:hover { color: var(--white); border-color: rgba(242,240,236,.55); }
.fbtn:focus-visible { outline: none; border-color: var(--red); color: var(--white); }
.fbtn--go { border-color: var(--red-d); color: var(--white); }
.fbtn--go:hover { border-color: var(--red); color: var(--red); }

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

.yr { border-bottom: 1px solid rgba(242,240,236,.055); }
.yr__t {
  display: flex; align-items: baseline; gap: .8rem; cursor: pointer;
  padding: .7rem 0; list-style: none;
  color: var(--bone); font-size: .78rem; letter-spacing: .1em;
}
.yr__t::-webkit-details-marker { display: none; }
.yr__t::before {
  content: ""; flex: none; width: 0; height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  transition: transform .18s ease;
}
.yr[open] > .yr__t::before { transform: rotate(90deg); }
.yr__t:hover, .yr[open] > .yr__t { color: var(--red); }
.yr__t:focus-visible { outline: 1px solid var(--red); outline-offset: 3px; }
.yr__y { font-variant-numeric: tabular-nums; }
.yr__n {
  color: var(--grey); font-size: .65rem; letter-spacing: .14em;
  text-transform: uppercase;
}
.rows--yr { margin-bottom: .9rem; }

/* `hidden` must win over the table-row display the rows would otherwise take */
.lv [hidden] { display: none !important; }

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

/* ── record lists (LIVE) ───────────────────────────────────────────────── */
.rows { width: 100%; border-collapse: collapse; font-size: .82rem; }
.rows caption { text-align: left; color: var(--grey); font-size: .7rem;
                letter-spacing: .14em; padding-bottom: .7rem; }
.rows th { text-align: left; font-weight: 400; color: var(--grey);
           font-size: .65rem; letter-spacing: .2em; text-transform: uppercase;
           padding: 0 .8rem .5rem 0; border-bottom: 1px solid rgba(242,240,236,.14); }
.rows td { padding: .5rem .8rem .5rem 0; vertical-align: top;
           border-bottom: 1px solid rgba(242,240,236,.055); color: var(--bone); }
.rows tr:hover td { background: rgba(216,31,38,.07); color: var(--white); }
.rows .c-date { color: var(--white); white-space: nowrap; font-variant-numeric: tabular-nums; }
.rows .c-proj { color: var(--red); font-size: .72rem; letter-spacing: .07em;
                text-transform: uppercase; white-space: nowrap; }

/* ── THE ARCHIVE RANK IS GONE ──────────────────────────────────────────────
   A block of about 150 lines stood here: `.rank`, `.rank__track`, `.shard` and
   `.rank__panel` — past projects as narrow grey shards drifting left to right
   for ever, where clicking one centred it, widened it to a featured tile's
   width, drained the grey out of it and opened its detail underneath.

   It is removed because PROJECTS is one list now, on the artist's instruction:
   "get rid of archive and continue the actual chronology with all these projects
   in the same ranking system". A separate archive said the past was a different
   kind of thing; one ordered list says it is the same work, further back — and
   `PROJECT_ORDER` already ran the whole sequence, so the archive had been cutting
   it in half and drawing the halves in two different languages.

   The implementation is in the history, not reconstructed from a comment: it
   arrived in `7e6f69e` and left here. Worth knowing it existed, because the two
   ideas in it were sound and may be wanted again — a marquee driven from a rAF
   loop rather than a CSS animation, so it can be stopped mid-flight and steered;
   and a detail that opens INSIDE the page under the thing it belongs to rather
   than over the top of it. `projectBody` still exists for exactly that second
   one. */

/* ── project + release cards ───────────────────────────────────────────── */
/* THREE PER ROW, at any width the row can hold three at. It was
   `auto-fill, minmax(228px, 1fr)`, which on a wide screen laid six or seven
   narrow columns and turned the current work into a contact sheet. Three is a
   count, not a floor, so it is written as a count: `repeat(3, …)`.

   `minmax(0, 1fr)` and not `1fr`. A grid track's automatic minimum is its
   content's min-content width, so one long unbroken word — a URL in a summary,
   a project name with no spaces — can push a `1fr` column wider than its share
   and overflow the row. Zeroing the minimum makes the three columns equal
   whatever is inside them.

   Below 640px the row is one column: three tiles across a phone are three
   thumbnails, which is the layout this replaced. */
/* THE GAP DOES TWO JOBS. The artist asked for "a bit more distance between the
   tiles" and for the tiles themselves to "be more slim", and with the column
   count fixed at three those are the same lever: every rem the gap gains comes
   straight out of the columns. `1.9rem` from `.8rem` takes about 22px off each
   tile and puts it between them. */
.grid {
  display: grid; gap: 1.9rem;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* One number for the picture's height, because four rules need to agree on
     it: the photograph, the lineup box that stands in for a missing one, the
     frame a travelling picture flies across, and the archive's own override.
     RAISED TWICE, both times on the artist's word. 140/9vw/205 became
     175/11.25vw/256 for "a quarter more height", and then 215/13.8vw/315 for
     "taller images again i guess about rau" — RAU's picture is a 600x600 square
     and `cover` is already the least it can be scaled and still fill, so the
     only way to show more of a square is to make the frame less wide relative
     to itself. Every project's picture moves with it, which is the trade. */
  --pic-h: clamp(215px, 13.8vw, 315px);
}
@media (max-width: 640px) {
  .grid { gap: 1.2rem; grid-template-columns: minmax(0, 1fr); }
}

.tile {
  border: 1px solid rgba(242,240,236,.13);
  padding: .85rem .95rem;
  background: rgba(0,0,0,.35);
}
/* ── the tile's picture ────────────────────────────────────────────────────
   Full width, edge to edge, above the name — the tile's padding is cancelled
   with negative margins so the image meets the border rather than floating in a
   frame inside a frame.

   ONE COMMON HEIGHT HERE, unlike the modal. The grid is the one place the assets
   have to agree with each other: a 3:2 photograph beside a 0.3:1 logotype at
   their own proportions makes a row of tiles of wildly different heights, and
   the grid stops reading as a set. `cover` crops them all to the same box and
   `object-position: center` keeps the subject. The modal shows each asset whole;
   this shows them level.

   A HEIGHT, NOT A RATIO — this used to be `aspect-ratio: 16 / 9`, which was the
   same thing while a column was 230px wide. At three per row a column is more
   than twice that, and a ratio would have made the pictures twice as tall with
   it: the artist asked for the tiles to be "same height but broader", and a
   ratio is exactly the rule that refuses to do that. So the height is stated and
   the width is whatever the column gives.

   It is not a bare number either. `11.25vw` between two stops lets the picture
   take a little more room on a wide screen while the clamp keeps it from ever
   growing back into the ratio it replaced. The height lives on `.grid` as
   `--pic-h`; see the note there.

   `overflow: hidden`, AND IT IS NOT TIDINESS. Every animated picture on this
   page moves by `transform`, and a transform is not clipped by anything — the
   scaling and travelling images were painting straight over their neighbours in
   the row, which is what the artist saw. The frame has to be a frame. */
.tile__pic {
  margin: -.85rem -.95rem .7rem;
  border-bottom: 1px solid rgba(242,240,236,.13);
  background: #000;
  overflow: hidden;
}
.tile__pic img {
  display: block; width: 100%;
  height: var(--pic-h, 175px);
  object-fit: cover;
  /* `--focus` comes off the dossier as `photo_focus`, because where a
     photograph's subject sits is a fact about the photograph and not about the
     layout. Centre is right for most and wrong for the two whose people stand
     at the top of the frame. */
  object-position: var(--focus, center);
}

/* ── a tile that BLENDS between several photographs ────────────────────────
   The layers are stacked in the same box and dissolved into one another on one
   cycle, each offset by a share of it — the delay is computed in panel.js from
   `MORPH_S`, which must equal the duration below.

   IT IS ALWAYS BLENDING. This began as a hold-then-swap: each layer sat opaque
   for its own 1/n of the cycle and traded in a hurry at the end of it. The
   artist asked for the pictures to "transition into each other constant but
   slow", and for Abyssity's five faces to "slow blend from one to the next" —
   which is the opposite instruction. So each layer now ramps LINEARLY down to
   nothing across the slot after its own and back up across the slot before it,
   and at every instant exactly two adjacent pictures are on screen, mixed. A
   face is only fully itself for one frame; the rest of the time it is becoming
   the next one, which is the point.

   `26s` for the whole cycle. At n=2 that is a thirteen-second dissolve each way,
   which is as slow as "constant but slow" can be while still visibly moving; at
   n=5 each face owns roughly five seconds and resolves in the middle of them.
   The number has to serve both counts because there is one cycle length, and
   these are the only two projects that morph.

   ONE KEYFRAME SET PER LAYER COUNT, and they are not interchangeable — the ramp
   has to reach zero at exactly 100/n%, a different percentage for every n, and
   keyframe percentages cannot take a custom property. So the count is written on
   the container and the right set is selected by it; an undeclared count falls
   back to the two-image timing rather than to nothing.

   The delays in panel.js are `(i/n − 1) × cycle` — the extra whole cycle makes
   them NEGATIVE, so every layer is already running on the first frame. A
   positive delay would hold each layer at its 0% state (opaque) until its turn
   came round, and the tile would open showing all of them at once.

   The first layer is NOT absolutely positioned — it gives the box its height,
   the way the single-image case does. The rest are laid over it. */
/* `plus-lighter` IS THE WHOLE REASON THE BLEND LOOKS RIGHT. Two half-faded
   layers stacked the ordinary way composite as `t·top + (1−t)·b·bottom`, and
   with the triangular ramps below that comes to three quarters of full weight at
   every crossover — the tile visibly darkens each time one picture becomes the
   next, which on photographs this dark reads as a dip to black. `plus-lighter`
   adds the layers instead of painting one over the other, so opacities that sum
   to one produce exactly one image's worth of light and the dissolve is flat.
   `isolation` keeps that addition inside the picture box, and the box's own
   black is what the sum is measured against. */
.tile__pic--morph { position: relative; isolation: isolate; }
.tile__pic--morph img {
  mix-blend-mode: plus-lighter;
  animation: shotFade2 var(--morph, 26s) linear infinite both;
}
.tile__pic--morph img + img {
  position: absolute; inset: 0;
}
.tile__pic--morph[data-shots="3"] img { animation-name: shotFade3; }
.tile__pic--morph[data-shots="4"] img { animation-name: shotFade4; }
.tile__pic--morph[data-shots="5"] img { animation-name: shotFade5; }
.tile__pic--morph[data-shots="6"] img { animation-name: shotFade6; }

/* full at its own moment, gone one slot later, back over the slot before —
   a triangle, so the rate of change never varies and never stops */
@keyframes shotFade2 {
  0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; }
}
@keyframes shotFade3 {
  0% { opacity: 1; } 33.33%, 66.67% { opacity: 0; } 100% { opacity: 1; }
}
@keyframes shotFade4 {
  0% { opacity: 1; } 25%, 75% { opacity: 0; } 100% { opacity: 1; }
}
@keyframes shotFade5 {
  0% { opacity: 1; } 20%, 80% { opacity: 0; } 100% { opacity: 1; }
}
@keyframes shotFade6 {
  0% { opacity: 1; } 16.67%, 83.33% { opacity: 0; } 100% { opacity: 1; }
}
/* ── a blend that never arrives ────────────────────────────────────────────
   `pic--merge` is the ordinary blend with its ends cut off: instead of running
   0 to 1, each layer runs between .22 and .78, so neither picture is ever alone
   on screen and the tile never settles into being one of them. The artist asked
   for exactly that of the Howling Mystics polaroids — "constantly morph from one
   to another, dont stop at a stable image, constant merge" — and for a diptych
   of two people photographed in one sitting it is the right reading: the tile is
   about the pair, not about either half of it.

   The two figures were re-cropped to make it work. A merge that never resolves
   only reads as one image if the bodies land on top of one another, so the
   prints are cropped to put both heads at the same place AND at the same size —
   the right-hand figure was photographed 1.23x larger, so its square is 1.23x
   wider and both are emitted at their own scale. Crops that run past a print's
   own edge are filled with the polaroid's paper rather than with the print next
   to it.

   The pair still sums to 1, which is what keeps `plus-lighter` honest: .78 and
   .22 at every moment, so the tile's brightness never moves. */
.tile__pic--morph.pic--merge img { animation-name: mergeFade2; }
.pj-bg.pic--merge[data-shots="2"] img { animation-name: mergeFade2; }
@keyframes mergeFade2 {
  0% { opacity: .78; } 50% { opacity: .22; } 100% { opacity: .78; }
}

/* ── a blend that only ever leans ──────────────────────────────────────────
   `pic--pulse` is `merge` with a much shorter leash: .40 to .60 rather than .22
   to .78, and eased rather than linear, so the tile swings gently one way and
   then the other and is never within sight of being one picture. The artist
   asked for it of WAX — "remain in a constant mid blend just swinging in one or
   the other direction in a regular slow pulse but its always both of them, never
   arrives at a singular, dont swing too wide."

   It is the right shape for that project specifically. The two frames are a
   close portrait of one member and a wide shot of both; neither is the duo on
   its own, and a blend that resolves would keep claiming one of them is.

   `ease-in-out` on a symmetric triangle is what makes it read as a pulse rather
   than a slider being dragged — and because the easing is symmetric, the two
   layers still sum to exactly 1 at every instant, which is what `plus-lighter`
   needs. The cycle stays `--morph`, so the delay panel.js computed is still
   half of it and the layers stay exact complements. */
.tile__pic--morph.pic--pulse img,
.pj-bg.pic--pulse[data-shots="2"] img {
  animation-name: pulseFade2;
  animation-timing-function: ease-in-out;
}
@keyframes pulseFade2 {
  0% { opacity: .6; } 50% { opacity: .4; } 100% { opacity: .6; }
}

/* Reduced motion gets the first photograph and no cycle. The blend is
   decoration; the picture is the content. */
@media (prefers-reduced-motion: reduce) {
  .tile__pic--morph img { animation: none; }
  .tile__pic--morph img + img { display: none; }
}

/* ── an unbroken chain of pictures crossing the frame ──────────────────────
   `pic--shoot-ne` sends the photograph across the tile on the diagonal, bottom
   left to top right, over and over — but not as one picture making the trip.
   Declared on the dossier, not switched on a project id here, because it is a
   fact about the picture.

   IT MOVES, IT NO LONGER PANS. The first version walked the visible crop from
   one corner of a stationary photograph to the other while scaling it up; the
   artist's description was of travel, not of panning: "as if the corns are
   traveling from left bottom corner to right top corner flying through space."

   IT IS A CHAIN, NOT A PICTURE ON A LOOP. Two copies half a cycle apart came
   next, and left visible gaps: "it should look like as if it was a constant
   string of corn moving in a diagonal line from bottom left to top right. like a
   unbroken chain." So the picture is drawn EIGHT times along one diagonal, each
   copy one step behind the one in front, and the whole file slides. That is the marquee construction: when a copy runs off the end and
   restarts, the copy behind it has arrived exactly where it was, so the pattern
   on screen never changes and the loop has no seam anywhere to find.

   SCREEN, BECAUSE THE FRAMES OVERLAP EVEN THOUGH THE CORN DOES NOT. A step is
   half an image, so consecutive copies still cover one another as rectangles —
   opaque photographs would simply hide one
   another — the last one drawn would win and the chain would be a row of hard
   square edges. This photograph is corn on black: its median pixel is 1/255 and
   only its top percentile is bright, so `screen` drops the ground away entirely
   and adds the kernels together. `isolation` keeps that arithmetic inside the
   picture box instead of reaching the page behind it.

   45°, BECAUSE THE PHOTOGRAPH IS. The corn already lies on a diagonal: sampled
   on a 10x10 grid, every cell with pixels brighter than 90/255 sits on the line
   x + y = 1, from (25%, 75%) to (78%, 25%) of the frame. Travelling at 45° puts
   every copy's band on ONE straight line, so the chain is straight; any other
   angle makes it a zigzag. Change the photograph and this is wrong.

   WHERE THAT LINE FALLS IS COMPUTED FROM THE FRAME, in container units, and this
   is what was wrong on the first two attempts. A 45° line has one degree of
   freedom — how far along `x + y` it sits — and getting it wrong by a hundred
   pixels puts the whole chain through one corner, which is what "hotcorn is
   still not right" was looking at. The corn's own line sits at `x + y = s` in
   image coordinates, so the offset that lands it on the frame's centre is
   `(w + h)/2 − s` shared between the axes; with `s` at 220% of the frame height
   that comes out as `25cqw − 85cqh`. `container-type: size` on the frame is what
   makes `cqw`/`cqh` mean the tile's own width and height, so this stays true at
   any tile proportion instead of being tuned to one screen.

   THE STEP IS THE CORN'S OWN LENGTH — 51.5% of the image, which is that band's
   extent along the 45° direction — so one copy's run of kernels ENDS where the
   next one's begins. Copies used to step by a twentieth of the journey, which
   overlapped them four deep and screened their kernels into one another: "corns
   slower and not morphed, all the sizes of corn should be used". Abutting bands
   morph nothing, and each pass shows the photograph's whole sequence — the
   unpopped kernel at the bottom left through to the two burst open at the top
   right — instead of a crowd of whichever ones happened to overlap.

   That fixes the step at `114.4cqh` (51.5% of 220% of the frame height), so the
   journey is a whole number of steps and NOT a function of the frame width:
   `--chain-l` is four of them, eight steps end to end. Eight clears the frame by
   a whole image at both ends for any tile narrower than 4.75 times its own
   height, which every layout this grid produces is. 220% of the frame's height
   is the zoom, and the artist asked for more of it twice.

   LINEAR, because this site has already been bitten once by easing a long cycle:
   on the 59-97s backdrop frames an ease-in-out spends its slow tails in whole
   seconds and parks at the extremes, which reads as stuck. */
.tile__pic.pic--shoot-ne {
  position: relative; height: var(--pic-h, 175px);
  isolation: isolate;
  container-type: size;              /* so cqw/cqh below mean THIS frame */
  --chain-c: calc(25cqw - 85cqh);    /* puts the corn's line across the middle */
  --chain-l: 457.6cqh;               /* half the journey: four 114.4cqh steps */
}
.tile__pic.pic--shoot-ne img {
  position: absolute; left: 0; top: 0;
  width: auto; height: 220%; aspect-ratio: 1;
  object-fit: cover;
  mix-blend-mode: screen;
  animation: shootNE var(--travel, 42s) linear infinite both;
  will-change: transform;
}
@keyframes shootNE {
  from { transform: translate(calc(var(--chain-c) - var(--chain-l)),
                              calc(var(--chain-c) + var(--chain-l))); }
  to   { transform: translate(calc(var(--chain-c) + var(--chain-l)),
                              calc(var(--chain-c) - var(--chain-l))); }
}
/* The detail panel's backdrop gets ONE copy of a single-photograph project, so
   it cannot carry the chain — and a picture flying past behind a column of text
   would be reading matter competing with itself. It holds still, quarter-turned
   like every other backdrop. */
.pj-bg.pic--shoot-ne img { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .tile__pic.pic--shoot-ne img { animation: none; position: static;
                                 width: 100%; height: 100%;
                                 mix-blend-mode: normal; }
  .tile__pic.pic--shoot-ne img + img { display: none; }
}

/* The no-photograph case: the same 16:9 box, filled with the lineup.
   It has to hold the row's rhythm without pretending to be a picture, so it
   keeps the frame and the black ground but drops to small caps and grey —
   legibly a caption, not an image. */
.tile__pic--names {
  height: var(--pic-h, 175px);        /* the picture's height — see `--pic-h` on `.grid` */
  display: grid; place-items: center;
  padding: .6rem .9rem;
}
.tile__pic--names ul {
  list-style: none; margin: 0; padding: 0;
  text-align: center;
}
.tile__pic--names li {
  font-size: .72rem; line-height: 1.55;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--grey);
}
.tile__pic--names li + li { margin-top: .1rem; }

/* An Instagram handle inside a project's own prose. Underlined like every other
   link here and in the site's red, but it stays at the surrounding size and
   weight — it is part of a sentence the artist wrote, not a call to action. */
.ig { color: var(--red); text-decoration: underline; text-underline-offset: 2px; }
.ig:hover, .ig:focus-visible { color: var(--white); }

.tile h3 { margin: 0 0 .3rem; font-size: .86rem; letter-spacing: .06em; color: var(--white); }
.tile p  { margin: .4rem 0 0; font-size: .76rem; color: var(--grey); line-height: 1.5; }
.tile dl { margin: .5rem 0 0; font-size: .73rem; display: grid;
           grid-template-columns: auto 1fr; gap: .12rem .6rem; }
.tile dt { color: var(--grey); }
.tile dd { margin: 0; color: var(--bone); }

.badge {
  display: inline-block; font-size: .58rem; letter-spacing: .18em;
  text-transform: uppercase; padding: .16rem .45rem; border: 1px solid currentColor;
  color: var(--grey); vertical-align: middle;
}
.badge--active { color: var(--red); }

.tile--release h3 { color: var(--white); }
.tile--release .artist { color: var(--red); font-size: .74rem;
                         letter-spacing: .05em; display: block; margin-bottom: .35rem; }

/* ── releases: the cover grid ──────────────────────────────────────────────
   A uniform field of equal squares showing ONLY artwork. No metadata is printed
   on a tile and nothing is revealed on hover — the grid reads as images, and
   the information lives in the detail that expands beneath the row.

   `object-fit: cover` is deliberate here, against the usual instinct to show
   art uncropped: the grid's whole point is that every square is the same size.
   The UNCROPPED image is shown in the expanded detail, so nothing is lost.
   ------------------------------------------------------------------------ */
.rel-grid {
  display: grid;
  gap: 6px;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}

.tile-sq {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1;                 /* every tile is the same square */
  padding: 0; margin: 0; border: 0;
  background: #0b0b0d;
  cursor: pointer;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
  transition: filter .25s var(--ease-out), outline-color .2s;
  outline: 1px solid rgba(242,240,236,.08);
  outline-offset: -1px;
}
.tile-sq img {
  width: 100%; height: 100%;
  object-fit: cover;               /* uniform field of squares */
  display: block;
}

/* coverless proper release: a restrained typographic square, same geometry.
   It carries its title only because a blank square would be unusable — this is
   an identifier, not a "missing cover" message. */
.tile-sq--type {
  display: grid; place-items: center;
  padding: .7rem;
  background: #101013;
  outline-color: rgba(216,31,38,.35);
}
.tile__type {
  font-size: .74rem; line-height: 1.35; letter-spacing: .02em;
  color: var(--bone); text-align: center; overflow-wrap: anywhere;
}

@media (hover: hover) {
  .tile-sq:hover { filter: brightness(1.12); outline-color: rgba(242,240,236,.35); }
}
.tile-sq:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }

/* the selected tile stays visibly marked while its detail is open */
.tile-sq[aria-expanded="true"] {
  outline: 2px solid var(--red);
  outline-offset: -2px;
  filter: none;
}
.tile-sq[aria-expanded="true"]::after {
  content: "";
  position: absolute; inset: 0;
  box-shadow: inset 0 0 0 6px rgba(216,31,38,.18);
  pointer-events: none;
}

/* ── the release popup ─────────────────────────────────────────────────────
   A WINDOW over the site, not a takeover. The carwash keeps playing and the
   deck stays visible around it — only a light veil separates them, enough to
   push the page back without hiding it.

   The selected cover is still used twice, but the blurred copy now lives INSIDE
   the window rather than filling the viewport: each record gets its own
   atmosphere within its own frame.
   ------------------------------------------------------------------------ */
.modal {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center;
  padding: clamp(.5rem, 3vh, 2.5rem) clamp(.5rem, 3vw, 2.5rem);
  /* light enough that the carwash and the cards read through it */
  background: rgba(6,6,7,.28);
}
/* MUST come after the rule above. `display: grid` in a class selector beats the
   user-agent `[hidden] { display: none }`, so without this the closed modal
   stays laid out as a full-viewport overlay — invisible, but eating every click
   on the page. That is not theoretical: it shipped, and it made every card open
   RELEASES and then killed all interaction. */
.modal[hidden] { display: none; }

.modal__box {
  position: relative;
  width: min(1000px, 100%);
  max-height: min(78vh, 820px);
  overflow: hidden;                       /* clips the blurred cover inside */
  border: 1px solid rgba(242,240,236,.2);
  border-top: 2px solid var(--red);
  border-radius: 3px;
  box-shadow: 0 30px 80px rgba(0,0,0,.72), 0 0 0 1px rgba(0,0,0,.5);
  background: #0b0b0e;
  animation: modalIn .26s var(--ease-out) both;
  display: flex; flex-direction: column;
  /* `.pj-bg` blends with `screen`. Without a stacking context of its own the
     blend reaches past this box into the cards and the backdrop behind it. */
  isolation: isolate;
}
@keyframes modalIn {
  from { opacity: 0; transform: translateY(12px) scale(.99); }
  to   { opacity: 1; transform: none; }
}

/* the record's own atmosphere, contained by the window */
.modal__bg {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
  filter: blur(30px) saturate(1.4) brightness(.7);
  transform: scale(1.25);                 /* hide the blur's soft edges */
  pointer-events: none;
}
.modal__scrim {
  position: absolute; inset: 0;
  pointer-events: none;
  /* the reading veil: strong enough that text contrast never depends on which
     cover is behind it, light enough that the artwork still reads as colour */
  background:
    linear-gradient(180deg, rgba(9,9,12,.80), rgba(9,9,12,.90));
}

.modal__grid {
  position: relative;                     /* above bg and scrim */
  z-index: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* No visible scrollbar — the same treatment `.panel__body` and `.fdlg__scroll`
     already carry, which this container was simply never given. It still scrolls
     by wheel, drag, touch and keyboard; the track and thumb are not drawn. A bar
     down the right edge of a modal that already has its own border and close
     control is one more vertical line competing for the same edge. */
  scrollbar-width: none;
  display: grid;
  grid-template-columns: minmax(220px, 320px) 1fr;
  gap: 1.5rem;
  padding: 1.4rem 1.5rem 1.6rem;
}
.modal__grid::-webkit-scrollbar { display: none; }
.modal__left { min-width: 0; }
.modal__right { min-width: 0; }

.modal__cover {
  width: 100%; height: auto; display: block;
  object-fit: contain;                      /* uncropped in the foreground */
  border: 1px solid rgba(242,240,236,.18);
  box-shadow: 0 10px 34px rgba(0,0,0,.6);
}
.modal__cover--type {
  aspect-ratio: 1; display: grid; place-items: center;
  background: rgba(16,16,19,.9); padding: 1rem; text-align: center;
  color: var(--bone); font-size: .9rem; overflow-wrap: anywhere;
}

.modal__artist {
  margin: 0; font-size: .72rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--red);
}
.modal__title {
  margin: .25rem 0 1rem; font-size: 1.45rem; line-height: 1.15;
  letter-spacing: .01em; color: var(--white);
}

.modal__close {
  position: absolute; top: .5rem; right: .5rem; z-index: 2;
  background: rgba(6,6,7,.72); border: 1px solid rgba(242,240,236,.3);
  color: var(--bone); width: 30px; height: 30px; line-height: 1;
  font-size: 1.1rem; cursor: pointer; border-radius: 2px; padding: 0;
}
.modal__close:hover, .modal__close:focus-visible { border-color: var(--red); color: var(--red); }

/* ── player ─────────────────────────────────────────────────────────────── */
.pl { margin-top: 1rem; }
.pl__row { display: flex; align-items: center; gap: .7rem; }

.pl__play {
  flex: 0 0 auto;
  width: 52px; height: 52px; border-radius: 50%;
  background: var(--red); color: #fff; border: 0; cursor: pointer;
  font-size: 1rem; display: grid; place-items: center;
}
.pl__play:hover { filter: brightness(1.12); }
.pl__mid { flex: 1 1 auto; min-width: 0; }
.pl__now {
  margin: 0 0 .25rem; font-size: .74rem; color: var(--white);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pl__bar {
  height: 6px; background: rgba(242,240,236,.18); cursor: pointer; position: relative;
}
.pl__bar:focus-visible { outline: 2px solid var(--red); outline-offset: 3px; }
.pl__fill { position: absolute; inset: 0 auto 0 0; width: 0; background: var(--red); }
.pl__time { margin: .25rem 0 0; font-size: .68rem; color: var(--grey); }

.pl__side { display: flex; align-items: center; gap: .3rem; flex: 0 0 auto; }
.pl__btn {
  background: none; border: 1px solid rgba(242,240,236,.22); color: var(--bone);
  width: 28px; height: 28px; cursor: pointer; border-radius: 2px; padding: 0;
  font-size: .7rem;
}
.pl__btn:hover { border-color: var(--red); color: var(--red); }
.pl__vol input { width: 62px; accent-color: var(--red); }

.pl__msg { margin: .5rem 0 0; font-size: .7rem; color: var(--red); }

.pl__list { list-style: none; margin: .8rem 0 0; padding: 0; }
.pl__list button {
  display: grid; grid-template-columns: 1.4rem 1fr auto; gap: .5rem;
  width: 100%; text-align: left; background: none; cursor: pointer;
  border: 0; border-bottom: 1px solid rgba(242,240,236,.08);
  padding: .4rem .2rem; color: var(--bone); font: inherit; font-size: .74rem;
}
.pl__list button:hover { color: var(--white); background: rgba(242,240,236,.05); }
.pl__list button[aria-current="true"] { color: var(--red); }
.pl__n { color: var(--grey); }
.pl__t { overflow-wrap: anywhere; }
.pl__d { color: var(--grey); }

.pl__embed { width: 100%; height: 340px; border: 0; display: block; background: #0d0d10; }
.pl__attr { margin: .5rem 0 0; font-size: .68rem; color: var(--grey); }
.pl--empty { border: 1px dashed rgba(242,240,236,.22); padding: .9rem; }
.pl__none { margin: 0 0 .3rem; font-size: .8rem; color: var(--bone); }


/* ── shared definition/credit blocks inside the popup ───────────────────── */
.det__meta {
  margin: 0 0 .3rem; font-size: .78rem;
  display: grid; grid-template-columns: 7.4rem 1fr; gap: .22rem .9rem;
}
.det__meta dt { color: var(--grey); }
.det__meta dd { margin: 0; color: var(--bone); overflow-wrap: anywhere; }

.modal__right h4 {
  margin: 1.2rem 0 .4rem;
  font-size: .62rem; letter-spacing: .22em; text-transform: uppercase;
  color: var(--grey); font-weight: 700;
}
.det__list {
  margin: 0; padding: 0 0 0 1.1rem;
  font-size: .78rem; color: var(--bone);
}
.det__list li { margin: .14rem 0; overflow-wrap: anywhere; }
/* track rows keep the position recorded in the archive ("13.", "B5") — an <ol>
   would renumber a partial listing and silently falsify it */
.det__list--tracks { list-style: none; padding-left: 0; }
.det__links { list-style: none; padding-left: 0; }

/* ── singles / compilations / appearances — compact rows, never cover cards ── */
.group {
  margin: 2.4rem 0 .8rem;
  font-size: .66rem; letter-spacing: .24em; text-transform: uppercase;
  color: var(--grey); font-weight: 700;
  border-top: 1px solid rgba(242,240,236,.16); padding-top: 1.1rem;
}

.mini-list { list-style: none; margin: 0; padding: 0; }
.mini { padding: 0; border-bottom: 1px solid rgba(242,240,236,.07); }
.mini__open {
  width: 100%; background: none; border: 0; cursor: pointer; font: inherit;
  color: inherit; text-align: left;
  display: grid;
  grid-template-columns: 4.4rem minmax(9rem, 1.2fr) minmax(7rem, 1fr) minmax(8rem, 1.3fr);
  gap: .4rem 1rem;
  padding: .5rem .3rem;
  font-size: .76rem;
  align-items: baseline;
}
.mini__open:hover { background: rgba(242,240,236,.05); }
.mini__open:hover .mini__title { color: var(--red); }
.mini__open:focus-visible { outline: 2px solid var(--red); outline-offset: -2px; }
.mini__open[aria-expanded="true"] .mini__title { color: var(--red); }
.mini__year   { color: var(--grey); }
.mini__title  { color: var(--white); }
.mini__artist { color: var(--red); }
.mini__format { color: var(--grey); overflow-wrap: anywhere; }

/* ── contact ───────────────────────────────────────────────────────────── */
.contact { font-size: .95rem; }
.contact ul { list-style: none; padding: 0; margin: .8rem 0 0; }
.contact li { padding: .35rem 0; }
.contact .phone { color: var(--grey); font-size: .8rem; }
.contact .phone span { color: var(--bone); }

.msg { color: var(--grey); font-size: .82rem; }
.msg--error { color: var(--red); }

@media (max-width: 760px) {
  /* still a square grid on mobile — the metaphor does not degrade to a list */
  .rel-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 5px; }
  /* the popup stacks: cover, then player, then information */
  .modal { padding: .6rem; }
  .modal__box { max-height: 88vh; width: 100%; }
  .modal__grid { grid-template-columns: 1fr; gap: 1.1rem; padding: 1rem .9rem 1.6rem; }
  .modal__cover { max-width: 260px; margin: 0 auto; }
  .modal__title { font-size: 1.2rem; }
  .pl__embed { height: 300px; }
  .pl__side { display: none; }        /* prev/next/volume live in the track list on touch */
  .det__inner { grid-template-columns: 1fr; gap: .9rem; padding: 1rem; }
  .det__art { max-width: 200px; }
  .det__meta { grid-template-columns: 5.6rem 1fr; font-size: .74rem; }
  .mini { grid-template-columns: 3.6rem 1fr; }
  .mini__artist, .mini__format { grid-column: 2; }
}

@media (max-width: 700px) {
  .panel__inner { max-height: 66vh; }
  .rows { font-size: .76rem; }
  /* The Venue column drops on narrow screens. The HEADER carries the same class
     as the cells so both go together — hiding only the cells left every column
     after it sitting under the wrong heading. */
  .rows .c-venue { display: none; }
  /* Controls go full width and stack rather than squeezing a 15rem select into
     a 320px screen. */
  .lv__bar { gap: .55rem; }
  /* Nineteen years on a 386px screen is three lines; tightening the row gap
     keeps that block from pushing the first performance off the fold. */
  .lv__jump { gap: .1rem .6rem; padding: .6rem 0 .6rem; }
  .lv__search { flex: 1 1 100%; min-width: 0; }
  .lv__n { margin-left: 0; width: 100%; }
  .yr__t { padding: .62rem 0; }

  /* Dropping the Venue column is not enough on a phone: a billing line is one
     long unbroken string, and auto table layout widens the table to fit it
     rather than wrapping — measured at 656px inside a 334px panel, so the
     chronology scrolled sideways. Fixed layout makes the columns obey the
     panel and the text wrap inside them. The date column is sized to its own
     content (a date never wraps well) and everything else shares what is left. */
  .rows--yr { table-layout: fixed; width: 100%; }
  .rows--yr th, .rows--yr td { overflow-wrap: anywhere; padding-right: .5rem; }
  .rows--yr .c-date { width: 5.3rem; }

  /* The Project column goes too. Four columns in ~334px left the billing — the
     one field that says who actually played — about 100px, wrapping to two
     words a line, which is not reading. It is also the most redundant column
     here: the filter directly above asks the same question better, and on a
     phone it is the only sensible way to ask it. Date, billing and place survive.

     THE OLD REASON NO LONGER HOLDS, and is corrected rather than left standing:
     this used to say the column was "empty on 530 of the 615 entries because the
     Wix era carries no structured project". That stopped being true when the
     curated attribution layer was projected into the column — it is now filled
     on 231 of 615, not 85. The column is still dropped here, but on the space
     argument alone, which was always the real one: four columns in ~334px left
     the billing about 100px, wrapping to two words a line, which is not
     reading. */
  .rows--yr .c-proj { display: none; }
  /* Place is a city and a country code; billing is the sentence that says
     who played. Capping place stops the two sharing the width equally and
     gives the remainder to the field that needs it. */
  .rows--yr .c-place { width: 6.4rem; }
}

@media (prefers-reduced-motion: reduce) {
  .modal__box { animation: none; }
  .panel { animation: none; }
}

/* ── project detail ──────────────────────────────────────────────────────────
   A project tile that has something behind it becomes the button that opens it.
   The button carries no chrome of its own: the tile's border and padding stay
   exactly where they were, so the grid reads identically until you hover. */
.tile--open { padding: 0; }
.tile__hit {
  display: block; width: 100%; text-align: left;
  background: none; border: 0; cursor: pointer; font: inherit; color: inherit;
  padding: .85rem .95rem;
}
.tile__hit:hover { background: rgba(242,240,236,.05); }
.tile__hit:hover h3 { color: var(--red); }
.tile__hit:focus-visible { outline: 2px solid var(--red); outline-offset: -2px; }
.tile__hit[aria-expanded="true"] h3 { color: var(--red); }

/* An earlier name is provenance. It sits under the current name in the archive's
   quiet grey — present, findable, and visibly not the name in use. */
.tile__proto {
  margin: .1rem 0 0 !important;
  font-size: .66rem !important; letter-spacing: .1em; text-transform: uppercase;
  color: var(--grey) !important;
}
.tile__more {
  margin: .6rem 0 0 !important;
  font-size: .66rem !important; letter-spacing: .14em; text-transform: uppercase;
  color: var(--red) !important;
}

/* The project popup has no cover to sit beside, so it drops the two-column split
   rather than leaving an empty well where the artwork would be. */
.modal__grid--one { grid-template-columns: 1fr; max-width: 62rem; }

.det__note {
  margin: .3rem 0 .9rem; font-size: .72rem; color: var(--grey);
  line-height: 1.55; font-style: italic;
}
.rows--mini { margin: .2rem 0 .4rem; font-size: .76rem; }
.rows--mini td { padding: .32rem .7rem .32rem 0; }

/* releases listed inside a project — small covers, and each one opens the real
   release detail rather than a reduced copy of it */
.pr-list { list-style: none; margin: .2rem 0 1rem; padding: 0;
           display: grid; gap: .35rem;
           grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.pr__open {
  width: 100%; background: none; border: 0; cursor: pointer; font: inherit;
  color: inherit; text-align: left;
  display: grid; grid-template-columns: 2.6rem 1fr; gap: .6rem;
  align-items: center; padding: .3rem; 
}
.pr__open:hover { background: rgba(242,240,236,.05); }
.pr__open:hover .pr__title { color: var(--red); }
.pr__open:focus-visible { outline: 2px solid var(--red); outline-offset: -2px; }
.pr__cover { width: 2.6rem; height: 2.6rem; object-fit: cover; display: block; }
.pr__cover--type {
  display: grid; place-items: center; text-align: center;
  border: 1px solid rgba(242,240,236,.2);
  font-size: .48rem; letter-spacing: .04em; color: var(--grey);
  overflow: hidden; padding: .1rem;
}
.pr__meta { min-width: 0; display: grid; }
.pr__title { font-size: .76rem; color: var(--bone); overflow-wrap: anywhere; }
.pr__year  { font-size: .68rem; color: var(--grey); font-variant-numeric: tabular-nums; }

/* ── tours ─────────────────────────────────────────────────────────────────
   A date column and the CV's own line beside it. The date leads and is set in
   the same red the chronology uses for a project name, because it is the one
   part a reader scans; the line itself stays bone and is never abbreviated —
   these carry the countries, which is most of what a tour entry says.

   `auto 1fr` rather than a fixed column: the CV writes "2016 November/December"
   and "2014 March-June" as well as "2018 March", and a column sized for the
   short form would wrap the long ones to two lines for no reason. */
.det__list--tours { list-style: none; padding-left: 0; }
.det__list--tours li { border-bottom: 1px solid rgba(242,240,236,.06); }

/* The fold. `.tour__head` is the shared row so an undocumented tour — no dates,
   so no disclosure control — sits on exactly the same grid as one that opens,
   and the column of CV dates stays straight down the list either way. */
.tour__head {
  display: grid; grid-template-columns: auto 1fr auto; gap: .3rem .9rem;
  align-items: baseline; padding: .3rem 0;
  cursor: pointer; list-style: none;
}
.tour__head::-webkit-details-marker { display: none; }
.tour__head--flat { cursor: default; }
.tour__head:focus-visible { outline: 1px solid var(--red); outline-offset: 2px; }

.tour__date {
  color: var(--red); font-size: .68rem; letter-spacing: .06em;
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.tour__text { color: var(--bone); overflow-wrap: anywhere; }
/* the count doubles as the affordance: it is the only thing on the row that says
   there is something behind it, so it carries the drawn marker */
.tour__n {
  color: var(--grey); font-size: .66rem; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.tour__n::after {
  content: ""; display: inline-block; vertical-align: middle;
  margin-left: .4rem; width: 0; height: 0;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent; border-bottom: 3.5px solid transparent;
  transition: transform .18s ease;
}
.tour__head--flat .tour__n::after { display: none; }
.tour[open] .tour__n::after { transform: rotate(90deg); }
.tour[open] .tour__n, .tour__head:hover .tour__n { color: var(--red); }

/* A tour this project JOINED rather than owned. It says so before the dates,
   because the line above it is another act's tour and the short list under it
   would otherwise read as this project's whole run. */
.tour__joined {
  margin: .25rem 0 .1rem; padding-left: .7rem;
  border-left: 1px solid rgba(216,31,38,.35);
  font-size: .72rem; line-height: 1.5; color: var(--grey);
}

/* the tour's own dates, inset so the fold is legible as belonging to its tour */
.tour .rows--mini {
  margin: .1rem 0 .55rem;
  border-left: 1px solid rgba(216,31,38,.35);
  padding-left: .7rem;
}
/* Where it went is the column that matters here, so it gets the room and the
   date column is pinned to its own width. `c-venue` is hidden on narrow screens
   in the main chronology; this column is NOT that column and must never be. */
.rows--tour { table-layout: auto; width: 100%; }
.rows--tour .c-date { width: 1%; }
.rows--tour .c-where { color: var(--grey); overflow-wrap: anywhere; }

/* narrow: the CV date sits above its line rather than squeezing it to a ribbon */
@media (max-width: 30rem) {
  .tour__head { grid-template-columns: 1fr auto; }
  .tour__date { grid-column: 1 / -1; }
}
@media (prefers-reduced-motion: reduce) {
  .tour__n::after { transition: none; }
}

.det__list--media { list-style: none; padding-left: 0; }
.det__list--media li {
  display: grid; grid-template-columns: 1fr auto; gap: .4rem 1rem;
  align-items: baseline; padding: .22rem 0;
  border-bottom: 1px solid rgba(242,240,236,.06);
}

/* ── a recording's own lineup ──────────────────────────────────────────────
   Folded, and quiet: it belongs to the track above it, not to the section. The
   summary states the count so the fold is worth opening before you open it, and
   says "not recorded" when there are no names — the honest common case for a
   band whose personnel changed every night. */
.lineup { grid-column: 1 / -1; margin: .15rem 0 .1rem; }
.lineup__t {
  cursor: pointer; list-style: none;
  font-size: .64rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--grey);
}
.lineup__t::-webkit-details-marker { display: none; }
.lineup__t::before {
  content: ""; display: inline-block; vertical-align: middle;
  margin-right: .4rem; width: 0; height: 0;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent; border-bottom: 3.5px solid transparent;
  transition: transform .18s ease;
}
.lineup[open] .lineup__t::before { transform: rotate(90deg); }
.lineup__t:hover, .lineup[open] .lineup__t { color: var(--red); }
.lineup__t:focus-visible { outline: 1px solid var(--red); outline-offset: 2px; }
.lineup__l { margin: .3rem 0 0; padding-left: 1.1rem; font-size: .74rem; color: var(--bone); }
.lineup__n {
  margin: .3rem 0 .2rem; padding-left: .7rem;
  border-left: 1px solid rgba(242,240,236,.14);
  font-size: .7rem; line-height: 1.55; color: var(--grey);
}
@media (prefers-reduced-motion: reduce) { .lineup__t::before { transition: none; } }

/* ── B-sides / appendix / extras ─────────────────────────────────────────────
   Deliberately not cover squares. These are self-published tracks, not records,
   and the grid is reserved for things that were actually issued. */
.extras { margin: 0 0 1.6rem; }
.extras__title {
  margin: .9rem 0 .2rem; font-size: .8rem; letter-spacing: .06em; color: var(--white);
  font-weight: 400;
}
.extras__by {
  margin-left: .6rem; font-size: .64rem; letter-spacing: .16em;
  text-transform: uppercase; color: var(--red);
}
.extras__blurb {
  margin: 0 0 .7rem; font-size: .74rem; color: var(--grey); line-height: 1.55;
  max-width: 46rem;
}
.extras .mini__open { text-decoration: none; }
.extras .mini__open:hover .mini__title { color: var(--red); }

@media (max-width: 720px) {
  .modal__grid--one { padding: 1.1rem 1rem 1.3rem; }
  .pr-list { grid-template-columns: 1fr; }
}
.det__lead {
  margin: .5rem 0 1rem; font-size: .82rem; color: var(--bone);
  line-height: 1.6; max-width: 52rem;
}
/* A Wix-era row whose structured fields are all "unknown": the source's own
   line, shown whole rather than split into columns it does not actually have. */
.rows .c-raw { color: var(--bone); }

/* ── press ───────────────────────────────────────────────────────────────────
   A reading column, not a grid. These are quotations, and setting them as tiles
   would turn other people's sentences into decoration. */
.press { max-width: 62rem; display: grid; gap: 1.6rem; }
.pq { border-left: 2px solid rgba(216,31,38,.5); padding: .1rem 0 .1rem 1rem; }
.pq__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .8rem; }
.pq__pub {
  margin: 0; font-size: .82rem; letter-spacing: .05em; color: var(--white);
  font-weight: 400;
}
.pq__pub a { color: inherit; }
.pq__pub a:hover { color: var(--red); }
.pq__meta {
  margin: 0; font-size: .62rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--grey);
}
.pq__quote {
  margin: .5rem 0 .4rem; padding: 0;
  font-size: .88rem; line-height: 1.62; color: var(--bone);
  /* the publication's words, so they get the room and the site's voice recedes */
}
/* ── a tour's own poster ───────────────────────────────────────────────────
   Inside the fold, above the dates, because for the tours that have one the
   poster IS the date list — hand-written, printed and carried, where a listing
   is only a plan. Small: it sits in a column of text and opens at full size in a
   new tab, the same bargain the press scans make. */
/* Same reason the collaborator table brings its own ground: `.pj-bg` is
   brightest at the right of the panel, which is where a tour row's venue and
   city land, and grey on a lit photograph is not readable. */
.rows--tour {
  background: rgba(0,0,0,.5);
  border: 1px solid rgba(242,240,236,.08);
}
.rows--tour td { padding-left: .5rem; padding-right: .5rem; }

.tour__poster { margin: .6rem 0 .8rem; max-width: 210px; }
.tour__poster a { display: block; border: 1px solid rgba(242,240,236,.16); background: #000; }
.tour__poster a:hover, .tour__poster a:focus-visible { border-color: var(--red); }
.tour__poster img { display: block; width: 100%; height: auto; }
.tour__poster figcaption {
  margin-top: .35rem; color: var(--grey);
  font-size: .64rem; line-height: 1.5; letter-spacing: .03em;
}

/* ── the collaborator roll ─────────────────────────────────────────────────
   Fifty-nine people with an instrument and a country each. It folds, like the
   tours and the lineups do, because that is a wall of names in a panel that
   already has releases and press under it — and the count sits on the summary so
   the size of the thing is readable without opening it. */
.collab { margin: 1.1rem 0 0; }
.collab__t {
  display: flex; align-items: baseline; gap: .6rem; cursor: pointer;
  list-style: none;
  color: var(--white); font-size: .72rem; letter-spacing: .18em; text-transform: uppercase;
}
.collab__t::-webkit-details-marker { display: none; }
.collab__t::before {
  content: ""; flex: none; width: 0; height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  transition: transform .18s ease;
}
.collab[open] .collab__t::before { transform: rotate(90deg); }
.collab__t:hover { color: var(--red); }
.collab__n { color: var(--red); letter-spacing: .1em; }
/* THE TABLE SITS ON ITS OWN GROUND. `.pj-bg` is brightest at the right-hand
   edge of the panel, which is exactly where the country column lands, and a
   two-letter code in grey over a lit photograph is not readable. Everything else
   in this panel is a paragraph the mask was tuned around; a full-width table is
   not, so it brings its own backing. */
.collab__l {
  margin-top: .5rem;
  background: rgba(0,0,0,.5);
  border: 1px solid rgba(242,240,236,.08);
}
.collab__l td { padding-left: .5rem; padding-right: .5rem; }
.collab__name { color: var(--white); white-space: nowrap; }
.collab__inst { color: var(--grey); }
.collab__cc {
  color: var(--grey); text-align: right; white-space: nowrap;
  font-size: .66rem; letter-spacing: .12em;
}
@media (prefers-reduced-motion: reduce) { .collab__t::before { transition: none; } }

/* ── RELEASES: five at the front, then the whole run ───────────────────────
   The section used to be one uniform grid of squares. The artist asked for a
   different shape: five featured covers in a row, and under them the entire
   discography as a chronological list, newest first, each line led by a cover
   the size of the type — with the featured five still in it.

   FIVE ACROSS, ALWAYS. `repeat(5, …)` and not `auto-fit`: the row is five
   because the artist said five, and an unfilled slot is drawn rather than closed
   up so the gap is visible to whoever fills it. Below 640px it becomes a
   scrolling rank rather than five slivers.

   `minmax(0, 1fr)` for the same reason the project grid uses it — a track's
   automatic minimum is its content's min-content width, and a long unbroken
   title would push a column past its share. */
.feat-row {
  display: grid; gap: .5rem;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  margin: 0 0 1.8rem;
}
.feat {
  position: relative; display: block; width: 100%;
  aspect-ratio: 1;
  padding: 0; border: 1px solid rgba(242,240,236,.13); background: #000;
  cursor: pointer; overflow: hidden;
}
.feat img { display: block; width: 100%; height: 100%; object-fit: cover; }
.feat:hover, .feat:focus-visible { border-color: var(--red); }
.feat:focus-visible { outline: 2px solid var(--red); outline-offset: -2px; }
.feat--type { display: grid; place-items: center; padding: .6rem;
              color: var(--grey); font-size: .7rem; text-align: center; }

/* An empty slot: the frame, its number, and nothing else. Not a button, not in
   the tab order, and `aria-hidden` — there is nothing here to announce. */
.feat--empty {
  display: grid; place-items: center; cursor: default;
  border-style: dashed; border-color: rgba(242,240,236,.16);
}
.feat--empty .feat__n {
  color: rgba(242,240,236,.22);
  font-size: 1.5rem; letter-spacing: .1em;
}

/* ── the discography list ──────────────────────────────────────────────────
   One row per record, the cover reduced to an icon that leads the line. The
   columns are a grid rather than a table because a row is a single button and a
   table cell cannot be one. */
.rl-list { list-style: none; margin: 0; padding: 0; }
.rl + .rl { border-top: 1px solid rgba(242,240,236,.08); }
.rl__open {
  display: grid; align-items: center; gap: .7rem;
  grid-template-columns: 2rem 3.2rem minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, .9fr);
  width: 100%; padding: .42rem .2rem; text-align: left;
  background: none; border: 0; cursor: pointer; font: inherit; color: inherit;
}
.rl__open:hover { background: rgba(242,240,236,.05); }
.rl__open:hover .rl__title { color: var(--red); }
.rl__open:focus-visible { outline: 1px solid var(--red); outline-offset: -1px; }
.rl__open[aria-expanded="true"] .rl__title { color: var(--red); }
/* The frame is drawn whether or not there is a cover in it: a record whose
   artwork was never recovered keeps its place in the column instead of letting
   the line slide left, and the empty square says the picture is missing. */
.rl__cover {
  width: 2rem; height: 2rem; display: block;
  border: 1px solid rgba(242,240,236,.13); background: #000;
}
.rl__cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
.rl__year {
  color: var(--grey); font-size: .72rem; letter-spacing: .06em;
  font-variant-numeric: tabular-nums;
}
.rl__title { color: var(--white); font-size: .8rem; overflow-wrap: anywhere; }
.rl__artist { color: var(--grey); font-size: .72rem; overflow-wrap: anywhere; }
.rl__format {
  color: var(--grey); font-size: .66rem; letter-spacing: .03em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

@media (max-width: 640px) {
  .feat-row {
    display: flex; overflow-x: auto; scrollbar-width: none;
    overscroll-behavior-x: contain;
  }
  .feat-row::-webkit-scrollbar { display: none; }
  .feat { flex: 0 0 42%; }
  /* the format column is the first thing a narrow screen can do without */
  .rl__open { grid-template-columns: 1.7rem 2.8rem minmax(0, 1.4fr) minmax(0, 1fr); }
  .rl__format { display: none; }
}

/* ── a scan of the printed page ────────────────────────────────────────────
   Print press is the hardest kind for an archive to hold — it can cite an issue
   and a page and still have nothing to show. Where the page itself survives it
   is shown at a size that says "this exists and here it is", and clicking opens
   the file at its own resolution. Deliberately NOT full width: a magazine column
   at 800px is neither readable nor a thumbnail, and the list is a list. */
.pq__scan { margin: .9rem 0 0; max-width: 260px; }
.pq__scan a { display: block; border: 1px solid rgba(242,240,236,.16); background: #000; }
.pq__scan a:hover, .pq__scan a:focus-visible { border-color: var(--red); }
.pq__scan img { display: block; width: 100%; height: auto; }
.pq__scan figcaption {
  margin-top: .4rem; color: var(--grey);
  font-size: .66rem; line-height: 1.5; letter-spacing: .03em;
}

.pq__by { margin: 0; font-size: .72rem; display: flex; flex-wrap: wrap; gap: .2rem .8rem; }
.pq__author { color: var(--red); }
.pq__about  { color: var(--grey); }
.pq__note {
  margin: .35rem 0 0; font-size: .7rem; color: var(--grey); font-style: italic;
}
