/* ════════════════════════════════════════════════════════════
   shoop.photo · Eric Schoep landscape photography
   Gallery white. Light, legible, audit-clean. Text is always on a
   solid surface, never floating over an uncontrolled photograph.
   ════════════════════════════════════════════════════════════ */

/* ── self-hosted variable fonts ─────────────────────────── */
@font-face {
  font-family: "Fraunces";
  src: url("/assets/fonts/fraunces-var.woff2") format("woff2");
  font-weight: 300 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("/assets/fonts/inter-var.woff2") format("woff2");
  font-weight: 400 700; font-style: normal; font-display: swap;
}

:root {
  --paper:    #FAFAF9;   /* museum wall */
  --paper-2:  #F1EFEA;   /* subtle band */
  --paper-3:  #EAE7DF;   /* image placeholder */
  --ink:      #16140F;   /* near-black, 17.6:1 on paper */
  --ink-soft: #54514A;   /* 7.6:1  */
  --ink-dim:  #6E6A60;   /* 5.2:1  */
  --line:     rgba(22,20,15,.14);
  --line-2:   rgba(22,20,15,.08);
  --accent:   #9c6b39;   /* warm bronze, large/decorative + UI only */
  --accent-ink:#7a5325;  /* 6.5:1, safe for accent TEXT */
  --radius:   3px;
  --ease:     cubic-bezier(.22,.61,.36,1);
  --maxw:     1320px;
  --pad:      clamp(1.15rem, 4vw, 3rem);
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: "Inter", system-ui, -apple-system, sans-serif;
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
h1, h2, h3 { font-family: "Fraunces", Georgia, serif; font-weight: 400; line-height: 1.1; letter-spacing: -.011em; color: var(--ink); }
img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

/* visible keyboard focus everywhere (accessibility) */
:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; border-radius: 2px; }

.skip-link {
  position: fixed; top: .6rem; left: .6rem; z-index: 200;
  background: var(--ink); color: var(--paper); padding: .7em 1.1em; border-radius: var(--radius);
  font-size: .82rem; letter-spacing: .04em; transform: translateY(-160%); transition: transform .25s var(--ease);
}
.skip-link:focus { transform: none; }

.visually-hidden {
  position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* ── buttons ────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; gap: .5em;
  font-family: "Inter", sans-serif; font-size: .78rem; font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase;
  padding: .95em 1.7em; border-radius: var(--radius);
  cursor: pointer; border: 1px solid transparent; color: var(--ink);
  transition: transform .28s var(--ease), background .28s var(--ease), color .28s var(--ease), border-color .28s var(--ease);
}
.btn--solid { background: var(--ink); color: var(--paper); }
.btn--solid:hover { background: var(--accent-ink); transform: translateY(-2px); }
.btn--quiet { border-color: var(--line); color: var(--ink-soft); }
.btn--quiet:hover { border-color: var(--accent-ink); color: var(--accent-ink); transform: translateY(-2px); }
/* ── the second, bigger action ────────────────────────────────────────────
   "Add all 4 panels" was btn--quiet: a grey outline in --ink-soft sitting directly under a
   solid black "Add to cart". It read as disabled, and it is the larger order of the two, a
   four-panel set against a single print. Filled in the brand bronze it is unmistakably
   live and still clearly a different action rather than a second primary button competing
   with the first.

   MEASURED, not eyeballed: --paper on --accent-ink is 6.50:1, clearing WCAG AA. Do NOT
   "brighten" it to --accent (#9c6b39), which is the obvious move and measures 4.39:1, a
   FAIL. The two bronzes look nearly identical and one of them is unreadable. */
.btn--accent { background: var(--accent-ink); border-color: var(--accent-ink); color: var(--paper); }
.btn--accent:hover { background: var(--ink); border-color: var(--ink); color: var(--paper); transform: translateY(-2px); }

/* ── header / nav ───────────────────────────────────────── */
.nav {
  position: sticky; top: 0; z-index: 60;
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 1rem var(--pad);
  background: rgba(250,250,249,.86);
  backdrop-filter: blur(10px) saturate(1.1);
  border-bottom: 1px solid var(--line-2);
}
.nav__brand { display: flex; flex-direction: row; align-items: center; gap: .62rem; line-height: 1; }
.nav__brandText { display: flex; flex-direction: column; line-height: 1; }
/* The gaps are STRUCTURAL, not tuned. letter-spacing lands after a letter but not
   between two inline SVGs, so H→lens carried a letter-space and lens→lens carried none:
   measured 6.91px against -0.01px, the two lenses literally touching. No amount of
   margin fixes that, because one uniform value moves the outer gaps together and the
   middle one twice as far. A flex row with one `gap` makes every gap the same by
   construction, and there is a single number to change. */
/* THE WORDMARK, KERNED BY HAND. Blake placed every element in the kerning tool against
   measured INK edges, not layout boxes, which is the distinction that made the earlier
   attempts wrong: equal flex gaps are not equal optical gaps, because each letter carries
   its own side bearings and the lens ink sits inside 13% of padding on each side.
   Every offset is in em, so the kerning holds at any nav size.
   NOTE the lens no longer carries a negative inline margin. The exported margins already
   account for the ink inset, and keeping both would double-count it. */
.nav__mark {
  display: inline-flex; align-items: baseline;
  font-family: "Fraunces", serif; font-weight: 600;
  font-size: 2.4rem;
  letter-spacing: 0;
  color: var(--ink); transition: color .3s var(--ease);
}
.nav__mark > * + * { margin-left: 0.1389em; }
.nav__mark > :nth-child(1) { margin-left: -0.0099em; transform: translateY(-0.0125em); }  /* S */
.nav__mark > :nth-child(2) { margin-left: 0.1389em; transform: translateY(0em); }         /* H */
.nav__mark > :nth-child(3) { margin-left: 0.0571em; transform: translateY(0.2552em); }    /* lens */
.nav__mark > :nth-child(4) { margin-left: -0.1411em; transform: translateY(0.2522em); }   /* lens */
.nav__mark > :nth-child(5) { margin-left: 0.06em; transform: translateY(-0.0148em); }     /* P */

.nav__lens { display: block; width: 1.2em; height: 1.2em; flex: 0 0 auto;
  color: var(--accent); transition: color .3s var(--ease); }
/* the lens the pointer is on lifts; the other stays put, matching mark.js driving them
   one at a time */
.nav__lens:hover { color: var(--accent-ink); }
.nav__brand:hover .nav__mark { color: var(--accent-ink); }

/* ".photo" is a separate WORD, not a sixth letter, so it keeps its own wider gap and is
   deliberately lighter and smaller: a lowercase TLD beside a spaced serif wordmark should
   recede rather than compete. */
.nav__tld {
  font-size: 0.4em; font-weight: 400; letter-spacing: .02em;
  margin-left: 0.1944em; transform: translateY(0em);
  color: var(--ink-dim); transition: color .3s var(--ease);
}
.nav__brand:hover .nav__tld { color: var(--accent-ink); }
.nav__sub  { font-size: .6rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ink-dim); margin-top: .42em; }
.nav__brand:hover .nav__mark { color: var(--accent-ink); }
.nav__links { display: flex; align-items: center; gap: clamp(.9rem, 2.4vw, 2.1rem); }
.nav__links a { font-size: .76rem; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-soft); position: relative; padding: .25em 0; }
.nav__links a::after { content: ""; position: absolute; left: 0; bottom: -.1em; height: 1px; width: 0; background: var(--accent-ink); transition: width .3s var(--ease); }
.nav__links a:hover { color: var(--ink); }
.nav__links a:hover::after { width: 100%; }
.nav__cart { display: inline-flex; align-items: center; gap: .4em; background: none; border: 1px solid var(--line); color: var(--ink-soft);
  font: inherit; font-size: .72rem; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; padding: .45em .7em; border-radius: 99px; cursor: pointer;
  transition: border-color .3s var(--ease), color .3s var(--ease); }
