@import url("fonts/fonts.css");

/* ==========================================================================
   TT Fine Art
   Three zones: a fixed nav column, a narrow serif text column, and an image
   grid behind a vertical hairline. Small serif type on white.
   Source Serif 4 for text; Diatype Regular for captions and meta.
   ========================================================================== */

:root {
  --paper: #fff;
  --ink: rgba(0, 0, 0, 0.85);
  --dim: rgba(0, 0, 0, 0.42);
  --hair: rgba(0, 0, 0, 0.16);
  --panel: #f2f2f0;
  --accent: #7a2f1c;

  --serif: "Source Serif 4", Charter, "Iowan Old Style", Georgia, serif;
  --sans: "Diatype", "Helvetica Neue", Helvetica, Arial, sans-serif;

  /* Small, the way the reference sets it. */
  --t: 0.855rem;        /* ~13.7px body */
  --t-cap: 0.7rem;      /* captions and meta */

  --pad: 1rem;
  /* Sized to the nav's widest item ("Information", 80px) plus slack, rather
     than an arbitrary column width. */
  --nav-w: 6.5rem;
  --text-w: 23rem;
  --gap: 2.25rem;
  --cap-gap: .55rem;    /* picture -> its caption */
  --work-gap: 1.1rem;   /* that caption -> the next picture down */
}

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

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--serif);
  font-size: var(--t);
  line-height: 1.34;
  font-weight: 400;
  font-optical-sizing: auto;
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
}

img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }

:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }

.skip { position: absolute; left: -9999px; }
.skip:focus {
  left: var(--pad); top: var(--pad); z-index: 20;
  background: var(--ink); color: var(--paper); padding: .5rem .8rem;
  font-family: var(--sans); font-size: var(--t-cap);
}

/* ---------- shell ---------- */

.page {
  display: grid;
  grid-template-columns: var(--nav-w) minmax(0, var(--text-w)) minmax(0, 1fr);
  align-items: start;
  /* align-content:start matters once the rows stack: min-height would otherwise
     share the leftover height between them and open gaps under the nav. */
  align-content: start;
  min-height: 100vh;
  padding: var(--pad);
  column-gap: var(--gap);
}

/* Nav stays put while the image column scrolls past it. */
.nav {
  position: sticky;
  top: var(--pad);
  grid-column: 1;
}

/* 1.34em to match the standing column's own paragraph rhythm (.prose > * + *) —
   the two sides were on different scales, which is what read as uneven. */
.brand { display: block; font-weight: 700; text-decoration: none; margin-bottom: 1.34em; }

.nav ul { list-style: none; margin: 0 0 1.34em; padding: 0; }

/* The only feedback is a lighten while the mouse is down. No hover colour, and
   no marker on the open section — deliberately unstyled, so aria-current is
   carried for assistive tech only. No transition, so it snaps back on release
   rather than easing out. The tap-highlight reset stops mobile browsers
   painting their own grey box over the top. */
