/* site.css — Vanessa Spiral */

/* Switzer is dropped into assets/fonts/ by hand; the stack below covers its absence. */
@font-face {
  font-family: "Switzer";
  src: url("../fonts/Switzer-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Switzer";
  src: url("../fonts/Switzer-VariableItalic.woff2") format("woff2");
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
}

:root {
  --field: #FAF9F6;
  --aperture: #FFFFFF;
  --ink: #000000;
  --hairline: 0.5px solid var(--ink);
  --footer-h: 150px; /* the horizon's height; main's bottom padding matches it */
  --page-gap: 0.6vw; /* the leaf gap between the two pages of a Libra spread */
  --plaque-units: 6; /* plaque width as a whole-number multiple of a colophon column */
  /* the Swiss grid: the horizon text's bottom padding is one half-gutter; the
     full gutter between rectangles is twice that */
  --half-gutter: 1.25rem;
  --gutter: calc(2 * var(--half-gutter));
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  /* no elastic overscroll: the hero can't bounce away from the fixed horizon */
  overscroll-behavior-y: none;
}

body {
  background: var(--field);
  color: var(--ink);
  font-family: "Switzer", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 17px;
  line-height: 1.55;
}

main {
  padding-bottom: var(--footer-h);
}

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

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}

/* ---- the moon ---------------------------------------------------- */

#moon {
  position: fixed;
  top: 15vh;
  right: 15vw;
  width: clamp(56px, 5vw, 88px);
  aspect-ratio: 1;
  border-radius: 50%; /* the one permitted radius */
  background: var(--aperture);
  pointer-events: none;
  z-index: 100;
}

/* ---- the horizon ------------------------------------------------- */

/* Anchored to the viewport, not to the document: a ground line that is always
   there is the point of it. The cost is that browser zoom, which shrinks the
   viewport it is anchored to, walks it up the page — that is inherent to a
   fixed element and not a defect to be fixed here. */
#horizon {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 50;
  /* height tightens to content + symmetric half-gutters; site.js measures it
     and publishes the result as --footer-h for main's padding and the hero */
  background: var(--field);
  border-top: var(--hairline);
  display: flex;
  align-items: flex-end; /* the colophon sits on the ground, not up by the hairline */
  gap: var(--gutter); /* the full gutter between rectangles */
  padding: var(--half-gutter); /* equal half-gutter clearance on all four sides */
  font-size: 12px;
  line-height: 1.5;
}

#horizon p {
  margin: 0;
}

/* black is the default for everything; hover underline on anything clickable;
   the dimmed state exists only inside the project column and marks the
   current project. The index dims nothing. */
#horizon a {
  display: block;
  color: var(--ink);
  text-decoration: none;
}

#horizon a:hover {
  text-decoration: underline;
}

/* amends the scope note above: the contact column marks the current page too,
   now that About is a page of the site and sits in it */
#horizon .projects a[aria-current="page"],
#horizon .contact a[aria-current="page"] {
  color: #888;
  pointer-events: none;
}

/* rationalised colophon: name, project links, and contact are equal-width
   columns; the plaque is a whole-number multiple of that unit. Flex ratios, so
   the row still fills the width and the contact stays at the right edge. */
#colophon > div:first-child,
#colophon .projects,
#colophon .contact {
  flex: 1 1 0;
  min-width: 0;
}

#plaque {
  flex: var(--plaque-units, 2) 1 0;
  min-width: 0;
}

/* an empty leading unit, so the grid runs 9 units: empty, name, projects,
   plaque (5), contact */
#horizon::before {
  content: "";
  flex: 1 1 0;
  min-width: 0;
}

/* Mobile scaffolding, inert on the wide layout: the collapsed bar is hidden
   and the colophon wrapper dissolves, so #horizon's flex row is unchanged
   above the breakpoint. */
.bar {
  display: none;
}

#colophon {
  display: contents;
}

/* ---- the narrow horizon: a line, with a menu ---------------------
   At 900 rather than the 720 this used to sit at, because the colophon's four
   columns want 830px between them — 630 of type, four 40px gutters, two 20px
   margins — and below that they crush: the name breaks over two lines, the
   role over three, and near 730 the contact column runs off the edge. The one
   number is also the one the monograph already reflows at, so the site has a
   single wide/narrow threshold instead of two that disagree. Zooming in now
   collapses the horizon to a 52px band rather than growing it. */
@media (max-width: 900px) {
  :root {
    --footer-h: 52px; /* collapsed band height; also main's bottom padding */
  }

  /* the collapsed band is a single row holding the bar */
  #horizon {
    height: var(--footer-h);
    padding: 0;
    display: flex;
    align-items: stretch;
    gap: 0;
  }

  #horizon::before {
    display: none; /* no grid units on the phone bar */
  }

  #horizon.is-expanded {
    border-top-color: transparent; /* single hairline: the panel's top edge */
  }

  .bar {
    display: flex;
    flex: 1;
    align-items: center;
    gap: 1rem;
    padding: 0 4vw;
    position: relative; /* opaque, above the colophon, so the panel slides under it */
    z-index: 1;
    background: var(--field);
  }

  .bar-home {
    flex: 0 0 auto;
  }

  .bar-here {
    flex: 1 1 auto;
    min-width: 0;
    color: #888;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis; /* truncate rather than wrap */
  }

  .menu-toggle {
    flex: 0 0 auto;
    appearance: none;
    background: none;
    border: 0;
    margin: 0;
    padding: 0;
    font: inherit;
    color: var(--ink);
    cursor: pointer;
  }

  .menu-toggle:hover {
    text-decoration: underline;
  }

  /* the panel: the four blocks, two columns, tight rows, above the bar. Fixed
     like the band it belongs to, so shut it can sit clear below the viewport
     without lengthening the page — an absolute box hanging below its container
     would. */
  #colophon {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 24px 3rem;
    position: fixed;
    left: 0;
    right: 0;
    bottom: var(--footer-h);
    padding: 1.25rem 4vw;
    background: var(--field);
    border-top: var(--hairline);
    transform: translateY(calc(100% + var(--footer-h)));
    transition: transform 150ms ease, visibility 150ms;
    visibility: hidden;
  }

  #horizon.is-expanded #colophon {
    transform: translateY(0);
    visibility: visible;
  }
}

/* ---- the body column --------------------------------------------- */

.col {
  max-width: 680px;
  margin-left: auto;
  margin-right: auto;
  padding-left: 24px;
  padding-right: 24px;
}

figure {
  margin: 0 0 4rem;
}

figure.hero {
  height: 100vh;
  height: 100svh;
}

/* the photographic index hero: the whole frame plus the footer band both sit
   on screen at load. Scoped to :has(picture) so step-01 placeholder heroes
   on other pages are untouched. */
figure.hero:has(picture) {
  height: calc(100svh - var(--footer-h));
  width: 100vw;
  margin: 0; /* no figure rhythm here: frame + footer fill the viewport exactly */
  margin-left: calc(50% - 50vw); /* full-bleed, independent of any column */
}