.nav__cart:hover { border-color: var(--accent-ink); color: var(--accent-ink); }
/* The word "Cart" is gone and the trolley says it instead, which gives the hamburger back
   the room it was being crowded out of on a phone. Blake: "remove 'cart' and replace with
   a shopping cart with a counter so the hamburger is viewed." The button carries
   aria-label="Cart" because it no longer has any text of its own to be named by. */
.nav__cartIcon { flex: 0 0 auto; }
.nav__cartCount { background: var(--ink); color: var(--paper); min-width: 1.55em; height: 1.55em; border-radius: 99px; display: inline-grid; place-items: center; font-size: .7rem; }
.nav__cluster { display: flex; align-items: center; gap: clamp(.9rem, 2.4vw, 2.1rem); }
.nav__toggle { display: none; place-items: center; width: 42px; height: 42px; border: 1px solid var(--line); border-radius: 99px; background: none; color: var(--ink); cursor: pointer; transition: border-color .3s var(--ease), color .3s var(--ease); }
.nav__toggle:hover { border-color: var(--accent-ink); color: var(--accent-ink); }
.nav__bars, .nav__bars::before, .nav__bars::after { display: block; width: 18px; height: 1.6px; background: currentColor; border-radius: 2px; transition: transform .3s var(--ease), background .15s var(--ease); }
.nav__bars { position: relative; }
.nav__bars::before { content: ""; position: absolute; left: 0; top: -6px; }
.nav__bars::after  { content: ""; position: absolute; left: 0; top: 6px; }
.nav[data-open="true"] .nav__bars { background: transparent; }
.nav[data-open="true"] .nav__bars::before { transform: translateY(6px) rotate(45deg); }
.nav[data-open="true"] .nav__bars::after  { transform: translateY(-6px) rotate(-45deg); }

/* ── hero: the words and the way in, side by side, and no photograph ─────────
   The hero used to carry a featured photograph and it went through five rejected
   versions before the mechanism came out (see the note in site.js). Blake then made
   the simpler call: "I don't like how much space the whole hero takes now. Lets
   remove the image and put the customizeable search pills next to the header text
   and then let the categories do the talking for photos."
   That is right, and it is what the reference sites do. Burkard's shop opens on
   photographs, not on a banner about photographs. So the hero is now a masthead: who
   this is on the left, the facets on the right, and the first thing anyone SEES is a
   category card carrying a real frame. One screen, no scroll, nothing decorative.
   The facets live here rather than above the grid on purpose: they are the navigation,
   so they belong beside the words rather than as a bar the eye skips. */
.hero { max-width: var(--maxw); margin: 0 auto; padding: clamp(1.8rem,4vh,3rem) var(--pad);
  display: grid; grid-template-columns: minmax(0, 26rem) minmax(0, 1fr);
  gap: clamp(1.6rem, 4vw, 3.5rem); align-items: start; }
.hero__text { min-width: 0; }
.hero__eyebrow { font-size: .68rem; letter-spacing: .26em; text-transform: uppercase; color: var(--accent-ink); margin: 0 0 .8rem; }
.hero__title { font-size: clamp(1.8rem, 2.6vw, 2.4rem); text-wrap: balance; font-weight: 300; margin: 0; letter-spacing: -.02em; }
.hero__lede { color: var(--ink-soft); margin: .9rem 0 0; font-size: .95rem; }
.hero__filters { min-width: 0; }
/* .filters was built as a full-width BAND, so it carries the page's own max-width,
   auto margins and `var(--pad)` side padding. Nested in the hero column those become
   96px of dead space INSIDE the column: measured, the grid resolved to 656px inside a
   752px box, which is why the SUBJECT row dropped "Cities" onto a line of its own while
   there was clearly room beside it. A band's own gutters are wrong once it stops being
   a band. */
.hero__filters .filters { max-width: none; margin: 0; padding: 0; }

/* The disclosure is invisible furniture on a desktop: the toggle is gone and the facets
   just sit there. It only becomes a control once the columns stack. */
.browse__toggle { display: none; }
.browse[open] .filters { display: grid; }

/* Below the split the two columns stack, and the facets go FIRST: on a phone the way
   in matters more than the introduction, which is a paragraph anyone can scroll to. */
@media (max-width: 800px) {
  .hero { grid-template-columns: 1fr; gap: 1rem; }

  /* Header text FIRST, then the way in. The facets used to be ordered above it and Blake
     asked for the swap: the masthead says whose photographs these are, which is the one
     thing a first-time visitor needs before a filter means anything. */
  .browse__toggle { display: flex; align-items: center; justify-content: space-between;
    gap: 1rem; cursor: pointer; list-style: none; width: 100%;
    padding: .8em 1.1em; border: 1px solid var(--ink-dim); border-radius: 999px;
    font-size: .74rem; letter-spacing: .16em; text-transform: uppercase;
    transition: border-color .25s var(--ease), background .25s var(--ease); }
  .browse__toggle::-webkit-details-marker { display: none; }
  /* The chevron is drawn here rather than shipped as an asset: it is two lines. */
  .browse__toggle::after { content: ""; width: .5em; height: .5em; flex: 0 0 auto;
    border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
    transform: translateY(-.15em) rotate(45deg); transition: transform .25s var(--ease); }
  .browse[open] .browse__toggle::after { transform: translateY(.1em) rotate(225deg); }
  .browse__toggle:hover { border-color: var(--accent-ink); }
  .browse[open] .browse__toggle { background: var(--ink); color: var(--paper); border-color: var(--ink); }
  .browse[open] .filters { padding-top: 1.1rem; }

  /* ONE FACET PER BLOCK. At 5.2rem the label sat in a fixed column beside the pills and
     squeezed them into a two-pill-wide gutter that wrapped raggedly. The label goes on its
     own line, so each facet reads as a labelled group rather than a run of pills. */
  /* EVERY PILL IN A FACET SHARES ONE LEFT EDGE, wrapped ones included. As a flex row the
     first line started after the label and the overflow fell back to the page gutter, so
     "Wildlife, Landmarks, Cities" read as a loose row of its own rather than the tail of
     SUBJECT. Blake: "move those bottom pills over so the section stands alone." Taking the
     label out of flow and indenting the row by its width gives the group one column.
     SCOPED TO .hero__filters ON PURPOSE, and not for tidiness: the base .filters rules are
     defined LATER in this stylesheet, so at equal specificity they win on source order and
     an unscoped override silently does nothing. */
  .hero__filters .filters__row { display: block; position: relative; padding-left: 5.6rem; }
  .hero__filters .filters__lbl { position: absolute; left: 0; top: .55em; width: 5.2rem; }
  .hero__filters .filters__row .chip { margin: 0 .35rem .35rem 0; }
  .hero__filters .filters__q { display: block; width: 100%; flex: none; }
  .hero__filters .filters__count { display: block; margin-top: .5rem; }
}

/* ── section heads ──────────────────────────────────────── */
.section-head { max-width: var(--maxw); margin: 0 auto; padding: clamp(4rem,9vh,6.5rem) var(--pad) 2.2rem; }
.section-head h2 { font-size: clamp(2rem,4.2vw,3rem); font-weight: 300; margin: 0 0 .45rem; }
.section-head p { color: var(--ink-dim); margin: 0; }

