/* =============================================================================
   portfolio.css — styles used only by index.html (the portfolio homepage).
   Shared pieces (colors, fonts, header, footer) are in css/site.css.

   Sections, top to bottom of the page:
     1. Intro — three columns (who / what I make / off the clock)
     2. Book hero
     3. "Selected work" heading + tag filter pills
     4. Uniform grid of project tiles
     5. Unfolding project panel (opens under a tile's row)
   ============================================================================= */

/* js/portfolio.js scrolls the page to follow panels as they open and close;
   the browser's own scroll anchoring would fight that, so it's off here */
body { overflow-anchor: none; }


/* -----------------------------------------------------------------------------
   1. INTRO — three columns, stacks to one on phones
   ----------------------------------------------------------------------------- */
.trio {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(22px, 3vw, 44px);
  padding-block: clamp(28px, 3.5vw, 48px);
  border-bottom: 1px solid var(--rule);
}
.trio h2 {                       /* the small mono headings: "Hi, I'm Jake." etc. */
  font-family: var(--mono); font-weight: 500; font-size: 15px; letter-spacing: .01em;
  margin: 0 0 10px; color: var(--ink);
}
.trio p {
  margin: 0;
  font-size: clamp(16.5px, 1.25vw, 18.5px); line-height: 1.62;
  text-wrap: pretty;             /* avoids a lonely last word on its own line */
}
/* links stay ink-colored with a thick pale-coral underline, so a paragraph
   with several links doesn't turn into a wall of red */
.trio a {
  color: var(--ink); text-decoration: none;
  border-bottom: 3px solid var(--accent-soft);
  transition: border-color .15s;
}
.trio a:hover { border-bottom-color: var(--accent); }

@media (max-width: 760px) {
  .trio { grid-template-columns: 1fr; gap: 20px; }
}


/* -----------------------------------------------------------------------------
   2. BOOK HERO
   Desktop: the cover flush with the page's left edge, the text running to its
   right edge, so the two have the same margin on either side. The cover's
   column is --feature-w wide.
   Tablet (641–900px): cover beside the text, top-aligned.
   Phone (≤640px): cover stacked above the text, full column width.
   ----------------------------------------------------------------------------- */
/* the width of the book cover, and how far the book section sits in from
   the page's edges */
.wrap { --feature-w: clamp(280px, 34vw, 440px); --feature-inset: clamp(0px, 5vw, 80px); }
@media (max-width: 720px) { .wrap { --feature-inset: 0px; } }   /* phones use the full width */
.hero {
  display: grid;
  grid-template-columns: var(--feature-w) minmax(0, 1fr);
  gap: clamp(24px, 4vw, 56px);
  align-items: stretch;
  padding-block: clamp(30px, 4vw, 56px);
  padding-inline: var(--feature-inset);
  border-bottom: 1px solid var(--rule);
}
.hero-cover { margin: 0; display: flex; align-items: center; }
.hero-cover img {
  width: 100%; height: auto; align-self: center;
  border: 1px solid var(--stroke);
  box-shadow: var(--shadow-lift);
}

/* "OUT NOVEMBER 17 · PREORDERS OPEN" */
.eyebrow {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--mono); font-size: 12px; letter-spacing: var(--track);
  text-transform: uppercase; color: var(--accent-ink);
}

.hero h1 {
  font-family: var(--display); font-weight: 400; line-height: 1.1;
  font-size: clamp(2rem, 1.2rem + 3vw, 3.4rem);
  margin: 14px 0 0; text-wrap: balance;
}
.hero p {
  color: var(--muted);
  margin: 15px 0 0; font-size: clamp(15px, 1vw, 16.5px);
}
.hero-meta {                     /* Belt Publishing · 8×8 hardback · $30.00 */
  display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 18px;
  font-family: var(--mono); font-size: 12px; color: var(--muted);
}
.hero-meta span { white-space: nowrap; }

/* -- LINKS -------------------------------------------------------------------
   Every link that leads somewhere looks the same: the Preorder link (.btn), a
   panel's story and book links (.plinks), lists of stories (.storylinks),
   picture captions (.pcap) and story cards (.scard-ttl). Text with a soft coral
   underline; on hover the underline turns full coral and the ↗ arrow (.ar)
   hops up and to the right. Change them all here. */