.hero picture,
.hero img {
  display: block;
  width: 100%;
  height: 100%;
}

.hero img {
  object-fit: cover;
  /* client-tunable crop: keep the water horizon low and open sky upper-right */
  object-position: var(--hero-focus, 50% 60%);
}

.placeholder {
  width: 100%;
  aspect-ratio: 4 / 3;
  background: #E9E7E2; /* placeholder tone; goes away with the real assets */
}

.hero .placeholder {
  height: 100%;
  aspect-ratio: auto;
}

figcaption {
  font-size: 13px;
  line-height: 1.5;
  margin-top: 0.5rem;
}

img {
  display: block;
  max-width: 100%;
}

/* full-bleed breakout for images that should exceed the column */
.bleed {
  width: 100vw;
  margin-left: calc(50% - 50vw);
}

/* ---- the Basalt Libra spread carousel ---------------------------- */

.carousel {
  /* client-tunable: spread box aspect and the height cap for short viewports.
     The cap is generous so width-alignment to the project links wins at laptop
     heights; it only binds on genuinely short viewports. */
  --spread-ratio: 1.5;
  --spread-h: 75svh;
  --peek: 3vw; /* how much each neighbour shows — the leafing cue */
  --gap: 2vw; /* the space between the central spread and the cut-off neighbours */
  /* The whole carousel — cut-off neighbours included — spans from the "Vanessa
     Spiral" guide, centred on the page. --page-w / --col-name are measured live
     in site.js; the vw fallback approximates them before JS runs or if it's off. */
  --track-w: calc(var(--page-w, 100vw) - 2 * var(--col-name, 14vw));
  /* the spread is what's left once the two peeking neighbours and their gaps
     are taken out of the track; height-capped for short viewports */
  --slide-w: min(
    calc(var(--track-w) - 2 * var(--peek) - 2 * var(--gap)),
    calc(var(--spread-h) * var(--spread-ratio))
  );
  margin-top: 2rem; /* small top margin, no hero above */
}

.track {
  position: relative;
  display: flex;
  gap: var(--gap);
  width: var(--track-w);
  margin-inline: auto;
  padding-inline: calc(50% - var(--slide-w) / 2); /* lets first/last centre */
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none; /* the leafing is the affordance, not a bar */
}

.track::-webkit-scrollbar {
  display: none;
}

.slide {
  flex: 0 0 var(--slide-w);
  scroll-snap-align: center;
}

.spread {
  display: flex;
  gap: var(--page-gap); /* the two pages of a spread, with a leaf gap between */
  width: 100%;
  aspect-ratio: var(--spread-ratio);
  padding: 0;
  border: 0;
  background: none; /* absence beside a recto/verso is just field */
  cursor: pointer;
}

.spread .page {
  flex: 1 1 0; /* two pages share the box equally */
  min-width: 0;
  height: 100%;
}

.spread .page img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* single-page slides: one page on one half, plain field on the other */
.spread--recto {
  justify-content: flex-end;
}

.spread--verso {
  justify-content: flex-start;
}

.spread--recto .page,
.spread--verso .page {
  flex: 0 0 50%;
}

.controls {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 1.5rem;
  margin-top: 1rem;
  font-size: 12px; /* footer-size */
}

.controls button {
  appearance: none;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
}

.controls .is-hidden {
  visibility: hidden; /* hide-disabled at the ends; keep the counter centred */
}

/* pointer focus leaves no box; keyboard focus (:focus-visible) still rings */
.spread:focus:not(:focus-visible),
.controls button:focus:not(:focus-visible),
.lightbox__close:focus:not(:focus-visible),
.lightbox:focus {
  outline: none;
}

.mono {
  margin-top: 2rem;
  margin-inline: auto; /* centred on the page ... */
  /* ... with its left edge on the plaque guide (measured live in site.js;
     the vw fallback approximates it before JS runs or if it's off) */
  max-width: calc(var(--page-w, 100vw) - 2 * var(--col-plaque, 29vw));
  padding-inline: 0;
  color: #000000;
  font-size: 12px;
  text-justify: inter-character;
}

/* narrow: the colophon collapses to a bar, so its column guides don't apply —
   the carousel and monograph take their own full-width layout instead */
@media (max-width: 900px) {
  .carousel {
    --track-w: 94vw;
    --slide-w: min(84vw, calc(var(--spread-h) * var(--spread-ratio)));
  }

  .mono {
    max-width: none;
    margin-inline: 4vw;
  }

  /* the colophon's guides are gone on the phone bar, so the census takes its
     own full-width layout; the name column shrinks to its longest name rather
     than to a guide that is no longer published */
  .census,
  .studies {
    max-width: none;
    margin-inline: 4vw;
  }

  .census dl {
    grid-template-columns: max-content 1fr;
    column-gap: 1rem;
  }
}

/* ---- the lightbox ------------------------------------------------ */

.lightbox {
  width: 100vw;
  height: 100dvh; /* the actual current viewport, so the stage never overflows */
  max-width: 100vw;
  max-height: 100dvh;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--field);
}

.lightbox::backdrop {
  background: var(--field); /* full opacity, no blur */
}

.lightbox__stage {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--page-gap); /* the spread's two pages sit together, same leaf gap */
}

.lightbox__stage picture {
  display: block;
}

/* width/height auto so each element is exactly its fitted image — no letterbox
   padding inside it — so every point off the image dismisses. Viewport-unit
   caps keep the whole spread within the viewport less a small margin. */
.lightbox__stage img {
  display: block;
  width: auto;
  height: auto;
  max-height: 92dvh;
}

.lightbox__stage.lb--single img {
  max-width: 92vw;
}

.lightbox__stage.lb--pair img {
  max-width: 46vw; /* two pages + the gap fit across the viewport */
}

.lightbox__close {
  position: absolute;
  top: 4vmin;
  right: 4vw;
  appearance: none;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12px; /* footer-size */
  color: var(--ink);
  cursor: pointer;
}

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

/* ---- the Field ring plate ---------------------------------------- */

.ring-wrap {
  /* full-width plate: the square runs edge to edge, so it stands taller than the
     viewport and the page scrolls to the monograph, census, and footer below */
  width: 100%;
  margin: 0 auto;
  position: relative;
}

.ring {
  margin: 0;
  width: 100%;
  aspect-ratio: 1; /* the master is square */
}

.ring picture,
.ring img {
  display: block;
  width: 100%;
  height: 100%;
}

/* the centre monograph sits in the ring's void and scales with the plate */
.ring__mono {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* centred on the axis, with its left edge on the plaque guide */
  width: calc(var(--page-w, 100vw) - 2 * var(--col-plaque, 29vw));
  font-size: 12px; /* footer-size */
  line-height: 1.5;
  color: var(--ink);
  text-align: left; /* ragged right */
}

.ring__mono p {
  margin: 0 0 0.75em;
}

.ring__credit {
  margin-bottom: 0;
}