/* ── gallery grid ───────────────────────────────────────── */
.lb__shot {
  margin: .45rem 0 0; font-size: .68rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-dim); min-height: 1em;
}

/* ── gallery filters: subject, product type, and search ────────────────────
   Chips are rendered from the catalog, so a new category or product appears here
   with no CSS or markup change. */
.filters { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--pad) 2.4rem; display: grid; gap: .7rem; }
.filters__row { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; }
.filters__lbl {
  flex: 0 0 auto; width: 5.2rem; font-size: .68rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-dim);
}
.chip {
  font: inherit; font-size: .78rem; letter-spacing: .02em; cursor: pointer;
  padding: .4em .95em; border-radius: 999px; color: var(--ink-soft);
  background: transparent; border: 1px solid currentColor;
  transition: color .25s var(--ease), background .25s var(--ease), border-color .25s var(--ease);
}
.chip:hover { color: var(--accent-ink); }
.chip.is-on { color: var(--bg, #10100f); background: var(--accent-ink); border-color: var(--accent-ink); }
.chip.is-on:hover { filter: brightness(1.08); }
.filters__q {
  font: inherit; font-size: .82rem; flex: 1 1 14rem; min-width: 0;
  padding: .45em .8em; border-radius: 999px; color: var(--ink);
  background: transparent; border: 1px solid var(--ink-dim);
  transition: border-color .25s var(--ease);
}
.filters__q:hover, .filters__q:focus-visible { border-color: var(--accent-ink); outline: none; }
.filters__count { flex: 0 0 auto; font-size: .72rem; color: var(--ink-dim); }
.gallery__none { max-width: var(--maxw); margin: 0 auto; padding: 2rem var(--pad) 5rem; color: var(--ink-dim); }
.linkish {
  font: inherit; color: var(--accent-ink); background: none; border: 0; padding: 0;
  cursor: pointer; text-decoration: underline; text-underline-offset: .2em;
}
.linkish:hover { filter: brightness(1.15); }

/* ── a panel set renders as ONE row ────────────────────────────────────────
   Panels deliberately do NOT use `.grid`: its nth-child span rules give each tile
   a different width, which is what made a four-panel set read as four unrelated
   photographs sitting near each other. On narrow screens the row scrolls sideways
   rather than wrapping, because wrapping a panorama breaks its left-to-right read. */
.setblock { max-width: var(--maxw); margin: 0 auto 2.4rem; padding: 0 var(--pad); }
.setblock__head {
  display: flex; align-items: baseline; gap: .7rem; margin: 0 0 .7rem;
  font-family: "Fraunces", serif; font-weight: 400; font-size: 1rem;
  letter-spacing: .03em; color: var(--ink-soft);
  transition: color .3s var(--ease);
}
.setblock__head:hover { color: var(--accent-ink); }
.setblock__meta { font-family: inherit; font-size: .68rem; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-dim); }
/* N equal columns for however many panels the set has, with NO inline style:
   the CSP is style-src 'self' plus a hash, and a CSP HASH DOES NOT APPLY TO A
   style ATTRIBUTE, so `style="--panels:4"` was silently blocked in production and
   every set fell back to 3 columns. The local dev server sends no _headers, so
   this could only ever be caught on the deployed site. */
.setgrid { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem;
           grid-auto-flow: column; grid-auto-columns: 1fr; }
.setgrid > li { margin: 0; min-width: 0; }
@media (max-width: 880px) {
  .setgrid { grid-auto-flow: column; grid-template-columns: none; grid-auto-columns: minmax(45vw, 1fr); overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: .5rem; }
  .setgrid > li { scroll-snap-align: start; }
}

/* Category sections inside #galleries. Each carries its own head so the collection
   reads as grouped work rather than one undifferentiated wall of tiles. */
.gallery__cat { margin: 0 0 clamp(2.5rem, 6vh, 4.5rem); }
.gallery__catHead {
  max-width: var(--maxw); margin: 0 auto; padding: 0 var(--pad) 1rem;
  font-family: "Fraunces", serif; font-weight: 400;
  font-size: clamp(1.15rem, 2.1vw, 1.6rem); letter-spacing: .04em;
  color: var(--ink-soft); transition: color .3s var(--ease);
}
.gallery__catHead:hover { color: var(--accent-ink); }

/* A JUSTIFIED GALLERY, THE LAYOUT EVERY PRINT SITE USES.
   Photographs keep their own aspect and are never cropped; rows fill the width
   and their heights vary. flex-grow AND flex-basis both track the frame's aspect,
   so a wrapping row distributes leftover width while every frame holds its shape.
   The .jfill items are zero-height spacers that stop a short last row from
   stretching two frames across the whole page. */
.jgal { list-style: none; margin: 0 auto; padding: 0 var(--pad) clamp(3rem,8vh,6rem);
  max-width: var(--maxw); display: flex; flex-wrap: wrap; gap: clamp(.5rem,1vw,.85rem); }
/* THE ASPECT IS A CLASS, NOT A CUSTOM PROPERTY SET BY SCRIPT. The generated pages carry
   no site.js, so anything applied through CSSOM never reached them and their rows simply
   did not fill. The catalog holds exactly four shapes, so four classes cover it and the
   layout works with no JavaScript at all, which is the right dependency for a page whose
   whole job is showing photographs. */
.jitem { flex-grow: var(--a, 1.5); flex-basis: calc(var(--a, 1.5) * 15rem); min-width: 0; }
.jitem--4x3 { --a: 1.3333; }
.jitem--3x2 { --a: 1.5; }
.jitem--1x1 { --a: 1; }
.jitem--3x4 { --a: 0.75; }
.jfill { flex-grow: 1e4; flex-basis: 0; height: 0; margin: 0; padding: 0; }
.jtile { position: relative; display: block; width: 100%; aspect-ratio: var(--a, 1.5);
  overflow: hidden; border-radius: var(--radius); background: var(--paper-3); }
.jtile img, .jtile picture { width: 100%; height: 100%; object-fit: cover; display: block; }
.jtile img { transition: transform 1s var(--ease); }
.jtile:hover img { transform: scale(1.035); }
/* The caption stays OFF the photograph until you ask for it, which is how these sites
   present work: nothing between the viewer and the picture. */
.jtile__cap { position: absolute; inset: auto 0 0 0; display: flex; align-items: baseline;
  justify-content: space-between; gap: .8rem; padding: 2.2rem .9rem .75rem;
  background: linear-gradient(to top, rgba(20,18,13,.82) 0%, rgba(20,18,13,0) 100%);
  opacity: 0; transition: opacity .35s var(--ease); }
.jtile:hover .jtile__cap, .jtile:focus-visible .jtile__cap { opacity: 1; }
.jtile__t { font-family: "Fraunces", Georgia, serif; font-size: 1rem; color: #fff; }
.jtile__p { font-size: .66rem; letter-spacing: .14em; text-transform: uppercase; color: #f0d9b6; white-space: nowrap; }

.tile {
  display: block; width: 100%; padding: 0; border: 0; background: var(--paper-3); cursor: pointer;
  position: relative; overflow: hidden; border-radius: var(--radius); aspect-ratio: 3/2;
  opacity: 0; transform: translateY(22px); transition: opacity .8s var(--ease), transform .8s var(--ease);
}
.tile.is-in { opacity: 1; transform: none; }
/* Tiles inside an opened subject appear AT ONCE. They fade in on the way down the page,
   which is right for browsing, and wrong here: the panel opens and scrolls to itself, so
   the reveal had already been scrolled past and the tiles sat invisible, holding their
   grid space. That is the "bunch of blank space". */
.catopen .tile { opacity: 1; transform: none; }
.tile img { width: 100%; height: 100%; object-fit: cover; transition: transform 1s var(--ease); }
.tile:hover img { transform: scale(1.045); }
.tile__veil {
  position: absolute; inset: auto 0 0 0; display: flex; flex-direction: column; gap: .1rem;
  padding: 2.4rem 1.2rem 1.05rem; text-align: left;
  background: linear-gradient(to top, rgba(20,18,13,.86) 0%, rgba(20,18,13,.5) 45%, rgba(20,18,13,0) 100%);
  opacity: 0; transform: translateY(8px); transition: opacity .45s var(--ease), transform .45s var(--ease);
}
.tile:hover .tile__veil, .tile:focus-visible .tile__veil { opacity: 1; transform: none; }
.tile__title { font-family: "Fraunces", serif; font-size: 1.2rem; color: #fff; }
.tile__place { font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; color: rgba(255,255,255,.85); }
.tile__cta { margin-top: .45rem; font-size: .66rem; letter-spacing: .18em; text-transform: uppercase; color: #f0d9b6; }

/* ── prints / value band ────────────────────────────────── */
.prints { background: var(--paper-2); border-block: 1px solid var(--line-2); }
.prints__inner { max-width: var(--maxw); margin: 0 auto; padding: clamp(4rem,8vh,6rem) var(--pad); }
.prints__inner > h2 { font-size: clamp(1.9rem,4vw,2.9rem); font-weight: 300; margin: 0 0 2.4rem; max-width: 18ch; }
.prints__row { list-style: none; margin: 0; padding: 0; display: grid; gap: clamp(1.4rem,3vw,2.6rem); grid-template-columns: repeat(3, 1fr); }
.feature { border-top: 1px solid var(--line); padding-top: 1.1rem; }
.feature h3 { font-size: 1.28rem; margin: 0 0 .45rem; }
.feature p { color: var(--ink-soft); margin: 0; font-size: .98rem; }

/* ── about (split) ──────────────────────────────────────── */
.about__inner { max-width: var(--maxw); margin: 0 auto; padding: clamp(4rem,8vh,6rem) var(--pad); display: grid; grid-template-columns: .9fr 1.1fr; gap: clamp(2rem,5vw,4.5rem); align-items: center; }
.about__media img { width: 100%; height: auto; border-radius: var(--radius); }
.about__text h2 { font-size: clamp(2rem,4vw,2.9rem); font-weight: 300; margin: 0 0 1.3rem; }
.about__text p { color: var(--ink-soft); margin: 0 0 1.1rem; }
.about__text .btn { margin-top: .9rem; }

/* ── footer ─────────────────────────────────────────────── */
.foot { border-top: 1px solid var(--line); }
.foot__inner { max-width: var(--maxw); margin: 0 auto; padding: 2.6rem var(--pad); display: flex; flex-wrap: wrap; align-items: center; gap: 1.1rem 2rem; }
.foot__brand { font-family: "Fraunces", serif; letter-spacing: .26em; font-size: 1.02rem; }
.foot__links { display: flex; gap: 1.4rem; flex: 1; flex-wrap: wrap; }
.foot__links a { font-size: .76rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft); }
.foot__links a:hover { color: var(--accent-ink); }
.foot__fine { width: 100%; color: var(--ink-dim); font-size: .76rem; margin: 0; }

/* ── lightbox dialog ────────────────────────────────────── */
.lb:not([open]) { display: none; }
.lb { width: min(1180px, 94vw); max-width: 94vw; max-height: 94vh; padding: 0; border: none; border-radius: var(--radius);
  background: var(--paper); color: var(--ink); overflow: visible; }
.lb::backdrop { background: rgba(20,18,13,.74); backdrop-filter: blur(3px); }
.lb__stage { display: grid; grid-template-columns: minmax(0,1.55fr) minmax(280px,.85fr); gap: clamp(1rem,2.4vw,2rem); padding: clamp(1rem,2.4vw,1.8rem); align-items: center; }
.lb__figure { margin: 0; min-width: 0; }
.lb__figure img { width: 100%; height: auto; max-height: 72vh; object-fit: contain; border-radius: var(--radius); background: var(--paper-3); }
.lb__cap { padding-top: .9rem; }
.lb__cap h2 { font-size: 1.55rem; font-weight: 400; margin: 0 0 .2rem; }
.lb__place { margin: 0; color: var(--ink-dim); font-size: .74rem; letter-spacing: .12em; text-transform: uppercase; }
.lb__blurb { margin: .6rem 0 0; color: var(--ink-soft); font-size: .98rem; }
.lb__close { position: absolute; top: -.5rem; right: -.5rem; transform: translate(40%,-40%); display: grid; place-items: center;
  width: 42px; height: 42px; border-radius: 99px; background: var(--ink); color: var(--paper); border: none; cursor: pointer; transition: background .3s var(--ease), transform .3s var(--ease); }
.lb__close:hover { background: var(--accent-ink); transform: translate(40%,-40%) rotate(90deg); }
.lb__nav { position: absolute; top: 50%; translate: 0 -50%; display: grid; place-items: center; width: 46px; height: 46px;
  border-radius: 99px; border: 1px solid var(--line); background: rgba(250,250,249,.9); color: var(--ink); cursor: pointer; transition: border-color .3s var(--ease), color .3s var(--ease), transform .3s var(--ease); }
.lb__nav:hover { border-color: var(--accent-ink); color: var(--accent-ink); }
.lb__nav--prev { left: -23px; } .lb__nav--prev:hover { transform: translateY(-50%) translateX(-3px); }
.lb__nav--next { right: -23px; } .lb__nav--next:hover { transform: translateY(-50%) translateX(3px); }

.buy { background: var(--paper-2); border: 1px solid var(--line-2); border-radius: var(--radius); padding: 1.4rem; }
.buy__from { margin: 0 0 1.1rem; font-size: .76rem; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-dim); }
.buy__from strong { color: var(--ink); }
.buy__field { margin-bottom: .95rem; }
.buy__field label { display: block; font-size: .66rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-dim); margin-bottom: .4rem; }
.buy__field select {
  width: 100%; background: var(--paper); color: var(--ink); border: 1px solid var(--line); border-radius: var(--radius);
  padding: .75em .9em; font: inherit; font-size: .95rem; cursor: pointer; appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-soft) 50%), linear-gradient(135deg, var(--ink-soft) 50%, transparent 50%);
  background-position: calc(100% - 18px) 1.15em, calc(100% - 13px) 1.15em; background-size: 5px 5px; background-repeat: no-repeat;
  transition: border-color .25s var(--ease);
}
.buy__field select:hover, .buy__field select:focus-visible { border-color: var(--accent-ink); }
.buy__matblurb { margin: -.3rem 0 1rem; font-size: .82rem; color: var(--ink-dim); min-height: 1.1em; }
.buy__price { display: flex; justify-content: space-between; align-items: baseline; border-top: 1px solid var(--line); padding-top: .95rem; margin-bottom: 1.1rem; }
.buy__price span { font-size: .7rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-dim); }
.buy__price strong { font-family: "Fraunces", serif; font-size: 1.7rem; font-weight: 400; }
.buy__add { width: 100%; justify-content: center; }
.buy__note { font-size: .76rem; color: var(--accent-ink); margin: .85rem 0 0; text-align: center; min-height: 1.1em; }