.nav a {
  display: block;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.nav a:active { color: var(--dim); }

/* The standing column. It is the same markup on every URL and is never
   re-rendered — only .panel changes when a section opens. Pinned like the nav,
   so scrolling a long panel leaves the standing text where it is instead of
   dragging it off the top. It sticks within its grid row, which the panel makes
   tall, so the two scroll independently without either needing its own
   scroll container. */
.col-text {
  grid-column: 2;
  position: sticky;
  top: var(--pad);
}

/* The panel region. Empty at rest. The margin and padding that make room for
   the rule are permanent — invisible, since there's nothing in the column to
   see them against — so that only the rule's own colour needs to transition;
   nothing about the layout has to change shape as it fades in or out.
   Three gaps are meant to match: standing text -> rule, rule -> panel content,
   and panel content -> the viewport's right edge. The last is already
   var(--pad), from .page's own padding; the negative margin below cancels out
   .page's own column-gap so the first lands on var(--pad) too, regardless of
   what that column-gap is set to. */
.panel {
  grid-column: 3;
  min-height: calc(100vh - 2 * var(--pad));
  margin-left: calc(var(--pad) - var(--gap));
  padding-left: var(--pad);
  border-left: 1px solid transparent;
  transition: border-color .22s ease;
}
.panel[data-open="true"]:not(.is-toggling) { border-color: var(--hair); }

/* Both sections ship in every page; only the open one is shown. Stated
   explicitly rather than leaning on the UA default for [hidden], which any
   later display rule would override. */
.panel__section { margin: 0; transition: opacity .22s ease; }
.panel__section[hidden] { display: none; }

/* The fade. is-fading fades the contents on every change; is-toggling is added
   only when the panel is entering or leaving the resting state, and takes the
   rule with it — so the rule fades in step with the contents instead of
   snapping away the instant data-open flips, but still stays put during a
   section swap, where it is the frame. app.js reads the duration off
   .panel__section so the two cannot drift. */
.panel.is-fading .panel__section { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .panel, .panel__section { transition-duration: .01ms; }
}

/* ---------- text ---------- */

h1, h2, h3 { margin: 0; font-size: inherit; font-weight: 400; line-height: inherit; }
.lede { font-style: italic; margin: 0 0 1.34em; }

.prose > * { margin-block: 0; }
.prose > * + * { margin-top: 1.34em; }
.prose strong { font-weight: 700; }
.prose em { font-style: italic; }
.prose ul { margin: 1.34em 0; padding-left: 1.1em; }
.prose li + li { margin-top: .5em; }
.prose li p { margin: 0; }
.prose a, .cv a, a.link {
  color: inherit; text-decoration: underline;
  text-decoration-thickness: .5px; text-underline-offset: .18em;
  transition: text-decoration-color .15s ease;
}
.prose a:hover, .cv a:hover, a.link:hover { text-decoration-color: transparent; }

/* ---------- image column ---------- */

/* Two columns that pack independently, so a short picture lets the next one in
   its column ride up instead of leaving a shelf across the row. The columns are
   real elements rather than grid tracks because a grid row is as tall as its
   tallest cell — which is exactly the levelling we're getting rid of. Equal
   tracks, so the column width is unchanged. */
.works {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* The columns keep their full gutter across; down the page the spacing is
     tighter, so a caption stays tied to the picture above it. row-gap only bites
     once the columns stack, where it keeps that seam matching the spacing
     inside each column. */
  column-gap: var(--gap);
  row-gap: var(--work-gap);
  align-items: start;
}
.works__col {
  display: grid;
  gap: var(--work-gap);
  align-content: start;
}
/* Stacked: the two columns fall one after the other, and the grid's own gap
   keeps the spacing between them identical to the spacing within them. */
@media (max-width: 78rem) { .works { grid-template-columns: minmax(0, 1fr); } }

.work { margin: 0; }
.work__frame { background: var(--panel); padding: clamp(.75rem, 1.6vw, 1.5rem); }
.work__frame img { width: 100%; }

/* Project frames are buttons so they open by click or keyboard. Reset the
   control's own styling; the frame keeps the look it had as a div. */
button.work__frame {
  display: block;
  width: 100%;
  border: 0;
  font: inherit;
  color: inherit;
  cursor: zoom-in;
  -webkit-tap-highlight-color: transparent;
}
button.work__frame img { transition: opacity .18s ease; }
button.work__frame:hover img { opacity: .82; }

/* ---------- viewer ---------- */

/* One image centred on a near-opaque white field, with its mat and the caption
   beneath. No controls and no way to move between images: clicking anywhere
   outside the frame dismisses it, as does Escape. zoom-out mirrors the
   zoom-in on the tiles, since without icons nothing else says the surround is
   the way out. */
.lb {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: none;
  place-items: center;
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  margin: 0;
  padding: clamp(2.5rem, 6vw, 4rem);
  border: 0;
  background: rgba(255, 255, 255, .97);
  color: var(--ink);
  overflow: hidden;
  cursor: zoom-out;
}
.lb[open] { display: grid; }
.lb::backdrop { background: rgba(255, 255, 255, .97); }

/* While the frame is shrinking back onto its tile, clear the surround so the
   grid shows through and the landing is visible. Both surfaces have to go: the
   dialog covers the viewport, and its backdrop sits behind that. The controls
   and caption go with it — they belong to the viewer, not to the tile. */
.lb.is-closing { background: transparent; transition: background-color .18s ease; }
.lb.is-closing::backdrop { background: transparent; transition: background-color .18s ease; }
.lb.is-closing figcaption { opacity: 0; transition: opacity .18s ease; }

.lb__fig {
  margin: 0;
  min-height: 0;
  max-height: 100%;
  max-width: min(100%, 60rem);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .8rem;
}
/* The mat carried over from the grid tile, so the image keeps its frame rather
   than floating loose against the viewer's backdrop. It shrink-wraps on both
   axes so the mat is the same thickness on all four sides: stretched, a portrait
   image would leave deep gaps at the sides and a landscape one deep gaps above
   and below. */
.lb__frame {
  flex: 0 1 auto;
  min-height: 0;
  width: fit-content;
  max-width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel);
  padding: clamp(.75rem, 1.6vw, 1.5rem);
  /* The frame is what grows out of the clicked tile, so it carries the FLIP.
     Top-left origin is what app.js measures its deltas against. */
  transform-origin: top left;
  will-change: transform, opacity;
  cursor: default;
}
/* Capped here rather than measured in script: with the image centred there is
   no right margin to hug, so the mat stays even on its own. */