/* the census: caption-size prose lines on the centre axis, list-free */
.census {
  /* centred on the axis, with its left edge on the project-links guide */
  max-width: calc(var(--page-w, 100vw) - 2 * var(--col-projects, calc(20vw + 60px)));
  margin: 3rem auto 0;
  padding-inline: 0;
  font-size: 10px; /* caption-size */
  line-height: 1.6;
}

/* Two columns, landed on the horizon's guides. The census box already begins on
   the project-links guide, so the name column starts at its own left edge; the
   attribution column is offset to the plaque guide — the width of the first
   column is exactly the distance between the two guides. site.js publishes both;
   the vw fallbacks approximate them before it runs. */
.census dl {
  display: grid;
  grid-template-columns:
    calc(var(--col-plaque, 40vw) - var(--col-projects, calc(20vw + 60px))) 1fr;
  gap: 0.6rem 0;
  margin: 0;
}

.census dt {
  grid-column: 1;
}

.census dd {
  grid-column: 2;
  margin: 0;
}

.census em {
  font-style: italic;
}

/* ---- the element anatomy (Field) --------------------------------------
   Artwork with its own physics, boxed inside this figure: the site's chrome
   rules (no shadow, no radius) deliberately do not apply inside .anatomy. */

.anatomy {
  max-width: 720px;
  /* the site's own figure rhythm below as well as above: with the caption gone
     the picker otherwise ends flush against the horizon's hairline */
  margin: 4rem auto;
  padding-inline: 24px;
}

.anatomy__stage {
  position: relative;
  aspect-ratio: 5 / 4;
}

/* One per element, so a pick can dissolve: the outgoing stack and the incoming
   one are both in the stage for the length of the fade. The perspective lives
   here rather than on the stage because the fade does — an opacity below 1
   makes an element a grouping element, which forces its own transform-style
   back to flat, and putting it any deeper would collapse the scene out of
   perspective for as long as the fade ran. Outside the 3D context it is free. */
.anatomy__view {
  position: absolute;
  inset: 0;
  perspective: 2000px; /* the reference's m34 = −1/2000 */
  transition: opacity 240ms linear;
}

.anatomy__view.is-out {
  opacity: 0;
}

/* The ground: a flat plate a few percent under field white, covering the whole
   viewer. It is a property of the viewer, not of the scene — no perspective, no
   feathered edge, nothing that travels with a sheet. Diegetic to the figure;
   the page field outside it stays flat. Zeroable from the script's STAGE_WASH. */
.anatomy__stage::before {
  content: "";
  position: absolute;
  inset: 0;
  background: #000;
  opacity: var(--stage-wash, 0);
  pointer-events: none;
}

/* The scene faces 30° left and is seen a little from above; it never moves
   again. pointer-events: none because the sheets travel well outside the
   stage's own box, and an invisible corner of a peeled sheet lying over the
   picker below would swallow the clicks — this component takes no pointer
   input of any kind, so nothing is lost by making it untouchable. */
.anatomy__stack {
  position: absolute;
  inset: 0;
  pointer-events: none;
  transform-style: preserve-3d;
  transform:
    translate(var(--scene-x, 0), var(--scene-y, 0))
    rotateX(var(--scene-tilt, 12deg))
    rotateY(var(--scene-rot, -30deg));
}

/* Centred by offset rather than by grid: an element taller than the stage is a
   grid item larger than its track, which Chrome start-aligns instead of
   centring, and the portrait stacks then hang out of the bottom. Scaled in all
   three axes, so the lift foreshortens in proportion to the drawing. */
.anatomy__sheet {
  position: absolute;
  left: 50%;
  top: 50%;
  transform-style: preserve-3d;
  transform: translate(-50%, -50%) scale3d(var(--scale, 1), var(--scale, 1), var(--scale, 1));
}

/* The stratum is the whole frame box; .mask carries the window opening for the
   layers inside the file's mask chain; .shape is the drawn surface at the
   layer's own transcribed rect. */
/* --z is the sheet's altitude above the platter, driven from the clock. The
   stratum carries the transform and nothing else: Safari drops a transformed
   element out of its parent's 3D context as soon as it also carries a filter,
   and the whole scene then renders turned but perfectly flat. */
.stratum {
  position: absolute;
  inset: 0;
  transform: translate3d(0, 0, calc(var(--z, 0) * 1px));
}

/* A stratum that stays square to the camera while the scene around it turns:
   it undoes the two scene rotations, in the reverse order the scene applies
   them, so its own frame comes out with no net rotation at all. The 3D object's
   bitmap wants this — it is a photograph of a thing in the round, not a sheet
   of the stack, and turning it only foreshortened a picture that was never flat.
   The lift is untouched: translate3d runs first, so the sheet still rises along
   the scene's own z, and the counter-rotation is about the stratum's centre —
   which is the frame's, which is the bubble's — so nothing moves off centre.

   --float holds it clear of the planes it is no longer parallel to; without it
   the two intersect through the frame's centre and the bubble paints its own
   material over half the bitmap. See faceOnFloat() for how far. It comes last,
   after the counter-rotation, so it runs along the line of sight rather than
   along the scene's z — added to --z instead it slid the sheet left by half its
   own length, the scene being turned thirty degrees, and walked the bowl out of
   the bubble. */
.stratum.is-face-on {
  transform:
    translate3d(0, 0, calc(var(--z, 0) * 1px))
    rotateY(calc(-1 * var(--scene-rot, -30deg)))
    rotateX(calc(-1 * var(--scene-tilt, 12deg)))
    translateZ(calc(var(--float, 0) * 1px));
}

/* A sheet's shadow, thrown on the ground rather than on the sheet below it: it
   sits at z 0 wearing the silhouette of the sheet that owns it, and every cast
   is laid in before any stratum, so nothing above ever darkens what is below.
   box-shadow, not a drop-shadow filter — a filter's output is clipped to its
   filter region, which is what cut the old shadows off along a straight edge. */
.anatomy__sheet .cast {
  position: absolute;
  transform: translate3d(0, 0, 0);
  box-shadow: 0 calc(var(--cy, 4) * 1px) calc(var(--cb, 5) * 1px)
    rgba(0, 0, 0, var(--ca, 0));
  pointer-events: none;
}

.stratum .mask {
  position: absolute;
  inset: 0;
}

.stratum .shape {
  position: absolute;
}

/* The rim light, one fixed source. Not a sheet of its own — every sheet picks
   it up on its own edge, which is also what makes the separated planes legible
   once they are apart. */
.stratum .shape::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1.5px;
  background: conic-gradient(
    from 135deg,
    rgba(255, 255, 255, 0) 0deg,
    rgba(255, 255, 255, 0.8) 40deg,
    rgba(255, 255, 255, 0) 130deg,
    rgba(255, 255, 255, 0) 360deg
  );
  -webkit-mask: linear-gradient(#000, #000) padding-box, linear-gradient(#000, #000);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000, #000) padding-box, linear-gradient(#000, #000);
  mask-composite: exclude;
}