/* ── cart dialog drawer ─────────────────────────────────── */
/* A CLOSED DIALOG MUST STAY CLOSED. `display: flex` here overrode the browser's own
   `dialog:not([open]) { display: none }`, so the drawer rendered INLINE at the bottom of
   every page, pushing a screen of empty space under the footer. Same shape as the
   [hidden] trap: an explicit display beats an implicit hide. The flex layout is still
   wanted, so it is scoped to the open state instead of thrown away. */
.cart:not([open]) { display: none; }
.cart[open] { display: flex; flex-direction: column; }
.cart { margin: 0 0 0 auto; height: 100vh; max-height: 100vh; width: min(420px, 94vw); max-width: 94vw;
  padding: 0; border: none; background: var(--paper); color: var(--ink); }
.cart::backdrop { background: rgba(20,18,13,.5); backdrop-filter: blur(2px); }
.cart__head { display: flex; justify-content: space-between; align-items: center; padding: 1.3rem 1.4rem; border-bottom: 1px solid var(--line); }
.cart__head h2 { margin: 0; font-size: 1.35rem; font-weight: 400; }
.cart__close { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 99px; background: none; border: 1px solid var(--line); color: var(--ink); cursor: pointer; transition: border-color .3s, color .3s, transform .3s; }
.cart__close:hover { border-color: var(--accent-ink); color: var(--accent-ink); transform: rotate(90deg); }
.cart__items { flex: 1; overflow-y: auto; padding: .8rem 1.4rem; }
.cart__empty { color: var(--ink-dim); text-align: center; padding: 3rem 1rem; font-size: .94rem; }
.ci { display: grid; grid-template-columns: 60px 1fr auto; gap: .9rem; align-items: start; padding: .9rem 0; border-bottom: 1px solid var(--line-2); }
.ci__thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; }
.ci__name { font-size: .92rem; }
.ci__meta { font-size: .74rem; color: var(--ink-dim); margin-top: .1rem; }
.ci__price { font-family: "Fraunces", serif; font-size: 1.02rem; }
.ci__rm { background: none; border: none; color: var(--ink-dim); cursor: pointer; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; padding: 0; margin-top: .3rem; transition: color .25s; }
.ci__rm:hover { color: var(--accent-ink); }
.cart__foot { padding: 1.3rem 1.4rem; border-top: 1px solid var(--line); }
.cart__total { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: .95rem; }
.cart__total span { font-size: .72rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-dim); }
.cart__total strong { font-family: "Fraunces", serif; font-size: 1.55rem; font-weight: 400; }
.cart__checkout { width: 100%; justify-content: center; }
.cart__checkout:disabled { opacity: .45; cursor: not-allowed; }
.cart__note { font-size: .76rem; color: var(--accent-ink); text-align: center; margin: .8rem 0 0; min-height: 1.1em; }