.btn, .plinks a, .storylinks a, .pshots a.pcap, .scard-ttl > span {
  color: var(--ink); text-decoration: none;
  border-bottom: 2px solid var(--accent-soft);
  transition: border-color .15s;
}
.btn:hover, .plinks a:hover, .storylinks a:hover, .pshots a.pcap:hover,
.storycards a:hover .scard-ttl > span { border-bottom-color: var(--accent); }
.ar { display: inline-block; transition: transform .2s ease; }
.btn:hover .ar, .plinks a:hover .ar, .storylinks a:hover .ar,
.pshots a.pcap:hover .ar, .storycards a:hover .ar { transform: translate(2px, -2px); }

/* "Preorder the book" — a little larger than the other links */
.btn { display: inline-block; margin-top: 20px; font-size: 17px; line-height: 1.4; }

@media (max-width: 900px) {      /* tablet */
  .hero { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: 28px; align-items: start; }
  .hero-cover { display: block; }
  .hero-cover img { width: 100%; }
}
@media (max-width: 640px) {      /* phone */
  .hero { grid-template-columns: 1fr; gap: 22px; }
  .hero h1 { margin-top: 12px; }
}


/* -----------------------------------------------------------------------------
   3. "SELECTED WORK" HEADING + TAG FILTERS
   The pills are generated from TAGS in js/projects.js.
   ----------------------------------------------------------------------------- */
.sec-head {
  display: flex; flex-wrap: wrap; gap: 10px 20px;
  align-items: baseline; justify-content: space-between;
  padding-top: clamp(16px, 2.2vw, 28px);
}
.sec-head h2 {
  font-family: var(--display); font-weight: 400; margin: 0;
  font-size: clamp(1.25rem, 1rem + 1vw, 1.85rem);
}
.filters { padding-block: 16px 24px; }
.fgroup { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; }

.pill {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .04em;
  padding: 5px 12px; cursor: pointer;
  background: transparent; color: var(--ink);
  border: 1px solid var(--rule); border-radius: 999px;
  transition: background .14s, border-color .14s, color .14s;
}
.pill:hover { border-color: var(--stroke); }
.pill[aria-pressed="true"] { background: var(--accent-ink); color: #fff; border-color: var(--accent-ink); }

.fclear {                        /* "Clear filters" — only shown while a tag is selected */
  margin-left: auto; padding: 5px;
  font-family: var(--mono); font-size: 11px;
  background: none; border: 0; color: var(--accent-ink);
  cursor: pointer; text-decoration: underline;
}


/* -----------------------------------------------------------------------------
   4. UNIFORM GRID
   Every tile is the same 5:4 shape. Images fill it edge to edge
   (object-fit: cover), so reformat source images to 5:4 to control the crop.
   ----------------------------------------------------------------------------- */
.grid {
  /* spacing between tiles: across (columns) and down (rows) */
  --col-gap: 34px;
  --row-gap: 38px;
  display: grid;
  /* As many columns as fit at 218px or wider, but never more than 3:
     each column is at least a third of the row (minus the gaps).
     To change the maximum, change both 3 and 2 (the number of gaps). */
  grid-template-columns: repeat(auto-fill, minmax(max(218px, calc((100% - 2 * var(--col-gap)) / 3)), 1fr));
  gap: var(--row-gap) var(--col-gap);
  align-items: start;
  padding-bottom: 10px;
}
@media (max-width: 520px) {
  .grid { --col-gap: 16px; --row-gap: 22px; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); }
}

/* each tile is a <button> — clicking unfolds its panel */
.gcard {
  display: block; width: 100%; padding: 0; cursor: pointer;
  font: inherit; color: inherit; text-align: left;
  background: none; border: 0;
}

/* a large tile (size: "large" in projects.js): two columns wide and two
   rows tall. Its picture fills the height of both rows, so its bottom and
   its title line up with the tiles beside it; the picture is cropped to fit
   (a little taller than 5:4). Where nothing sits beside it (a two-column
   phone grid) it's simply 5:4, and on a one-column grid it's an ordinary tile. */