/* the print's silhouette is the travelling crop, not this box, so it takes its
   edge from the artwork instead */
.stratum--content .shape::after {
  display: none;
}

/* the print sits cropped by the opening when composed, and the crop opens out
   to the component's own footprint as the stack comes apart — the bleed from
   under the mat is the exhibit's key beat. JS interpolates the path. */
.stratum--content .mask {
  clip-path: var(--crop);
}

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

/* The document card's pane carries its own diffusion rather than sampling one:
   a blurred picture of the card it filters, clipped to the pane and travelling
   with it. See bakeBloom() in anatomy.js for why it is baked. */
.stratum .shape.has-bloom {
  overflow: hidden;
}

.stratum .bloom,
.stratum .tint {
  position: absolute;
  display: block;
}

/* the page inside the bloom: its content box is the page, its border is that
   page's own edge pixels stretched outward, which is how a background blur
   treats what lies past the element it filters */
.stratum .bloom .page {
  position: absolute;
  box-sizing: content-box;
  border-style: solid;
  border-color: transparent;
}

.stratum .tint {
  inset: 0;
}

/* The thick frosted glass: the mat on the three matted elements. It admits
   light and scatters it — a heavy blur of what is behind rather than a milky
   film over it, which is the difference between glass and tracing paper.
   Nothing in this stratum's ancestry may carry a filter or a clip-path: either
   makes it a backdrop root, and the frost is then diffusing an empty backdrop. */
.stratum .shape.is-frost {
  background: rgba(255, 255, 255, 0.55);
  backdrop-filter: blur(44px) saturate(1.9) brightness(1.06);
  -webkit-backdrop-filter: blur(44px) saturate(1.9) brightness(1.06);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .stratum .shape.is-frost {
    background: rgba(255, 255, 255, 0.94); /* poorer, structurally intact */
  }
}

/* the title pane is opaque — the file's Regular material, resolved */
.stratum--title .shape {
  background: var(--pane-fill, #F2F2F7);
}

/* the cards are OS artefacts, so their labels keep the system face rather than
   the site's Switzer — the register is the platform's, not the page's */
.stratum--title .line {
  position: absolute;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  color: #000;
  white-space: nowrap;
  letter-spacing: -0.01em;
}

.stratum--control .shape,
.stratum--icon .shape {
  display: grid;
  place-items: center;
}

.stratum--control svg {
  display: block;
  width: 100%;
  height: 100%;
}

.stratum--icon svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}


/* a row of tiles, each in its element's own proportion; --tile is the file's
   48pt module */
.anatomy__picker {
  --tile: 30px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.75rem 1rem;
  margin-top: 1.5rem;
  margin-bottom: 1.5rem;
}

.anatomy__picker button {
  appearance: none;
  display: block;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
}

/* each tile keeps the file's own proportion; --tile scales the whole row */
.anatomy__picker svg {
  display: block;
  max-width: none;
  overflow: visible;
}

/* dimmed marks where you are, per the site's current-state convention */
.anatomy__picker button.is-current {
  opacity: 0.32;
}

.anatomy__picker button:focus:not(:focus-visible) {
  outline: none;
}

/* The no-script stills: the eight stacks at rest, as the exhibit itself draws
   them, one at a time. Nothing here is scripted — scroll snapping is CSS, and a
   focused scroll container takes the arrow keys on its own, so the fallback
   keeps the live component's own manner of showing one element at a time. It is
   the spread carousel's construction exactly, at the plate's scale: the site has
   one carousel and this is it again, not a second one. */
.anatomy__stills {
  /* The slide is a fraction of the track and the end margins are what is left
     of it, halved — so the first and last can sit centred and each neighbour
     peeks by the remainder. Sized this way, and not from the container's own
     padding as the spread carousel is, because there the track's width is an
     explicit length: derive a slide from 100% and then pad the track by it and
     the two definitions chase each other.

     A neighbour shows (1 − slide)/2 − gap of the track, which is a quarter of
     its own width when slide ≈ 0.62 — the peek the client asked for. The gap is
     a percentage rather than a fixed 24px so that quarter holds at every width;
     a fixed gap would eat a bigger share of a narrow track and the peek would
     shrink on phones. */
  --slide: 62%;
  --gap: 3%;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: var(--gap);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none; /* the leafing is the affordance, not a bar */
}

.anatomy__stills::-webkit-scrollbar {
  display: none;
}

.anatomy__stills > li {
  flex: 0 0 var(--slide);
  scroll-snap-align: center;
}

.anatomy__stills > li:first-child {
  margin-inline-start: calc((100% - var(--slide)) / 2);
}

.anatomy__stills > li:last-child {
  margin-inline-end: calc((100% - var(--slide)) / 2);
}

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

/* named in the plate's own register, not the page's; centred under the slide,
   since the slide is centred in the track */
.anatomy__stills span {
  display: block;
  margin-top: 0.5rem;
  font-size: 12px;
  line-height: 1.5;
  text-align: center;
}

/* below the breakpoint the monograph leaves the void and reflows below the
   plate — the same element, moved by CSS — and the plate widens to the edge */
@media (max-width: 900px) {
  .ring__mono {
    position: static;
    transform: none;
    width: auto;
    max-width: 640px;
    margin: 1.5rem auto 0;
    padding-inline: 24px;
  }
}

/* ---- the Estradiol holofoil seal (hero) ---------------------------------
   Artwork with its own physics, like the Field anatomy: a live WebGL foil
   whose sweep IS the response, so the studio card's 3D tilt and drop-shadow
   are dropped and the site's flat, shadowless chrome is kept. The shader
   itself (assets/js/estradiol.js) is byte-identical to the settled component. */

.seal {
  display: flex;
  flex-direction: column;
  align-items: center; /* centred on the axis */
  margin: 2rem 0 4rem; /* small top entry, the site's figure rhythm below */
}

.seal__stage {
  position: relative;
  /* a seal at generous scale, not a full-bleed showpiece */
  width: min(56vh, 72vw, 640px);
  aspect-ratio: 1;
  touch-action: manipulation; /* the page still scrolls over the seal on touch */
  isolation: isolate; /* scope the sheen's soft-light blend to the stage */
}