/* ── order confirmation + license downloads ── */
.dl:not([open]) { display: none; }
.dl { position: relative; margin: auto; width: min(560px, 94vw); max-width: 94vw; padding: 2rem 1.9rem 1.9rem;
  border: none; border-radius: 6px; background: var(--paper); color: var(--ink); }
.dl::backdrop { background: rgba(20,18,13,.5); backdrop-filter: blur(2px); }
.dl .cart__close { position: absolute; top: 1rem; right: 1rem; }
.dl__title { margin: 0 2rem .5rem 0; font-size: 1.5rem; font-weight: 400; }
.dl__note { color: var(--ink-dim); font-size: .92rem; margin: 0 0 1.1rem; }
.dl__list { display: flex; flex-direction: column; gap: .7rem; }
.dl__item { display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: center;
  padding: .9rem 1rem; border: 1px solid var(--line); border-radius: 5px; }
.dl__name { font-size: .98rem; }
.dl__terms { font-size: .74rem; color: var(--ink-dim); margin-top: .2rem; }
.dl__get { white-space: nowrap; }

/* ── responsive ─────────────────────────────────────────── */
@media (max-width: 720px) {
  .nav__toggle { display: grid; }
  .nav__links {
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    padding: .2rem var(--pad) .9rem;
    background: var(--paper); border-bottom: 1px solid var(--line);
    box-shadow: 0 18px 30px -22px rgba(22,20,15,.55);
    opacity: 0; visibility: hidden; pointer-events: none; transform: translateY(-8px);
    transition: opacity .26s var(--ease), transform .26s var(--ease), visibility 0s linear .26s;
  }
  .nav[data-open="true"] .nav__links {
    opacity: 1; visibility: visible; pointer-events: auto; transform: none;
    transition: opacity .26s var(--ease), transform .26s var(--ease);
  }
  .nav__links a { padding: .95rem .15rem; border-bottom: 1px solid var(--line-2); }
  .nav__links a:last-child { border-bottom: 0; }
  .nav__links a::after { display: none; }
}

@media (max-width: 880px) {
  .nav__sub { display: none; }
  .hero { grid-template-columns: 1fr; min-height: 0; }
  .lb__stage { grid-template-columns: 1fr; }
  .lb__figure img { max-height: 48vh; }
  .lb__nav { display: none; }
  .prints__row { grid-template-columns: 1fr; }
  .about__inner { grid-template-columns: 1fr; }
  .about__media { max-width: 420px; }
}
@media (min-width: 881px) and (max-width: 1080px) { .nav__sub { display: none; } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
  html { scroll-behavior: auto; }
  .tile { opacity: 1; transform: none; }
}

/* ── footer social icons ──────────────────────────────────────────────────
   Generated into index.html by oye-social-engine; these are the host styles.
   The page used to hard-code ONE Instagram text link pointing at a handle the
   source did not recognise.

   Colours follow this file's own token annotations: --ink-soft is marked 7.6:1
   and --accent-ink 6.5:1 "safe for accent TEXT". Nothing new was invented. */
.foot__social { display: flex; gap: .55rem; align-items: center; }
.foot__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 50%;
  border: 1px solid var(--line); color: var(--ink-soft);
  transition: color .18s var(--ease), border-color .18s var(--ease), transform .18s var(--ease);
}
.foot__icon svg { width: 15px; height: 15px; fill: currentColor; display: block; }
.foot__icon:hover { color: var(--accent-ink); border-color: var(--accent-ink); transform: translateY(-1px); }

/* ── About Eric ───────────────────────────────────────────────────────────
   These replace a single "Follow @shoop.photo" button that linked to Instagram. With seven
   accounts now on file, that button was one of them wearing the name of all of them. Larger
   than the footer set because this is a call to action rather than a colophon. */
.about__social { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin-top: .4rem; }
.about__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 50%;
  border: 1px solid var(--line); color: var(--ink-soft);
  transition: color .18s var(--ease), border-color .18s var(--ease),
              background .18s var(--ease), transform .18s var(--ease);
}
.about__icon svg { width: 19px; height: 19px; fill: currentColor; display: block; }
.about__icon:hover { color: var(--paper); background: var(--accent-ink);
  border-color: var(--accent-ink); transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) { .about__icon:hover { transform: none; } }

/* ── 404 ──────────────────────────────────────────────────────────────────
   404.html LINKS this stylesheet rather than inlining its own copy. That is
   deliberate on two counts: style-src is 'self' plus a hash, and 'self' already
   permits a same-origin stylesheet, so the 404 needs no hash of its own; and
   build.mjs rewrites the WHOLE style-src directive with exactly one token on
   every deploy, so a second hash would be wiped the next time anyone shipped. */