.gcard.big {
  grid-column: span 2; grid-row: span 2; align-self: stretch;
  display: flex; flex-direction: column;
}
.gcard.big .gframe { flex: 1 0 auto; }     /* at least 5:4, taller to fill both rows */
.gcard.big .gframe img,
.gcard.big .gframe video { position: absolute; inset: 0; }
@media (max-width: 343px) {
  .gcard.big { grid-column: auto; grid-row: auto; display: block; }
}

/* the picture frame: 5:4, hairline black stroke */
.gframe {
  display: block;                /* must be block: aspect-ratio is ignored on inline elements */
  position: relative; aspect-ratio: 5 / 4; overflow: hidden;
  background: var(--plate);
  border: 1px solid var(--stroke);
  box-shadow: var(--shadow);
  transition: transform .22s cubic-bezier(.2,.7,.3,1), box-shadow .22s, border-color .18s;
}
.gframe img,
.gframe video { width: 100%; height: 100%; object-fit: cover; display: block; }

/* hover: the "lift" — slight tilt, raised shadow */
@media (hover: hover) {
  .gcard:hover .gframe { transform: rotate(1deg) translateY(-3px); box-shadow: var(--shadow-lift); }
}

/* the + / − chip: hidden at rest, shows on hover or keyboard focus,
   stays visible (as −) while the tile's panel is open */
.chip {
  position: absolute; right: 8px; bottom: 8px; z-index: 2;
  width: 24px; height: 24px;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg); border: 1px solid var(--stroke);
  font-family: var(--mono); font-size: 14px; line-height: 1; color: var(--ink);
  opacity: 0; transform: translateY(3px);
  transition: opacity .16s, transform .16s, background .16s;
}
@media (hover: hover) { .gcard:hover .chip { opacity: 1; transform: none; } }
.gcard:focus-visible .chip { opacity: 1; transform: none; }