.seal__canvas,
.seal__fallback {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.seal__fallback {
  object-fit: cover; /* the flat print, shown only if WebGL is unavailable */
}

/* the display:block above outranks the `hidden` attribute, so restore it */
.seal__fallback[hidden] {
  display: none;
}

/* the polyolefin laminate's satin sheen (06C) — a soft-light layer over the canvas,
   sized to the whole pouch front (the BACKING outline) so the backing catches it
   too, its highlight clipped by border-radius (a vector clip: no raster mask, no
   Safari edge speckle). estradiol.js slides --sheen-x/y counter to the tilt T; the
   peak and ellipse size are constant, so the laminate shines even at rest. Inset +
   radius are the backing rect (x3 y1 600² rx5 in a 606 frame — the tight corner is
   the design); at T = 0 (also reduced-motion / no-JS defaults) it rests upper-left. */
.seal__sheen {
  position: absolute;
  top: 0.165%;   /* backing inset: 1/606 */
  left: 0.495%;  /*                3/606 */
  right: 0.495%; /*                3/606 */
  bottom: 0.825%; /*               5/606 */
  border-radius: 0.83%; /* backing rx 5 / backing side 600 */
  z-index: 1;           /* over the canvas */
  pointer-events: none;
  mix-blend-mode: soft-light;
  background: radial-gradient(
    var(--sheen-r, 80%) var(--sheen-r, 80%)
      at var(--sheen-x, 42%) var(--sheen-y, 42%),
    rgba(255, 255, 255, var(--sheen-peak, 0.1)) 0%,
    rgba(255, 255, 255, 0) 60%
  );
}

/* the accelerometer opt-in, in census register, centred under the seal.
   Hidden until estradiol.js decides a touch device can offer it. */
.seal__motion {
  appearance: none;
  margin: 1rem 0 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 10px; /* census register */
  line-height: 1.6;
  color: var(--ink);
  cursor: pointer;
}

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

.seal__motion:disabled {
  color: #888; /* engaged — the site's current-state dim */
  cursor: default;
}

/* the no-script still (the printed pouch base), in the same slot and scale.
   contain, not cover, so the whole pouch shows rather than being cropped to fill. */
.seal__poster {
  width: min(56vh, 72vw, 640px);
  aspect-ratio: 1;
  object-fit: contain;
}

/* ---- Estradiol pouch photographs (receiving structure) ------------------
   Shipped hidden; the page reads finished without them. Unhiding needs only
   removing the `hidden` attribute; the lazy images don't fetch until then. */

.pouch {
  margin: 0 auto 4rem; /* the site's figure rhythm */
}

.pouch.col {
  max-width: 680px; /* the body column */
  padding-inline: 24px;
}

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

/* ---- the plaque-register line -------------------------------------------
   one line on the centre axis, between the photographs and the studies */
.register-line {
  max-width: 680px;
  margin: 0 auto 4rem;
  padding-inline: 24px;
  text-align: center;
  font-size: 12px; /* plaque register */
  line-height: 1.5;
}

/* ---- unchosen directions (working studies) ------------------------------
   a uniform grid of small squares. The hero above is the page's one
   scale-break, so these stay deliberately small — one scale only, no
   lightbox, no click-to-expand, no hover growth. Trivially extensible:
   add or reorder an <li class="study"> (nearest-to-chosen first). */

.studies {
  /* centred on the axis, left edge on the project-links guide — the heading and
     the row line up with the footer's project links, and the row runs wider than
     the old 680px column so the four studies sit larger. (Guides measured live in
     site.js; the vw fallback approximates them before it runs.) */
  max-width: calc(var(--page-w, 100vw) - 2 * var(--col-projects, calc(20vw + 60px)));
  margin: 0 auto 4rem;
  padding-inline: 0;
}

.studies__heading {
  margin: 0 0 1.5rem;
  font-size: 10px; /* census register */
  font-weight: 400; /* the register carries it, not weight */
  line-height: 1.6;
}

.studies__grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr); /* the four studies in one row */
  gap: 12px; /* tight gutters */
}

/* the square is the frame, not the <li> — so an optional caption can sit
   below it without breaking the grid's uniformity */
.study__frame {
  aspect-ratio: 1;
  overflow: hidden;
  background: #E9E7E2; /* placeholder tone until the crop lands */
}

.study__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover; /* uniform tiles, no orphan-row stretching */
}

/* a tile ships hidden and is revealed only once its crop has loaded (see
   estradiol.js); the display:block above outranks `hidden`, so restore it —
   until then the frame's placeholder tone shows, no broken glyph */
.study__frame img[hidden] {
  display: none;
}

/* the per-tile caption slot ships commented out; this readies it in case the
   client flips it on (see the markup) */
.study__cap {
  margin: 0.5rem 0 0;
  font-size: 10px; /* census register */
  line-height: 1.6;
}