.nf__code { margin: 0 0 1rem; font-size: .72rem; letter-spacing: .26em; text-transform: uppercase; color: var(--ink-dim); }
.nf__title { font-family: "Fraunces", Georgia, serif; font-weight: 400; font-size: clamp(2rem, 5.4vw, 3.4rem); line-height: 1.12; margin: 0 0 1rem; text-wrap: balance; }
.nf__lede { margin: 0 0 2.2rem; max-width: 46ch; color: var(--ink-soft); font-size: 1.02rem; }
.nf__main { flex: 1 0 auto; width: 100%; max-width: var(--maxw); margin: 0 auto; padding: clamp(4rem, 12vh, 9rem) var(--pad) 5rem; display: flex; flex-direction: column; justify-content: center; }
.nf__back { display: inline-flex; align-items: center; align-self: flex-start; min-height: 44px; padding: 0 1.4rem; border-radius: var(--radius); background: var(--ink); color: var(--paper); text-decoration: none; font-size: .78rem; letter-spacing: .1em; text-transform: uppercase; transition: opacity .18s var(--ease); }
.nf__back:hover { opacity: .86; }
body.nf { min-height: 100vh; display: flex; flex-direction: column; }

@media (prefers-reduced-motion: reduce) {
  .foot__icon, .nf__back { transition: none; }
  .foot__icon:hover { transform: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   PRODUCT, CATEGORY AND SET PAGES (generated by _private/build-pages.mjs)
   These pages LINK this stylesheet rather than inlining it, for the same reason
   404.html does: style-src is 'self' plus exactly ONE sha256 that build.mjs
   rewrites on every deploy, so a second inlined block would be wiped or blocked.
   ══════════════════════════════════════════════════════════════════════════ */
.crumb { max-width: var(--maxw); margin: 0 auto; padding: 1.6rem var(--pad) 0;
  font-size: .74rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-dim); }
.crumb a { color: var(--ink-soft); }
.crumb a:hover { color: var(--accent-ink); }
.crumb span { margin: 0 .45em; }

.pwrap { max-width: var(--maxw); margin: 0 auto; padding: 1.4rem var(--pad) clamp(3rem,7vh,5rem);
  display: grid; grid-template-columns: minmax(0,1.6fr) minmax(300px,.85fr); gap: clamp(1.5rem,3.5vw,3.2rem); align-items: start; }

/* ── THE THUMBNAIL RAIL SITS LEFT OF THE STAGE ────────────────────────────
   Blake, 2026-08-13: "adjust product pages to put mock ups to the left or right of the
   photo like amazon. The photos are different sizes so when a mockup is selected it pushes
   the rest of the mock up preview links down."

   He is describing a measured fact: the stage used to be `height: auto`, so its height came
   from whichever image was selected, and the thumbs sat under it in normal flow. Picking a
   taller mockup grew the stage and shoved the whole strip down the page. A rail anchored at
   the top of the grid cannot move when the stage changes, and the fixed stage below means
   the stage does not change either.

   DOM ORDER IS UNTOUCHED (stage, label, rail) and the rail is placed by grid area, so a
   screen reader still meets the photograph before the list of alternate views. */
.pgallery { display: grid; grid-template-columns: 64px minmax(0,1fr);
  grid-template-areas: "rail stage" "rail label"; gap: .5rem .85rem; align-items: start; }

/* ── THE STAGE IS SQUARE, AND THAT IS A MEASUREMENT ───────────────────────
   Every Printify mockup is 1200x1200, exactly square, across all 12 camera angles, so a
   square stage fits every one of them with no dead space at all. The photographs span 4:3
   (29 works), 3:2 (14), 3:4 (12) and 1:1 (7), a range of 0.750 to 1.500 whose least-worst
   container is 1.06, which is square to within a rounding error. One ratio on all 62 pages
   is what makes them uniform, and a fixed box is what stops the buy button sliding away
   while somebody is deciding.

   CONTAIN, NEVER COVER: cropping a photograph to fit the furniture is the one thing this
   site does not do. The ground is --paper-2 ON PURPOSE, matching the wall in the panel-set
   composites exactly, so a contained strip reads as floating on a wall rather than as bars. */
.pgallery__main { grid-area: stage; background: var(--paper-2); border-radius: var(--radius);
  overflow: hidden; aspect-ratio: 1 / 1; display: grid; place-items: center; }
.pgallery__main picture { display: block; width: 100%; height: 100%; }
.pgallery__main img { width: 100%; height: 100%; object-fit: contain; display: block; }
.pgallery__label { grid-area: label; margin: .35rem 0 0; font-size: .68rem; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink-dim); min-height: 1em; }
/* ⚠️ NO SCROLLBAR, AND NO CAP EITHER. Blake, 2026-08-13: "extend the window for the mockups,
   there's no reason for scroll bars."

   The first attempt capped the rail at a fixed 424px and scrolled, because `max-height: 100%`
   does nothing here (a grid row sized BY its content is indefinite, so the percentage is
   dropped) and an uncapped rail of 10 thumbs became the tallest thing in the row, setting the
   row height and stretching the grid past the stage.

   This solves both instead of trading one for the other. The thumbs are FLEXIBLE, `flex: 1 1 0`
   with `min-height: 0`, so their min-content contribution is zero and they no longer force the
   row height: the square stage decides it, and the rail stretches to match and divides that
   height between however many shots the page has. Two shots or ten, they all fit and nothing
   scrolls. max-height keeps a short rail from turning three thumbs into three slabs. */
.pgallery__thumbs { grid-area: rail; display: flex; flex-direction: column; flex-wrap: nowrap;
  gap: .5rem; align-self: stretch; min-height: 0; }
.pthumb { width: 64px; flex: 1 1 0; min-height: 0; max-height: 84px;
  padding: 0; border: 1px solid var(--line);
  background: var(--paper-2);
  border-radius: var(--radius); overflow: hidden; cursor: pointer; opacity: .68;
  transition: opacity .25s var(--ease), border-color .25s var(--ease), transform .25s var(--ease); }
/* Contained here too, because the first thumb on a panel page is the assembled wall and a
   wall strip is up to 8:1. Cover would show a meaningless slice of its middle. */
.pthumb img { width: 100%; height: 100%; object-fit: contain; }
.pthumb:hover { opacity: 1; transform: translateY(-2px); border-color: var(--accent-ink); }
.pthumb.is-on { opacity: 1; border-color: var(--accent-ink); }

.pbuy { background: var(--paper-2); border: 1px solid var(--line-2); border-radius: var(--radius);
  padding: clamp(1.2rem,2vw,1.7rem); position: sticky; top: 5.5rem; }
.pbuy__title { font-family: "Fraunces", Georgia, serif; font-weight: 400; font-size: clamp(1.6rem,3vw,2.1rem);
  line-height: 1.12; margin: 0 0 .3rem; }
.pbuy__place { margin: 0 0 .9rem; font-size: .72rem; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-dim); }
.pbuy__blurb { margin: 0 0 1.3rem; color: var(--ink-soft); font-size: .98rem; }

.pset { margin-top: 1.4rem; padding-top: 1.2rem; border-top: 1px solid var(--line); }
.pset__head { margin: 0 0 .4rem; font-size: .88rem; color: var(--ink-soft); }
/* The assembled set, in the buy panel, because "Part of Bixby Bridge, a set of 4" is a
   sentence and the thing being sold is a picture. Same paper-2 wall as the stage. */