.lb__frame img {
  max-width: 100%;
  max-height: calc(78vh - 2 * clamp(.75rem, 1.6vw, 1.5rem));
  width: auto;
  height: auto;
  object-fit: contain;
  will-change: transform, opacity;
}
.lb__fig figcaption {
  flex: none;
  font-family: var(--sans);
  font-size: var(--t-cap);
  text-align: center;
}
.lb__cap-title { color: var(--ink); }
.lb__cap-loc { color: var(--dim); }

.work figcaption {
  margin-top: var(--cap-gap);
  font-family: var(--sans);
  font-size: var(--t-cap);
  line-height: 1.45;
  color: var(--ink);
}
.work figcaption span { display: block; color: var(--dim); }

.stack { display: grid; gap: var(--gap); }

/* CV grid: sections flow across columns, each kept whole. A hairline sits
   above the heading, as on the reference's information page. */
/* Grid rather than CSS columns: with only three short sections, columns would
   balance them by height and leave the third track empty. */
.cv {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: var(--gap);
  margin-top: var(--gap);
  align-items: start;
}
.cv section { margin: 0; }
.cv > section > h3 {
  border-top: 1px solid rgba(0, 0, 0, 0.55);
  padding-top: .45rem;
  margin-bottom: .9rem;
  font-size: var(--t-cap);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .09em;
}
.cv ul { list-style: none; margin: 0; padding: 0; }
.cv li + li { margin-top: 1.05rem; }
.cv p { margin: 0; }
.cv .sub { color: var(--dim); }

/* ---------- narrow screens ---------- */

@media (max-width: 56rem) {
  :root { --gap: 1.5rem; }
  .page { grid-template-columns: minmax(0, 1fr); row-gap: 1.25rem; }
  .nav { position: static; grid-column: 1; }
  .brand { margin-bottom: .9em; }
  /* Stacked, one item per line — same as desktop. */
  .nav ul { margin-bottom: .9em; }
  /* The row-gap already separates the nav from the standing text; a trailing
     margin here as well left a gap twice the size of the nav's own breaks. */
  .nav ul:last-of-type { margin-bottom: 0; }
  .col-text, .panel { grid-column: 1; }
  /* Stacked, so nothing is pinned — the page scrolls as one. */
  .col-text { position: static; }

  /* An open section becomes a drawer over the standing column, anchored right,
     rather than being pushed below it. The standing text stays exactly where it
     was — a strip of it is left visible, and tapping that strip closes the
     drawer (the same "left of the rule" gesture as on desktop). */
  .panel { min-height: 0; }
  .panel[data-open="true"] {
    position: fixed;
    inset: 0 0 0 auto;
    z-index: 30;
    width: min(80%, 30rem);
    margin-left: 0;
    background: var(--paper);
    border-top: 0;
    padding: var(--pad) var(--pad) var(--pad) 1.25rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
}