/* the client asked for two-up on narrow screens */
@media (max-width: 720px) {
  .studies__grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* ---- the Basalt case page (revision 1) ----------------------------------
   The case column sits on the horizon's Swiss grid: it spans from the
   project-links guide to its mirror, centred on the page — the same measure as
   .census / .studies (site.js publishes --col-projects; the vw fallback
   approximates it before it runs). Every contained artifact — the cover set,
   the editorial spreads, the beats, the type sheet, the screen — shares that
   width. Below the site's one wide/narrow threshold the guides retract and the
   column takes a 4vw-margin full-width layout, as the census does. */
.case-basalt {
  --case-w: calc(var(--page-w, 100vw) - 2 * var(--col-projects, calc(20vw + 60px)));
}

.case-basalt .col,
.case-basalt .pair,
.case-basalt .case-spread,
.case-basalt .screen {
  max-width: var(--case-w);
  margin-inline: auto;
  padding-inline: 0; /* edges land on the guide, as .census / .studies do */
}

/* every case image fills its box; height:auto against each img's own
   width/height attributes reserves the space, so nothing reflows as the lazy
   images arrive (layout shift ~0) */
.case-basalt .col picture,
.case-basalt .col img,
.case-basalt .pair picture,
.case-basalt .pair img,
.case-basalt .case-spread picture,
.case-basalt .case-spread img,
.case-basalt .screen picture,
.case-basalt .screen img {
  display: block;
  width: 100%;
  height: auto;
}

/* the opening cover leads with a generous top margin — nothing above it */
.opening {
  margin-top: 5rem;
}

/* the cover set's two-up: equal columns, tops aligned */
.pair {
  display: flex;
  align-items: flex-start; /* aligned tops; ragged bottoms when heights differ */
  gap: 1.5rem;
  margin-block: 0 4rem; /* the figure rhythm, carried by the pair not its figures */
}

.pair figure {
  flex: 1 1 0;
  min-width: 0;
  margin: 0;
}

/* a credit belonging to a whole set rather than to one figure: the beats'
   caption register, sitting tight under the set. The captioned pair gives up its
   own bottom rhythm so the caption reads as part of the set, exactly as a
   figcaption does under a beat. */
.pair--captioned {
  margin-bottom: 0;
}

.set-caption {
  font-size: 13px; /* figcaption register */
  line-height: 1.5;
  margin-block: 0.5rem 4rem;
}

/* editorial spreads: verso and recto butt at the gutter with no gap — one
   visual object, its outer edge the site's one hairline. Spread C (the Turner)
   is a single full-width plate in the same frame. align-items: stretch so the
   blank verso takes the recto's height and the frame stays a clean rectangle. */
.case-spread {
  display: flex;
  align-items: stretch;
  margin-block: 0 4rem;
  border: var(--hairline);
}

.case-spread__page {
  flex: 1 1 0;
  min-width: 0;
}

.case-spread--single .case-spread__page {
  flex: 1 1 100%;
}

/* the blank verso: a plain page-colored panel; its edges are the spread's
   hairline, and it stretches to the recto's height */
.case-spread__blank {
  background: var(--field);
}

/* the clickable pages (spread halves, type sheet) reset to a bare button */
button.case-spread__page,
.typesheet__btn {
  appearance: none;
  display: block;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.typesheet__btn {
  width: 100%; /* a block button, not a flex child — fill the column */
}

/* pointer focus leaves no box; keyboard focus (:focus-visible) still rings */
button.case-spread__page:focus:not(:focus-visible),
.typesheet__btn:focus:not(:focus-visible) {
  outline: none;
}

/* the one screen surface: a phone-width still, centred in the grid column. The
   column itself is already centred on the guide (.case-basalt .screen above);
   auto inline margins centre the narrower figure inside it, at both widths —
   below 900px the column goes full-width and the 82vw figure centres in that. */
.screen {
  margin-block: 0 4rem;
}

.screen figure {
  margin: 0 auto;
  width: min(360px, 82vw);
}

/* below the one threshold the column goes full-width (4vw margins), the cover
   set stacks, and the spreads stay one object — they only shrink */
@media (max-width: 900px) {
  .case-basalt .col,
  .case-basalt .pair,
  .case-basalt .case-spread,
  .case-basalt .screen {
    max-width: none;
    margin-inline: 4vw;
  }

  .opening {
    margin-top: 3rem;
  }

  .pair {
    flex-direction: column;
    align-items: stretch;
  }

  .pair figure {
    flex: 0 0 auto;
  }
}

/* ---- the Basalt lightbox ------------------------------------------------
   A dimmed page behind, the image large. The dialog is transparent so only the
   image and the two small text affordances (close, and — editorial pages only —
   construction) sit over the scrim. Native <dialog>: Esc closes, focus stays
   inside, and the page behind is inert and does not scroll. */
.case-lightbox {
  width: 100vw;
  height: 100dvh;
  max-width: 100vw;
  max-height: 100dvh;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
}

.case-lightbox::backdrop {
  background: rgba(0, 0, 0, 0.6); /* the page behind, dimmed */
}

.case-lightbox__stage {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 5vmin;
}

/* the frame shrinks to the fitted image, so the construction toggle can hang off
   the enlarged page's own bottom-right corner rather than the viewport's */
.case-lightbox__frame {
  position: relative;
  display: inline-block;
  line-height: 0;
  max-width: 90vw;
  max-height: 90vh;
}

.case-lightbox__img {
  display: block;
  max-width: 90vw;
  max-height: 90vh;
  width: auto;
  height: auto;
}

/* the two affordances read on the dark scrim, so they are field-white here */
.case-lightbox__close,
.case-lightbox__toggle {
  position: absolute;
  appearance: none;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12px; /* footer-size */
  line-height: 1.5;
  color: var(--field);
  cursor: pointer;
}

.case-lightbox__close {
  top: 4vmin;
  right: 4vw;
}

/* to the right, hung just under the enlarged page's bottom edge — on the scrim,
   so the field-white label stays legible over a light page or its white wireframe */
.case-lightbox__toggle {
  top: 100%;
  right: 0;
  margin-top: 0.5rem;
}

.case-lightbox__close:hover,
.case-lightbox__toggle:hover,
.case-lightbox__toggle[aria-pressed="true"] {
  text-decoration: underline; /* engaged construction reads as the current state */
}

.case-lightbox__toggle[hidden] {
  display: none;
}

.case-lightbox:focus:not(:focus-visible),
.case-lightbox__close:focus:not(:focus-visible),
.case-lightbox__toggle:focus:not(:focus-visible) {
  outline: none;
}

/* ---- the Untitled page --------------------------------------------------
   A procession down the centre axis in six beats: the hero loop, the mark and
   its three icon loops, five campaign posters, the two system sheets, the
   product surfaces, the monograph.

   The case column sits on the horizon's Swiss grid exactly as Basalt's does:
   it spans from the project-links guide to its mirror, centred on the page
   (site.js publishes --col-projects; the vw fallback approximates it before it
   runs). Every beat but two takes that measure. The campaign posters are the
   first exception — they run at one uniform height and float narrower as each
   trim requires, with the squarest of the five landing exactly on the guide.
   The closing monograph is the second: it keeps the plaque measure, as on
   Libra. Below the site's one wide/narrow threshold the guides retract and the
   column takes a 4vw-margin full-width layout, as the census does.

   The artwork masters are line art and pages over transparency, derived onto
   the field (see FIELD in scripts/derive-images.mjs) — so a figure's edges are
   the field itself and every margin here is real air, not a printed border.
   The metadata cards are the exception: their corners stay cut out.

   Scoped to .case-untitled on <body>, as the Basalt case page is: the names
   here (mark, poster, sheet, surface) are the page's own vocabulary and general
   enough that another page could want them back. */

.case-untitled {
  --case-w: calc(var(--page-w, 100vw) - 2 * var(--col-projects, calc(20vw + 60px)));
}

.case-untitled .mark,
.case-untitled .icons,
.case-untitled .campaign,
.case-untitled .sheet,
.case-untitled .cards {
  max-width: var(--case-w);
  margin-inline: auto;
  padding-inline: 0; /* edges land on the guide, as .census / .studies do */
}

/* every clip on the page shares one manner: an object, not a player. No
   controls, no click behaviour; assets/js/untitled.js starts and stops them.
   The icon loops fill their tile; the hero's two are laid into its stage
   below. */
.case-untitled .icons video {
  display: block;
  width: 100%;
  height: auto;
}

/* Beat 1 — the solaris lift. First on the page, so a small top entry rather
   than the figure rhythm, as the Libra carousel and the Estradiol seal take.

   The hero is the one portrait object in the procession — 760 × 1340, taller
   than it is wide — so unlike every other beat it is capped by HEIGHT and floats
   narrower than the case column, the way the narrow campaign posters do. At the
   cap it renders at roughly the size of the phone it is a capture of, which is
   what it should read as. Client-tunable. */
.case-untitled .lift {
  width: fit-content;
  max-width: 100%;
  margin: 2rem auto 4rem;
}

/* The two clips stack in one box: the hover beneath, the lift above it. The box
   holds the aspect itself, so hiding the lift at the swap cannot reflow the page
   and neither clip's decode can shift it. */
.case-untitled .lift__stage {
  position: relative;
  height: min(74vh, calc(var(--case-w) * 1340 / 760));
  aspect-ratio: 760 / 1340;
}

.case-untitled .lift__stage video {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* the display:block above outranks the `hidden` attribute the swap sets */
.case-untitled .lift__rise[hidden] {
  display: none;
}

/* Beat 2 — the mark. Flush-cropped: the artwork runs to the pixel edge, so the
   air around it is entirely the layout's. Half the rendered mark height above
   and below, on top of the figure rhythm — enough that the mark never sits
   tight against the loop above or the icons below the way the pixel edge would
   allow. Both are client-tunable starting values. */
.case-untitled .mark {
  --mark-h: calc(var(--case-w) * 146 / 1025); /* the master's own proportion */
  margin-block: 4rem;
  padding-block: calc(0.5 * var(--mark-h));
}

.case-untitled .mark img {
  width: 100%;
  height: auto;
}

/* the three icon variants, equal loops on one row */
.case-untitled .icons {
  --icon-gap: 1.5rem;
  list-style: none;
  margin-block: 0 4rem;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--icon-gap);
  justify-items: center;
}

.case-untitled .icons li {
  width: min(240px, 100%);
}

/* narrow: one above two, not two above one — the first icon leads, as it does
   in the row. It spans both columns so it sits centred and alone, but takes a
   single column's width, because equal loops is the point of the row. */
@media (max-width: 640px) {
  .case-untitled .icons {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .case-untitled .icons li:first-child {
    grid-column: 1 / -1;
    width: min(240px, calc((100% - var(--icon-gap)) / 2));
  }
}

/* Beat 3 — the campaign. The five posters proceed down the axis at ONE height,
   each width following its own master's own aspect, so the set renders
   trim-true — Artforum visibly squarer and wider, Slanted narrowest. (The
   masters are exported at independent scales; it is each one's ASPECT, not a
   shared pixel height, that carries the trim.)

   The height is the squarest poster's width, so that poster lands exactly on
   the case column's guides and the other four float narrower inside it — which
   is the campaign's own proportion, not a layout accident. Artforum is exactly
   square as exported, so the height is simply the column; were a re-export to
   leave it off-square, this becomes case-w × its own height/width. */
.case-untitled .campaign {
  --poster-h: var(--case-w);
  margin-block: 0 4rem;
}

.case-untitled .poster {
  width: fit-content; /* the figure is the poster's width, so the caption lands under its left edge */
  max-width: 100%;
  margin: 0 auto 4rem;
}

/* height drives, width follows the master's aspect. max-width is released
   because a cap that bound would squash the poster rather than shrink it — and
   it cannot bind: --poster-h is already the squarest poster's own width. */
.case-untitled .poster img {
  display: block;
  height: var(--poster-h);
  width: auto;
  max-width: none;
}

/* The caption starts at the poster's left edge and takes no part in deciding
   the poster's width: a zero width contributes nothing to the figure's
   fit-content sizing, and the min-width then fills whatever the poster settled
   on. It is one line — these are one short sentence, and wrapping them inside
   a narrow poster's measure broke them for no reason; past the poster they
   simply run on into the column. */
.case-untitled .poster figcaption {
  width: 0;
  min-width: 100%;
  white-space: nowrap;
}

/* Beat 3 — the enlargeable posters reset to a bare block button, as the Basalt
   spread pages do. The system sheets are NOT triggers: they read at the column
   and have nothing more to show. */
.case-untitled .plate-btn {
  appearance: none;
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

/* pointer focus leaves no box; keyboard focus (:focus-visible) still rings */
.case-untitled .plate-btn:focus:not(:focus-visible) {
  outline: none;
}

/* Beat 4 — the sheets, on the case column. The type sheet is flush-cropped like
   the mark: its air is the figure's own margins. */
.case-untitled .sheet img {
  width: 100%;
  height: auto;
}

/* Beat 5 — the metadata cards, one per file with their corners cut out. The
   Jerónimos card stands alone at the column's full width, and the other four
   sit two-up beneath it: the component at size, then the component as a set. */
.case-untitled .cards {
  margin-block: 0 4rem;
}

.case-untitled .cards img {
  width: 100%;
  height: auto;
}

.case-untitled .cards figure {
  margin: 0;
}

.case-untitled .cards__grid {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
}

/* the client asked for two-up; on a phone the cards' own type would crush at
   half a 4vw column, so they stack */
@media (max-width: 560px) {
  .case-untitled .cards__grid {
    grid-template-columns: 1fr;
  }
}

/* below the one threshold the column goes full-width (4vw margins), as Basalt's
   does — the horizon's guides are gone on the phone bar */
@media (max-width: 900px) {
    .case-untitled .mark,
  .case-untitled .icons,
  .case-untitled .campaign,
  .case-untitled .sheet,
  .case-untitled .cards {
    max-width: none;
    margin-inline: 4vw;
  }

  .case-untitled {
    --case-w: 92vw; /* what the 4vw margins leave, for the poster height */
  }
}

/* ---- the About page -----------------------------------------------------
   Three beats down the centre axis: the two disk drawings as one spread, the
   biography, the colophon. Nothing here is a new register — the biography is
   .mono and the colophon .census, and the two text blocks share one measure,
   the plaque guide, so the page reads as a single column of type under a
   single plate of drawing.

   Two guides, then: the spread spans from the project-links guide to its
   mirror (the case column, as .census / .studies / Basalt's and Untitled's
   columns do), and the type sits inside it on the plaque guide. site.js
   publishes both; the vw fallbacks approximate them before it runs. Below the
   site's one 900px threshold the guides retract and both take the 4vw-margin
   full-width layout, as every other case column does. */

/* The drawings are a two-up row, frame 1 left and frame 2 right; --about-gap is
   the space between them, and the pair carries the figure rhythm, not its
   figures. Below the threshold they stack, as Basalt's cover pair does.

   Each frame's width is therefore fluid, and the <img> sizes attributes have to
   state it exactly — a `sizes` that lies wastes the ladder of native renders
   behind it. sizes cannot read a custom property, so the grid rule is
   transcribed there in plain units. The derivation, for whoever retunes this:

     the horizon is a flex row of five rectangles — ::before, name, projects,
     plaque (6 units), contact — so ten units, with 2 × 1.25rem of padding and
     4 × 2.5rem of gaps around them:

       unit           = (100vw - 200px) / 10          = 10vw - 20px
       --col-projects = 20px + 2 * unit + 2 * 40px    = 20vw + 60px
       spread         = 100vw - 2 * --col-projects    = 60vw - 120px
       one frame      = (spread - --about-gap) / 2    = 30vw - 72px

   which is what sizes carries above the threshold; below it the frames stack
   at the column's full 92vw. Change the gap, the plaque units, or the gutters
   and that last line moves — retune sizes with it. (The fallback below is that
   same derivation rather than an approximation of it, so the width is right on
   the first paint and does not change when site.js publishes the measured one.
   It was 19vw, which is ~23% wide at 1280 — enough that the opening image on a
   case page visibly stood too big until the deferred script landed.) */
.about-spread {
  display: flex;
  align-items: flex-start; /* the two canvases differ by 2px in height */
  gap: var(--about-gap, 1.5rem);
  max-width: calc(var(--page-w, 100vw) - 2 * var(--col-projects, calc(20vw + 60px)));
  /* first on the page: a small top entry, as the Libra carousel and the
     Estradiol seal take; the site's figure rhythm below */
  margin: 2rem auto 4rem;
}

.about-spread figure {
  flex: 1 1 0;
  min-width: 0;
  margin: 0;
}

.about-spread img {
  width: 100%;
  height: auto;
}

/* the colophon comes off the census's own project-links guide and onto the
   plaque guide, so it lands under the biography on one measure */
.about-colophon {
  max-width: calc(var(--page-w, 100vw) - 2 * var(--col-plaque, 29vw));
}

/* line-broken prose in the census register: no type is declared here — the
   .census block's own caption size and measure carry it, and this is only the
   reset the browser default would otherwise impose */
.about-colophon p {
  margin: 0;
}

@media (max-width: 900px) {
  /* the guides are gone on the phone bar, so the column goes full-width and the
     two frames stack rather than halving an already narrow measure.
     align-items: stretch, as Basalt's cover pair takes: start-aligned, a stacked
     figure shrinks to fit its content, and a lazy image inside one is then a
     100% of nothing — the frame collapses, never enters the viewport, and so
     never loads. */
  .about-spread {
    flex-direction: column;
    align-items: stretch;
    max-width: none;
    margin-inline: 4vw;
  }

  .about-spread figure {
    flex: 0 0 auto;
  }

  /* the census's own narrow rule is outranked by its base measure, so the
     About colophon states its own — it would otherwise sit at the plaque
     fallback, which is far too tight for a phone */
  .about-colophon {
    max-width: none;
    margin-inline: 4vw;
  }
}

/* ---- the horizon's hover previews ---------------------------------------
   The footer's five project names are the site's only door, and a name alone
   asks a viewer to guess. Hovering one puts that project in the middle of the
   viewport: flat, strokeless, borderless, no shadow, no panel — the work on
   the page's own ground, floating free of the layout entirely.

   Everything here is built by site.js at load; no page carries a line of it in
   markup, and construction is gated on a hover-capable pointer, so a touch
   device never sees this layer in its DOM at all. The layer is decoration —
   pointer-events: none and aria-hidden — so nothing here can be clicked,
   focused, read by a screen reader, or shift the page.

   Each project's preview has its own aspect and its own size, and the
   unevenness is deliberate: five objects, not five thumbnails on a grid. The
   four heights below are the hand-tunable numbers. */

/* Hung off the pointer rather than centred: a preview opens out of the cursor
   into the empty page above the horizon, bottom-left corner just up and to the
   right of it, and follows the cursor down the list. site.js does the placing —
   it is the only part of this that needs a number the stylesheet cannot know.
   The layer is the whole viewport again, since the anchor is a pointer that is
   itself inside the band. */
.pv {
  position: fixed;
  inset: 0;
  pointer-events: none;
  /* Over the horizon (50) as well as the page, since the pointer it hangs from
     is inside the band and a preview one nudge above it would otherwise be half
     swallowed by the band's own background. Under the moon (100), which is over
     everything by right. Nothing is lost by covering the band: the layer takes
     no pointer events, so the names underneath stay live. */
  z-index: 60;

  --pv-nudge: 12px;    /* the pointer's clearance, and the viewport's */
  --pv-gap: 12px;      /* between the three Basalt covers */
  --pv-single-h: 52vh; /* Estradiol, Basalt Libra — one image each */
  --pv-cover-h: 38vh;  /* each of the three Basalt covers */
  --pv-field-h: 52vh;  /* the one Field stack */
  --pv-video-h: 50vh;  /* the Untitled lift */
}

/* All five are placed off the same anchor and only one is ever on. left/bottom
   are site.js's, and neither is transitioned — a preview that eased toward the
   cursor would lag behind it, and the point is that it is where the cursor is.
   The fade lives on the ON state, not on the element: arriving at .is-on
   transitions, leaving it has no transition to run — so a preview fades up and
   cuts out, which is what skimming the list should feel like. Under reduced
   motion the site-wide transition reset makes both instant. */
.pv__item {
  position: absolute;
  max-width: 80vw;
  opacity: 0;
}

.pv__item.is-on {
  opacity: 1;
  transition: opacity 100ms linear;
}

/* Height is what is declared and width follows the aspect, for every preview
   here — nothing is cropped and nothing is fitted to a box. The max-width on
   the item above is a guard for viewports too narrow to hold the composed
   width; object-fit: contain is what that guard letterboxes with, so a squeeze
   can never distort an image. It does nothing until the guard bites. */
.pv__item img {
  display: block;
  object-fit: contain;
}

.pv--single img {
  height: var(--pv-single-h);
  width: auto;
  max-width: 100%;
}

/* the three covers in a row at one height — they are equal trims, so the row
   is even without being told to be */
.pv--row {
  display: flex;
  gap: var(--pv-gap);
}

.pv--row img {
  height: var(--pv-cover-h);
  width: auto;
  min-width: 0;
}

/* one stack, fully apart — Image 2 × 2, the exhibit's own photograph, captured
   on no ground: the sheets' own translucency shows whatever the preview is
   floating over, rather than a plate of field. */
.pv--field img {
  height: var(--pv-field-h);
  width: auto;
  max-width: 100%;
}

/* The lift, stacked as the Untitled hero stacks it: the hover beneath and the
   lift over it, so the handover is a visibility change and not a seek.

   What holds the box is neither clip but the poster, in flow and invisible. A
   <video> has no intrinsic size until its metadata arrives, and until then it
   lays out at the 300 × 150 that replaced elements default to — which in a
   shrink-to-fit box is a preview of briefly the wrong shape. The poster is the
   clip's own first frame, derived beside it, so it carries the clip's exact
   proportion and a replacement clip brings its own: nothing about the shape is
   written down here either. Invisible rather than absent because both clips are
   transparent in the surround, and a still lying behind them would ghost
   through as the content moves off it. What shows before they decode, and under
   reduced motion, is the videos' own poster attribute.

   No position of its own: .pv__item is already absolute, which is the
   containing block the two clips below need. Saying `relative` here — which is
   what this wanted when the items were grid cells — would put the box back in
   normal flow, and a flow box takes the width it is offered rather than the
   width of what is in it. */

.pv--video .pv__still {
  height: var(--pv-video-h);
  width: auto;
  max-width: 100%;
  visibility: hidden;
}

.pv--video video {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* the display: block above outranks the `hidden` attribute the swap sets */
.pv--video .pv__lift[hidden] {
  display: none;
}

/* ---- 404 ------------------------------------------------------------------
   The one page with no horizon. Everywhere else the ground line is invariant —
   it is what the site stands on — but 404.html is not a page of the site, it is
   what is returned when there is no page, and giving a nothing a ground would
   be the lie. So: the field, the moon in its standard mount, and two lines
   under it. No band, no plaque, no nav.

   Nothing new in the register either: the lines are .mono, the same 12px the
   monographs and the census take, and they sit on the plaque guide's measure —
   which here is the vw fallback, since with no horizon there is nothing for
   site.js to measure and no guides get published. */
.notfound {
  /* clear of the moon's mount — its own top offset, plus the largest diameter
     the clamp can give it, plus a gutter — so the lines land under the disc at
     every viewport rather than beside it at some */
  margin-top: calc(15vh + clamp(56px, 5vw, 88px) + var(--gutter));
  text-align: center;
}