.pset__wall { display: block; margin: .1rem 0 .8rem; border-radius: var(--radius);
  overflow: hidden; background: var(--paper-2); }
.pset__wall picture, .pset__wall img { display: block; width: 100%; height: auto; }
.pset__wall:hover { box-shadow: 0 6px 18px -12px rgba(22,20,15,.8); }
.pset__body { margin: 0 0 .9rem; font-size: .86rem; color: var(--ink-dim); }
.pset__add { width: 100%; justify-content: center; }
.pset__link { display: inline-block; margin-top: .8rem; font-size: .78rem; letter-spacing: .06em;
  color: var(--accent-ink); text-decoration: underline; text-underline-offset: .2em; }
.pset__link:hover { filter: brightness(1.15); }

.precs { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--pad) clamp(3rem,8vh,6rem); }
.precs__head { font-family: "Fraunces", Georgia, serif; font-weight: 400; font-size: clamp(1.3rem,2.4vw,1.8rem);
  margin: 0 0 1.3rem; border-top: 1px solid var(--line); padding-top: 2rem; }
/* ── YOU MIGHT ALSO LIKE ──────────────────────────────────────────────────
   A FIXED column count per breakpoint, not auto-fill. There are always 6 picks, and 6
   divides exactly by 6, 3 and 2, so the last row is full at every width. auto-fill resolved
   to 4 or 5 columns on common screens and orphaned one card beside a hole, which is the
   "adds empty space" Blake photographed. Nothing is hidden to achieve this; all 6 still show.

   align-items:start matters: each card is now its photograph's own shape, so a row holds
   cards of different heights and stretching them would put the crop back. */
.precs__row { list-style: none; margin: 0; padding: 0; display: grid; gap: clamp(.8rem,1.6vw,1.3rem);
  grid-template-columns: repeat(6, minmax(0, 1fr)); align-items: start; }
.prec { display: block; }
/* The media box clips the hover zoom and hosts the product preview overlay. Without it the
   scaled image bleeds over the title underneath. */
.prec__media { position: relative; display: block; overflow: hidden;
  border-radius: var(--radius); background: var(--paper-3); }
/* ⚠️ NO aspect-ratio AND NO HEIGHT HERE, deliberately. product.js states each photograph's
   real intrinsic width/height on the <img>, so height:auto gives every card its own shape
   while the attributes still reserve the right box. It used to hardcode 600x450, and an
   HTML height attribute is a presentational hint that this rule never overrode, so that 450
   beat `aspect-ratio: 4/3` and every card rendered 265x450 whatever the picture was. */
.prec img { width: 100%; height: auto; display: block;
  transition: transform .6s var(--ease); }
.prec:hover img { transform: scale(1.03); }
.prec__t { display: block; margin-top: .5rem; font-family: "Fraunces", Georgia, serif; font-size: 1rem; }
.prec__p { display: block; font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-dim); }
.prec:hover .prec__t { color: var(--accent-ink); }

/* A tile that is a real link. Used on the home gallery AND on the generated pages.
   It does NOT force opacity, because the home page fades its tiles in on scroll and
   flattening that would quietly delete an effect nobody asked to lose. The generated
   pages load no site.js and so have no observer to add .is-in, which is what
   .tile--static is for: they are visible from the start. */
.tile--link { display: block; position: relative; overflow: hidden; border-radius: var(--radius);
  background: var(--paper-3); aspect-ratio: 3/2; }
.tile--static { opacity: 1; transform: none; }
.tile--link img { width: 100%; height: 100%; object-fit: cover; transition: transform 1s var(--ease); }
.tile--link:hover img { transform: scale(1.045); }

/* ── the product preview that fades in on hover (hover.js) ────────────────
   CONTAIN, and that is Blake's explicit call. A mockup is square while a tile is 3:2 or
   4:3, so covering would crop the frame edge off the very product the preview exists to
   show. Contained on the paper ground it is white space, not bars. The ground is opaque so
   the photograph underneath does not ghost through the mockup's own white background.

   ⚠️ SELECTOR IS `img.hoverpv`, NOT `.hoverpv`, and that is load-bearing. The overlay is an
   <img> inside a tile, so it is matched by `.jtile img` and `.prec img`, both of which score
   0-1-1 against a bare class at 0-1-0 and win. Measured in the browser: the overlay rendered
   `object-fit: cover` while this rule plainly said contain, which crops the frame edge off
   the exact product the preview exists to show. It failed silently and looked fine, because
   a square mockup cropped to a 4:3 tile is still a picture. Element+class ties those rules
   and this block sits later in the file, so it wins on source order. Do not "tidy" it back
   to a bare class. */
img.hoverpv { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
  background: var(--paper-2); border-radius: inherit;
  opacity: 0; transition: opacity .32s var(--ease); pointer-events: none; z-index: 3; }
img.hoverpv.is-on { opacity: 1; }
@media (prefers-reduced-motion: reduce) { img.hoverpv { transition: none; } }
/* The set page leads with the assembled wall, which is the product a visitor is actually
   deciding on. The individual panels follow underneath. */
.setwall { max-width: var(--maxw); margin: 0 auto 1.6rem; padding: 0 var(--pad); }
.setwall picture, .setwall img { display: block; width: 100%; height: auto;
  border-radius: var(--radius); background: var(--paper-2); }
.setwall__cap { margin: .6rem 0 0; font-size: .74rem; letter-spacing: .06em;
  color: var(--ink-dim); }
.setgrid--page { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--pad); }
.setpage__note { max-width: var(--maxw); margin: 1.2rem auto 0; padding: 0 var(--pad) clamp(3rem,8vh,6rem);
  color: var(--ink-dim); font-size: .9rem; }

@media (max-width: 900px) {
  .pwrap { grid-template-columns: 1fr; }
  .pbuy { position: static; }
  /* A vertical rail on a 390px screen eats a third of the width, so it goes back under the
     stage as a single scrolling row. Amazon does the same thing at this size. */
  .pgallery { grid-template-columns: 1fr; grid-template-areas: "stage" "label" "rail"; }
  /* Back to a row here, so the flex sizing that divides the stage height on desktop has to be
     undone: thumbs get their fixed square back and the row scrolls sideways instead. */
  .pgallery__thumbs { flex-direction: row; align-self: auto; overflow-x: auto; overflow-y: hidden;
    padding-bottom: .35rem; }
  .pthumb { flex: 0 0 auto; height: 64px; max-height: none; }
}
/* 6 / 3 / 2 columns. Every one of them divides 6 exactly, so there is never an orphan. */
@media (max-width: 1080px) { .precs__row { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 620px)  { .precs__row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ── the cart stays in view while you shop ────────────────────────────────
   Desktop: the sticky header already carries it, so the button just gains the
   running total once there is something in it. Phones get a bar pinned to the
   bottom instead, because there the header scrolls away and the buy button is
   gone within a couple of seconds. Rendered by cart.js only while the cart has
   something in it; an empty bar costs a thumb's width of every page for nothing. */
.cartbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 70;
  display: none; align-items: center; gap: .8rem;
  padding: .7rem var(--pad) calc(.7rem + env(safe-area-inset-bottom));
  background: rgba(250,250,249,.94); backdrop-filter: blur(10px) saturate(1.1);
  border-top: 1px solid var(--line);
  box-shadow: 0 -10px 26px -22px rgba(22,20,15,.7);
}
.cartbar__n { font-size: .82rem; color: var(--ink-soft); }
.cartbar__n strong { color: var(--ink); }
.cartbar__t { font-family: "Fraunces", Georgia, serif; font-size: 1.15rem; margin-left: auto; }
.cartbar__go { padding: .7em 1.2em; font-size: .72rem; }
@media (max-width: 880px) { .cartbar { display: flex; } body { padding-bottom: 4.2rem; } }

/* desktop: the header cart shows what is in it */
@media (min-width: 881px) {
  .nav__cart[data-total]:not([data-total="$0"])::after {
    content: attr(data-total); margin-left: .1em; color: var(--accent-ink); font-weight: 600;
  }
}

/* ── product type as visible options, not a dropdown ──────────────────────
   These were a <select>, so nobody learned a puzzle existed unless they opened
   it. A dropdown suits sizes, where the buyer is picking a number they already
   have in mind; it is wrong for the product itself, where the whole job is
   showing what is on offer. Each carries its own lowest price, because "canvas"
   means nothing until you know it starts at $60. */
.buy__legend { display: block; font-size: .66rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-dim); margin-bottom: .45rem; }
.ptypes { display: grid; grid-template-columns: repeat(auto-fit, minmax(6.6rem, 1fr)); gap: .4rem; }
.ptype { display: flex; flex-direction: column; gap: .12rem; align-items: flex-start;
  padding: .6em .7em; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--paper); color: var(--ink-soft); font: inherit; cursor: pointer; text-align: left;
  transition: border-color .25s var(--ease), color .25s var(--ease), background .25s var(--ease), transform .25s var(--ease); }