/* open state */
.gcard[aria-expanded="true"] .chip { opacity: 1; transform: none; background: var(--accent); color: #fff; }
.gcard[aria-expanded="true"] .gframe {
  border-color: var(--accent);
  box-shadow: var(--shadow-lift), 0 0 0 2px var(--accent);
}

/* "IN THE BOOK" corner marker (inBook: true in projects.js) */
.marker {
  position: absolute; top: 0; left: 0; z-index: 2;
  font-family: var(--mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  background: var(--accent); color: #fff; padding: 4px 8px;
  border-right: 1px solid var(--stroke); border-bottom: 1px solid var(--stroke);
}

/* smaller on a panel's pictures ({ src, inBook: true } in projects.js),
   and in the top right corner */
.pshots .marker {
  font-size: 9px; padding: 3px 6px;
  left: auto; right: 0;
  border-right: 0; border-left: 1px solid var(--stroke);
}

/* title + tags under each tile */
.cap { display: block; margin-top: 12px; }
.cap .ttl {
  display: block;
  font-family: var(--display); font-weight: 400; font-size: 17.5px; line-height: 1.3;
  margin: 0; transition: color .15s;
}
.gcard:hover .cap .ttl,
.gcard[aria-expanded="true"] .cap .ttl { color: var(--accent-ink); }

.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.tag {
  font-family: var(--mono); font-size: 11px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
}
.tag::after { content: "\00B7"; margin-left: 6px; opacity: .5; }   /* the · between tags */
.tag:last-child::after { content: ""; }

/* phones: two small tiles side by side, so keep the captions compact */
@media (max-width: 520px) {
  .cap { margin-top: 9px; }
  .cap .ttl { font-size: 15px; }
  .tags { gap: 5px; margin-top: 5px; }
  .tag { font-size: 11px; }
}

/* shown when no project has every selected tag */
.empty { padding: 50px 0; text-align: center; color: var(--muted); font-family: var(--mono); font-size: 13px; }

/* the plain list of projects inside the grid, written by
   tools/build-project-list.mjs. It's for search engines, AI tools and anyone
   without JavaScript; for everyone else (html.js) it's hidden, and
   js/portfolio.js replaces it with the tiles. */
.js .plist { display: none; }
.plist { grid-column: 1 / -1; margin: 0; padding-left: 1.4em; max-width: 72ch; }
.plist > li { margin-bottom: 28px; }
.plist h3 { margin: 0 0 4px; font-family: var(--display); font-weight: 400; font-size: 1.2rem; }
.plist p { margin: 0 0 8px; line-height: 1.55; }
.plist ul { margin: 0; padding-left: 1.2em; }


/* -----------------------------------------------------------------------------
   5. UNFOLDING PANEL
   js/portfolio.js inserts the panel after the LAST tile in the clicked tile's
   row, so the grid never gets a gap in the middle of a row. It animates open
   by growing its grid row from 0fr to 1fr.
   ----------------------------------------------------------------------------- */
.panel {
  /* How far the panel sits below the open tile's picture: level with the top
     of the tile's title (js/portfolio.js measures it). The open tile's title
     and tags fade out (the panel repeats them), and the panel is pulled up
     into their space and the row gap, which js measures as --pull. */
  --panel-gap: 12px;
  grid-column: 1 / -1;           /* spans the whole grid width */
  display: grid; grid-template-rows: 0fr;
  /* its top edge is set in its final place from the start, so only its
     height animates: it unfolds downward, and folds back up from the bottom */
  margin-top: calc(var(--panel-gap) - var(--row-gap) - var(--pull, 0px));
  transition: grid-template-rows .32s cubic-bezier(.2,.7,.3,1);
}
.panel[data-open="true"] { grid-template-rows: 1fr; }
/* while a panel is open, the title and tags fade out on the open tile and
   on any tile in its row the panel reaches up over (js adds .hush) */
.gcard .cap, .gcard .tags { transition: opacity .2s; }
.gcard.hush .cap,
.gcard.hush .tags { opacity: 0; }
.panel > .panel-in { overflow: hidden; min-height: 0; }

.panel-body {
  position: relative;
  /* one column (phones, and text-only panels); PANEL LAYOUTS below arranges
     wider screens. minmax(0, …) lets it shrink; a plain 1fr would stretch to
     the widest picture and get cut off at the right edge. */
  display: grid; grid-template-columns: minmax(0, 1fr);
  --pbody-gap: clamp(18px, 2.6vw, 38px);   /* space around the text/pictures columns */
  --shot-gap: 12px;                        /* space between pictures in a row (.pshots.mosaic) */
  gap: var(--pbody-gap);
  padding: clamp(20px, 2.5vw, 32px);
  background: var(--plate);
  border: 1px solid var(--stroke);
}
@media (max-width: 720px) { .panel-body { --shot-gap: 8px; } }
/* a second group of pictures (splitBelow in js/portfolio.js, e.g. The Legacy
   Tree's photos). Where it stacks right under the top picture (phones, and
   1- or 2-column detail views), the negative margin cancels the grid's own
   (wider) row gap and substitutes --shot-gap, so the space between the two
   groups matches the gap between the pictures. On wider 3-column views it
   sits under the text instead (.split, below). */
.pmedia-below { margin-top: calc(var(--shot-gap) - var(--pbody-gap)); }
/* its credit line follows it, just as close as under any other pictures */
.panel-body > .pcredit { margin-top: calc(8px - var(--pbody-gap)); }
/* -- PANEL LAYOUT (wider screens) -------------------------------------------
   The text column sits beside the pictures: title, year and tags,
   description, awards (.ptop), then links and buttons (.pfoot).
   The text column is as wide as its text needs, up to 34rem (and at least
   300px); the pictures get the rest. js/portfolio.js sets the width of the
   pictures (.pmedia) to keep them within a set height (MEDIA_MAX_HEIGHT),
   and they sit centered in their column, with equal space above and below,
   and to either side. */
@media (min-width: 721px) {
  .panel-body:not(.solo) { grid-template-columns: fit-content(34rem) minmax(0, 1fr); }
  .panel-body:not(.solo) .panel-main { min-width: 300px; }
  .pmedia {
    justify-self: center; align-self: center;
    padding-block: 30px;                  /* clears the Close button, and keeps it centered */
  }
  /* mediaHalf: true in projects.js: the text and the pictures each get
     half the panel, and the pictures fill their half */
  .panel:not([data-cols]) .panel-body.half { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .panel:not([data-cols]) .half > .pmedia { justify-self: stretch; }

  /* mediaFull: true in projects.js: the pictures go under the text and fill
     the panel's whole width (js lifts the height limit) */
  .panel .panel-body.full:not(.solo) { grid-template-columns: minmax(0, 1fr); }
  .panel .full > .pmedia { justify-self: stretch; align-self: start; padding-block: 0; }
  /* …and the text runs the panel's whole width too, like the pictures */
  .panel .panel-body.full .desc,
  .panel .panel-body.full .award,
  .panel .panel-body.full .storylinks { max-width: none; }

  /* .split (splitBelow in js/portfolio.js, e.g. The Legacy Tree): the top
     picture sits at the right edge (js sizes it: sizeSplitMedia), and the
     other pictures (.pmedia-below) fill the width beside it, under the text,
     their bottoms lined up with its bottom. The gap between the pictures and
     the top picture is the same as between the pictures (--shot-gap). The
     credit goes under the pictures. */
  .panel:not([data-cols]) .panel-body.split {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto 1fr auto;
    column-gap: var(--shot-gap); row-gap: 0;
  }
  .panel:not([data-cols]) .split > .panel-main {
    grid-column: 1; grid-row: 1;
    padding-right: calc(var(--pbody-gap) - var(--shot-gap));   /* the text keeps its usual distance */
  }
  .panel:not([data-cols]) .split > .pmedia:not(.pmedia-below) {
    grid-column: 2; grid-row: 1 / span 2;
    justify-self: end; align-self: end;
    padding-block: 30px 0;                /* still clears the Close button */
  }
  .panel:not([data-cols]) .split > .pmedia-below {
    grid-column: 1; grid-row: 2;
    justify-self: stretch; align-self: end;
    padding-block: 0;
    margin-top: var(--pbody-gap);         /* at least the usual gap under the text */
  }
  .panel:not([data-cols]) .split > .pcredit { grid-column: 1; grid-row: 3; margin-top: 8px; }

  /* a one- or two-column detail view (panelCols: 1 or 2 in projects.js; js
     sets its width and position under the tile): one column inside, so the
     pictures and their links stack under the description, and the text runs
     the full width rather than stopping at a comfortable line length */
  .panel[data-cols] .panel-body:not(.solo) { grid-template-columns: minmax(0, 1fr); }
  .panel[data-cols] .panel-main { min-width: 0; }
  .panel[data-cols] .pmedia { justify-self: start; align-self: start; padding-block: 0; }
  .panel[data-cols] .desc,
  .panel[data-cols] .award,
  .panel[data-cols] .storylinks { max-width: none; }
}
.pfoot > :first-child { margin-top: 0; }
.ptop > :last-child { margin-bottom: 0; }
.pfoot { padding-top: 28px; }                 /* space between the text and the links */

.panel h3 {
  font-family: var(--display); font-weight: 400; line-height: 1.2;
  font-size: clamp(1.15rem, .95rem + .8vw, 1.55rem);
  margin: 0 0 10px; padding-right: 70px;  /* room for the Close button */
}
/* year and tags under the title — the same small labels as under each tile */
.pmeta { margin: 0 0 24px; }
/* the description */
.desc { font-size: 15.5px; line-height: 1.6; margin: 0 0 22px; max-width: var(--measure); }
/* a link inside a description gets the same coral underline as the story links */
.desc a {
  color: var(--ink); text-decoration: none;
  border-bottom: 2px solid var(--accent-soft); transition: border-color .15s;
}
.desc a:hover { border-bottom-color: var(--accent); }
/* a link inside an award line, e.g. to the awards page */
.award a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--accent-soft); }
.award a:hover { border-bottom-color: var(--accent); }
/* shown when a project's description is blank — so it's obvious what's left to write */
.desc.placeholder { color: var(--muted); font-style: italic; }
.award {                          /* ★ lines, from awards: [] in projects.js */
  position: relative; max-width: var(--measure);
  font-size: 13.5px; line-height: 1.55; margin: 0 0 12px; padding-left: 21px;
}
.award::before { content: "\2605"; position: absolute; left: 0; top: 1px; color: var(--accent); font-size: 12px; }
/* a quote from the judges ({ award, quote, quoteBy } in projects.js): a small
   box on the page's darker paper, with who said it underneath */
.award .quote {
  margin: 8px 0 0; padding: 11px 15px 12px;
  background: var(--bg); border: 1px solid var(--rule);
  font-style: italic; font-size: 14px; line-height: 1.55;
}
.award .quote footer {
  margin-top: 7px; font-style: normal;
  font-family: var(--mono); font-size: var(--label); letter-spacing: var(--track);
  text-transform: uppercase; color: var(--muted);
}

/* a panel's links: "Read the story ↗", "In the book ↗" (styled in LINKS above) */
.plinks { display: flex; flex-wrap: wrap; gap: 10px 26px; margin-top: 26px; }
.plinks a { font-size: 15px; line-height: 1.4; }

/* stories as cards (links: [{ label, url, image }] in projects.js): each
   story's sharing picture at its 1.91:1 shape, headline underneath, two to a row */
.storycards {
  list-style: none; margin: 4px 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 16px;
}
.storycards a { display: block; color: var(--ink); text-decoration: none; }
.scard-img {
  display: block; aspect-ratio: 1.91 / 1; overflow: hidden;
  border: 1px solid var(--stroke); background: var(--bg);
  transition: transform .18s;
}
.scard-img img, .scard-img video { width: 100%; height: 100%; object-fit: cover; display: block; }
.scard-ttl { display: block; margin-top: 8px; font-size: 14.5px; line-height: 1.4; }
.storycards a:hover .scard-img { transform: rotate(.8deg); }

/* list of stories (links: [] in projects.js), for projects that span several */
.storylinks {
  list-style: none; margin: 4px 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px 22px;
  max-width: var(--measure);
}
.storylinks a { font-size: 15px; line-height: 1.4; }   /* styled in LINKS above */

/* extra pictures (images: [] in projects.js), two per row; click one to open it full size */
.pshots { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; align-content: start; }
/* .pmedia holds the pictures */
.pmedia { min-width: 0; }
.pshots a {
  display: block; overflow: hidden;
  position: relative;                   /* holds an "In the book" .marker */
  border: 1px solid var(--stroke); background: var(--bg);
  transition: transform .18s;
}
.pshots a:hover { transform: rotate(.8deg); }
/* several pictures: even 4:3 thumbnails (click one to see it uncropped, full size) */
.pshots img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
/* stack: true in projects.js: the pictures one above the other, each the
   full width, uncropped (What the Interstates Destroyed) */
.pshots.stack { grid-template-columns: minmax(0, 1fr); gap: var(--shot-gap); }
.pshots.stack img { aspect-ratio: auto; height: auto; object-fit: fill; }
/* 6 or more (js adds .grid): three to a row, uncropped (best when they share
   a shape, like Lindsey Vonn's Mountains); two to a row on phones */
.pshots.grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--shot-gap); }
.pshots.grid img { aspect-ratio: auto; height: auto; object-fit: fill; }
@media (max-width: 720px) { .pshots.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* a single picture (or a lone video, e.g. a map split into its own .pmedia
   above splitBelow's photos) gets the whole column, shown uncropped */
.pshots a:only-child,
.pshots > .pvid:only-child { grid-column: 1 / -1; }
.pshots a:only-child img { aspect-ratio: auto; height: auto; object-fit: fill; }
/* 2 to 5 pictures (js adds .mosaic): uncropped, at matching heights. Two sit
   side by side; with three or more, the widest goes across the top and the
   rest share a row under it. js/portfolio.js gives each picture its shape
   (width ÷ height) as --ar and marks the widest .wide.
   Growing each one in proportion to its shape is what makes the heights
   match; the 2px basis covers the hairline border, so the match is exact. */
.pshots.mosaic { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--shot-gap); }
.pshots.mosaic > * { flex: var(--ar, 1) 1 2px; min-width: 0; }
.pshots.mosaic > .wide { flex-basis: 100%; order: -1; }
.pshots.mosaic img { aspect-ratio: auto; height: auto; object-fit: fill; }
@media (max-width: 720px) {
  /* 4 or 5 pictures: on a phone the lower row wraps, two to a line. A
     starting width in proportion to each picture's shape keeps the heights
     matched within each line; 30% per unit of shape fits two, never three. */
  .pshots.mosaic.many > :not(.wide) { flex-basis: calc(var(--ar, 1) * 30%); }
}
/* 4 pictures (.pairs): two rows of two at every width, by the same trick —
   a starting width of 30% per unit of shape fits two to a row, never three */
.pshots.mosaic.pairs > * { flex-basis: calc(var(--ar, 1) * 30%); }

/* a tall picture beside a column of others (tall: true in projects.js, e.g.
   Outdoor tales, Urban Paddling): the group fills the whole width, the
   column pictures at 3:2 and the tall one as tall as that column, all
   cropped to fit. js (fitTall) splits the width to suit the tall picture's
   shape and the number beside it; 1.6fr is its starting guess. */
.pshots.tallmix {
  display: grid; gap: var(--shot-gap);
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  grid-template-rows: repeat(var(--rest, 3), auto);
}
.pshots.tallmix > * { min-width: 0; }
.pshots.tallmix img { aspect-ratio: 3 / 2; object-fit: cover; height: auto; }
/* the tall one spans every row; taken out of the flow so its own shape
   doesn't stretch the rows, it simply fills the space they make */
.pshots.tallmix > [data-tall] { grid-row: 1 / -1; position: relative; }
.pshots.tallmix > [data-tall] img { position: absolute; inset: 0; width: 100%; height: 100%; aspect-ratio: auto; }

/* scattered pictures (scatter: true in projects.js, e.g. 22, A Map's
   glyphs): no frames, uncropped, all in one line across the width, at
   matching heights (each grows in proportion to its shape, the same trick
   as .mosaic). Each one is tilted a little, alternating, so they look
   scattered rather than lined up. */
.pshots.scatter {
  display: flex; flex-wrap: nowrap; align-items: center;
  gap: 24px; padding: 8px 4px;
}
.pshots.scatter > * { flex: var(--ar, 1) 1 0; min-width: 0; }
.pshots.scatter a { border: 0; background: none; overflow: visible; }
.pshots.scatter img { aspect-ratio: auto; height: auto; object-fit: contain; }
.pshots.scatter > :nth-child(4n+1) { transform: rotate(-3deg); }
.pshots.scatter > :nth-child(4n+2) { transform: rotate(2deg) translateY(6px); }
.pshots.scatter > :nth-child(4n+3) { transform: rotate(-1.5deg) translateY(-4px); }
.pshots.scatter > :nth-child(4n)   { transform: rotate(2.5deg); }
.pshots.scatter a:hover { transform: scale(1.05); }
@media (max-width: 720px) { .pshots.scatter { gap: 12px; } }

/* a picture that's only shown, not clickable (zoom: false in projects.js) */
.pstill { display: block; position: relative; min-width: 0; }
.pshots:not(.scatter) .pstill { border: 1px solid var(--stroke); background: var(--bg); }

/* the live map (basemaps: in projects.js; js/basemaps.js): a button per
   style above it, then the map, filling the picture column. The chosen
   style's name, description and stories show under the panel's text. */
.pmap { width: 100%; justify-self: stretch; }
.styleswitch { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 12px; }
.basemap-wrap { position: relative; }
.basemap {
  width: 100%; aspect-ratio: 4 / 3; max-height: 72vh;
  border: 1px solid var(--stroke); background: var(--bg);
}
.basemap-note {                        /* "Loading…", or a style that isn't uploaded yet */
  position: absolute; inset: 0; margin: 0; padding: 20px;
  display: flex; align-items: center; justify-content: center; text-align: center;
  font-family: var(--mono); font-size: 12px; color: var(--muted);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  border: 1px solid var(--stroke);
}
.basemap-note[hidden] { display: none; }
.basemap-state { display: flex; align-items: center; justify-content: center; color: #333; }
.styleinfo { margin-top: 26px; padding-top: 22px; border-top: 1px solid var(--rule); }
.stylename {
  font-family: var(--display); font-weight: 400; font-size: 1.15rem; line-height: 1.25;
  margin: 0 0 8px;
}
.styleabout { font-size: 15px; line-height: 1.55; margin: 0 0 16px; max-width: var(--measure); }
.inwild {
  margin: 0 0 8px;
  font-family: var(--mono); font-size: var(--label); letter-spacing: var(--track);
  text-transform: uppercase; color: var(--muted);
}

/* credit under a panel's pictures (credit: in projects.js), e.g. "Photos by …" */
.pcredit {
  margin: 8px 0 0;
  font-family: var(--mono); font-size: var(--label); letter-spacing: var(--track);
  text-transform: uppercase; color: var(--muted);
}
/* a picture with a story ({ src, story, label } in projects.js): the picture,
   then its label underneath as a link to the story, styled like the story
   links. .pcap undoes the frame the other picture links get. */
.pshot { margin: 0; min-width: 0; }
.pshots > .pshot:only-child { grid-column: 1 / -1; }
.pshot img { aspect-ratio: auto; height: auto; object-fit: fill; }
.pshot figcaption { margin-top: 8px; font-size: 14.5px; line-height: 1.35; }
/* a caption over its picture (captionAbove: true in projects.js). It takes
   no room of its own: it rises into the space above the row of pictures, so
   the row sits as close under the text and buttons as usual */
.pshot.cap-above { position: relative; }
.pshot.cap-above figcaption { position: absolute; left: 0; right: 0; bottom: 100%; margin: 0 0 8px; }
/* a caption styled like an award line, with its star (captionStar: true) */
.pshot figcaption.starred { font-size: 13.5px; line-height: 1.55; padding-left: 21px; }
.pshot:not(.cap-above) figcaption.starred { position: relative; }   /* holds the star */
.pshot figcaption.starred::before {
  content: "\2605"; position: absolute; left: 0; color: var(--accent); font-size: 12px; line-height: 1.8;
}
.pshots a.pcap {                  /* undoes the picture frame; underline from LINKS above */
  display: inline; overflow: visible; background: none;
  border: 0; border-bottom: 2px solid var(--accent-soft);
}
.pshots a.pcap:hover { transform: none; border-bottom-color: var(--accent); }

/* an .mp4 in images: [] — a looping video, framed like the pictures
   (as a link, it's an <a class="pvid"> and gets the tilt on hover too) */
.pvid { display: block; border: 1px solid var(--stroke); background: var(--bg); }
.pvid video { width: 100%; display: block; }

.panel .close {
  position: absolute; top: 12px; right: 12px; z-index: 3;
  padding: 6px 11px; line-height: 1; cursor: pointer;
  background: var(--bg); border: 1px solid var(--stroke); color: var(--ink);
  font-family: var(--mono); font-size: 11px; letter-spacing: .05em;
}
.panel .close:hover { background: var(--ink); color: var(--plate); }


/* While a detail view is open the rest of the page dims a little: a veil
   over the whole window (js/portfolio.js adds it and toggles .on), with the
   open tile and its panel lifted above it, so the tile's picture, title and
   tags draw at full strength (only the page behind the title is darker).
   The sticky header (z-index 50) stays above it too. Clicks pass straight
   through the veil.
   --dim sets how dark it gets. */
.dimmer {
  --dim: rgba(20, 17, 14, .28);
  position: fixed; inset: 0; z-index: 40;
  background: var(--dim);
  opacity: 0; pointer-events: none;
  transition: opacity .3s ease;
}
.dimmer.on { opacity: 1; }
.gcard[aria-expanded="true"],
.panel:not(.closing) { position: relative; z-index: 41; }


/* -----------------------------------------------------------------------------
   Less motion for visitors who ask for it
   ----------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .gframe, .chip, .cap .ttl, .panel, .pshots a, .btn, .pill, .trio a { transition: none; }
  .dimmer { transition: none; }
  .gcard .cap, .gcard .tags { transition: none; }
  .gcard:hover .gframe { transform: none; box-shadow: var(--shadow-lift); }
  .pshots a:hover, .storycards a:hover .scard-img, .ar,
  .btn:hover .ar, .plinks a:hover .ar, .storylinks a:hover .ar,
  .pshots a.pcap:hover .ar, .storycards a:hover .ar { transform: none; }
}