.ptype:hover { border-color: var(--accent-ink); color: var(--accent-ink); transform: translateY(-1px); }
.ptype.is-on { border-color: var(--accent-ink); background: var(--paper); color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--accent-ink); }
.ptype__n { font-size: .84rem; line-height: 1.2; }
.ptype__p { font-size: .68rem; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-dim); }
.ptype.is-on .ptype__p { color: var(--accent-ink); }

/* ══════════════════════════════════════════════════════════════════════════
   HOME: SUBJECTS FIRST, THEN SETS
   The home page used to render all 62 photographs in one scroll behind a filter
   bar. That is a contact sheet, not a storefront: everything competes with
   everything, nothing is the way in, and the sets, which are the highest-value
   thing on the site, sat buried among the singles. One frame per subject now,
   big enough to be the reason someone clicks.
   ══════════════════════════════════════════════════════════════════════════ */
.cats { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--pad);
  display: grid; gap: clamp(.7rem,1.5vw,1.2rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); }
.cat { position: relative; display: block; padding: 0; border: 0; cursor: pointer;
  background: var(--paper-3); border-radius: var(--radius); overflow: hidden;
  aspect-ratio: 4/3; transition: transform .5s var(--ease), box-shadow .5s var(--ease); }
.cat img { width: 100%; height: 100%; object-fit: cover; transition: transform 1.1s var(--ease); }
.cat:hover { transform: translateY(-3px); box-shadow: 0 22px 40px -30px rgba(22,20,15,.8); }
.cat:hover img { transform: scale(1.05); }
.cat__veil { position: absolute; inset: auto 0 0 0; display: flex; flex-direction: column; gap: .1rem;
  padding: 3rem 1.1rem 1rem; text-align: left;
  background: linear-gradient(to top, rgba(20,18,13,.9) 0%, rgba(20,18,13,.5) 55%, rgba(20,18,13,0) 100%); }
.cat__name { font-family: "Fraunces", Georgia, serif; font-size: clamp(1.35rem,2.6vw,1.85rem); color: #fff; line-height: 1.1; }
.cat__n { font-size: .68rem; letter-spacing: .16em; text-transform: uppercase; color: rgba(255,255,255,.82); }
.cat.is-on { box-shadow: inset 0 0 0 3px var(--accent); }
.cat.is-on .cat__name { color: #f0d9b6; }

.catopen:empty { display: none; }
.catopen__in { padding-top: clamp(1.6rem,4vh,2.6rem); }
.catopen__head { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--pad) 1.2rem; }
.catopen__head h3 { font-family: "Fraunces", Georgia, serif; font-weight: 400;
  font-size: clamp(1.5rem,3vw,2.1rem); margin: 0 0 .25rem; }
.catopen__head p { margin: 0 0 .7rem; color: var(--ink-soft); max-width: 46ch; }

/* sets get their own band: the one thing a buyer cannot assemble themselves */
.sets { background: var(--paper-2); border-block: 1px solid var(--line-2); }
.setcards { list-style: none; max-width: var(--maxw); margin: 0 auto;
  padding: 0 var(--pad) clamp(3rem,7vh,5rem); display: grid; gap: clamp(1rem,2vw,1.6rem);
  grid-template-columns: repeat(3, 1fr); }
@media (max-width: 980px) { .setcards { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px) { .setcards { grid-template-columns: 1fr; } }
.setcard { display: block; }
/* A FIXED FRAME, A TRUE STRIP. The frame keeps every card the same height so the rows
   line up; the strip inside carries the panels' real combined aspect and is CONTAINED,
   so the whole reconstructed photograph is visible instead of each panel being cropped
   into a sliver. A 5-panel 3:2 panorama is 7.5:1 and a 3-panel 3:4 is 2.25:1, which is
   why the frame cannot simply take the strip's shape. */
/* 2:1, not 3:2. The strips run 2.25:1 to 7.5:1, so a 3:2 frame left a wide panorama
   floating in dead space. A flatter frame fits the whole range far more tightly. */
.setcard__frame { display: grid; place-items: center; aspect-ratio: 2/1;
  background: var(--paper-3); border-radius: var(--radius); overflow: hidden; padding: .5rem; }
.setcard__strip { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 2px;
  aspect-ratio: var(--strip, 3); max-width: 100%; max-height: 100%; width: 100%; height: auto; }
.setcard__strip img { width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .7s var(--ease); }
.setcard:hover .setcard__strip img { transform: scale(1.04); }
.setcard__name { display: block; margin-top: .6rem; font-family: "Fraunces", Georgia, serif; font-size: 1.05rem; }
.setcard:hover .setcard__name { color: var(--accent-ink); }
.setcard__meta { display: block; font-size: .7rem; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-dim); }

/* The navigator sits ABOVE the subject cards and keeps both chip rows. I briefly replaced
   it with a lone search field and dropped product filtering, which is the part that gets
   used: "I like this navigator though, especially sorting for product." The section
   heading is gone instead, because the pills say what this is. */
.filters { padding-top: clamp(2rem,5vh,3.2rem); }
#galleries:empty { display: none; }

/* ── shipping & returns ────────────────────────────────────────────────────
   A policy page is READ, not browsed, so it gets a measure and generous
   leading rather than the gallery's density. Deliberately plain: Pinterest
   rejected the catalog for a returns policy that was "unclear, unavailable or
   difficult to find", and the fix for that is legibility, not design. */
.policy { max-width: 62ch; margin: 0 auto; padding: 2.2rem var(--pad) 4.5rem; }
.policy h1 { font-size: clamp(1.8rem, 2.6vw, 2.4rem); font-weight: 300; letter-spacing: -.02em; margin: 0 0 1.8rem; }
.policy h2 { font-size: 1.05rem; letter-spacing: .04em; text-transform: uppercase; color: var(--accent-ink);
             margin: 2.4rem 0 .7rem; }
.policy p { color: var(--ink-soft); line-height: 1.75; margin: 0 0 1rem; }
.policy strong { color: var(--ink); font-weight: 600; }
.policy a { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 2px; }
.policy a:hover { filter: brightness(1.15); }
