/* ==========================================================================
   cecelysterling.com
   Warm paper pages, near-black viewer. Images are matted, not full-bleed.
   ========================================================================== */

@font-face {
  font-family: "Lora";
  src: url("fonts/lora-var.woff") format("woff");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Lora";
  src: url("fonts/lora-italic-var.woff") format("woff");
  font-weight: 400 700;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Lato";
  src: url("fonts/lato-400.woff") format("woff");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Lato";
  src: url("fonts/lato-500.woff") format("woff");
  font-weight: 500;
  font-display: swap;
}

:root {
  --paper: #f6f3ee;
  --paper-deep: #efeae1;
  --ink: #191612;
  --ink-2: #4a443c;
  /* --ink-3 carries a lot of small text, so it has to clear 4.5:1 against both
     --paper and --paper-deep. #7d766b did not (4.06 / 3.75). */
  --ink-3: #6b655b;
  /* Decorative hairlines. Too faint to be the only sign of a control. */
  --rule: #ddd5c8;
  /* Borders that convey meaning — inputs, unselected chips — need 3:1. */
  --rule-strong: #878178;
  --void: #0b0a09;
  --on-void: #a39a8d;
  --accent: #8a3a2b;

  --serif: "Lora", Georgia, "Times New Roman", serif;
  --sans: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  --gap: 14px;
  --row: 300px;
  --shell: 1560px;
  --measure: 62ch;
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  /* A slower, softer curve for anything the eye tracks across a long distance —
     scroll reveals and the hero cross-fade. --ease is too eager over 800ms. */
  --ease-long: cubic-bezier(0.16, 0.84, 0.28, 1);
  --pad: clamp(18px, 4vw, 52px);
  /* Photographs sit on paper, so they get a mat edge and a shadow that reads as
     depth rather than as a UI drop-shadow: tight contact line, wide soft falloff. */
  --lift: 0 1px 1px rgba(25, 22, 18, 0.07), 0 10px 28px -12px rgba(25, 22, 18, 0.22);
  --lift-hi: 0 1px 1px rgba(25, 22, 18, 0.09), 0 22px 48px -16px rgba(25, 22, 18, 0.3);
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Reserve the scrollbar so opening the viewer doesn't shift the page. */
  scrollbar-gutter: stable;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 17px;
  line-height: 1.65;
  font-feature-settings: "kern" 1, "liga" 1;
  /* Old-style figures sit properly inside lowercase text — years and counts stop
     shouting. Ignored by faces that don't carry the feature. */
  font-variant-numeric: oldstyle-num proportional-nums;
  -webkit-font-smoothing: antialiased;
}

/* Paper tooth. One inline SVG turbulence tile, multiplied over the background at
   very low opacity — it reads as fibre in the paper rather than as noise, and it
   costs no request. Fixed, so it doesn't travel with the scroll and shimmer.
   Behind everything (z-index -1) so it never dulls a photograph. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.5;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.055'/%3E%3C/svg%3E");
}

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

/* ------------------------------------------------------- scroll reveal
   Scoped to .has-js, which an inline script in <head> sets before first paint.
   With JavaScript off the initial state never applies and everything is simply
   visible — the enhancement can't strand content. */
.has-js .reveal {
  opacity: 0;
  transform: translateY(18px);
  /* Cap the stagger: tile 30 of a long gallery should not wait two seconds. */
  transition: opacity 0.85s var(--ease-long) calc(min(var(--i, 0), 6) * 70ms),
              transform 0.85s var(--ease-long) calc(min(var(--i, 0), 6) * 70ms);
}
.has-js .reveal.is-in { opacity: 1; transform: none; }

/* The global reduced-motion rule zeroes transition-duration, which would leave
   anything still at opacity 0 permanently invisible. Reset the initial state
   outright rather than relying on a transition that no longer runs. */
@media (prefers-reduced-motion: reduce) {
  .has-js .reveal { opacity: 1; transform: none; }
}

/* <picture> is inline by default, which leaves a descender gap under images. */
.pic { display: block; }

/* Every image carries its blurred placeholder as a background. Without these
   three lines the 24px placeholder tiles instead of filling the frame. */
.pic img {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--ink);
  color: var(--paper);
  padding: 10px 16px;
  z-index: 200;
}
.skip:focus { left: 8px; top: 8px; }

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

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* main is focusable only as the skip-link target; it needs no visible ring. */
main:focus, main:focus-visible { outline: none; }
/* --accent is only 2.6:1 on the near-black viewer, so it gets a light ring. */
.viewer :focus-visible { outline-color: #e8e2d8; }

/* --------------------------------------------------------------- masthead */

/* ---------------------------------------------------------------- ribbon */

.ribbon {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: 6px 14px;
  padding: 10px var(--pad);
  background: var(--ink);
  color: var(--paper);
  text-decoration: none;
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.5;
  text-align: center;
}
.ribbon-tag {
  font-size: 10px;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  /* Reads as a label, not as body copy, without dropping below 4.5:1 on --ink. */
  color: #c8bfb1;
}
.ribbon-text { letter-spacing: 0.03em; border-bottom: 1px solid transparent; padding-bottom: 1px; }
.ribbon:hover .ribbon-text { border-bottom-color: #c8bfb1; }

/* --------------------------------------------------------------- masthead */

.masthead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  max-width: var(--shell);
  margin: 0 auto;
  padding: 30px var(--pad) 22px;
  border-bottom: 1px solid var(--rule);
}

/* On the home page the opener owns the whole viewport, so the masthead floats on
   top of it. Same paper, so no scrim is needed and the ink stays legible. */
.page-home .masthead {
  position: absolute;
  /* Absolute against the initial containing block, so top:0 would tuck the nav
     up underneath the ribbon. Offset by the ribbon's measured height. Defaults to
     0 rather than 80px: with no ribbon present there is nothing to clear, and the
     masthead has enough padding to survive being a little high if there is. */
  top: var(--ribbon-h, 0px);
  left: 0; right: 0;
  z-index: 60;
  border-bottom: 0;
  background: transparent;
}
.page-home .ribbon { position: relative; z-index: 61; }

.wordmark {
  font-family: var(--serif);
  font-size: clamp(17px, 1.5vw, 21px);
  letter-spacing: 0.055em;
  text-decoration: none;
  white-space: nowrap;
}
.wordmark:hover { color: var(--accent); }
/* The name is already set large in the opener; repeating it in the corner is
   redundant until the visitor has scrolled past it. */
.page-home .wordmark {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s var(--ease);
}
.page-home.is-scrolled .wordmark { opacity: 1; pointer-events: auto; }

.nav {
  display: flex;
  gap: clamp(14px, 2vw, 30px);
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}
.nav a {
  text-decoration: none;
  color: var(--ink-2);
  padding-bottom: 3px;
  border-bottom: 1px solid transparent;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease);
}
.nav a:hover { color: var(--ink); border-color: var(--ink); }
/* "page" is an exact match, "true" an ancestor section (Work → a photograph). */
.nav a[aria-current] { color: var(--ink); border-color: var(--accent); }

.nav-toggle { display: none; }

@media (max-width: 800px) {
  .masthead { align-items: center; padding: 18px 18px 16px; position: relative; }
  .nav-toggle {
    display: flex; flex-direction: column; justify-content: center; gap: 5px;
    width: 36px; height: 30px; padding: 0;
    background: none; border: 0; cursor: pointer;
  }
  .nav-toggle span { display: block; height: 1px; background: var(--ink); transition: transform 0.3s var(--ease); }
  .nav-toggle[aria-expanded="true"] span:first-child { transform: translateY(3px) rotate(45deg); }
  .nav-toggle[aria-expanded="true"] span:last-child { transform: translateY(-3px) rotate(-45deg); }
  .nav {
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; gap: 0;
    background: var(--paper);
    border-bottom: 1px solid var(--rule);
    max-height: 0; overflow: hidden;
    /* visibility, not just max-height: otherwise the six links stay focusable
       and screen-reader-visible while the menu looks closed. */
    visibility: hidden;
    transition: max-height 0.35s var(--ease), visibility 0s linear 0.35s;
    z-index: 60;
  }
  .nav.is-open {
    max-height: 60vh;
    visibility: visible;
    transition: max-height 0.35s var(--ease), visibility 0s;
  }
  .nav a { padding: 15px 18px; border-bottom: 1px solid var(--rule); }
  .nav a:last-child { border-bottom: 0; }
}

/* ------------------------------------------------------------- structure */

.band {
  max-width: var(--shell);
  margin: 0 auto;
  padding: clamp(46px, 7vw, 92px) clamp(18px, 4vw, 52px);
}
.band-top { padding-top: clamp(34px, 5vw, 62px); }
/* Consecutive bands would otherwise stack two full paddings into one enormous
   gap. Tinted bands are excluded: their padding is visible as the shape of the
   coloured area, so removing the top half makes the tint hug the heading and
   leave a large dead space underneath. */
.band + .band:not(.is-tinted) { padding-top: 0; }
/* Slightly tighter than a plain band, because the tint already separates it. */
.band.is-tinted { padding-top: clamp(38px, 5vw, 68px); padding-bottom: clamp(38px, 5vw, 68px); }

.measure { max-width: var(--measure); }
.center { margin-left: auto; margin-right: auto; text-align: center; }

.band-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  padding-bottom: 20px;
  margin-bottom: 26px;
  border-bottom: 1px solid var(--rule);
}

/* ----------------------------------------------------------- typography */

h1, h2, h3 {
  font-weight: 400;
  margin: 0;
  /* Even line lengths in headings rather than one orphaned word. */
  text-wrap: balance;
}

.h-lg {
  font-size: clamp(32px, 4.4vw, 52px);
  line-height: 1.1;
  letter-spacing: -0.012em;
  margin-bottom: 14px;
}
.h-md { font-size: clamp(23px, 2.4vw, 30px); line-height: 1.22; margin-bottom: 6px; }
.h-sm {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 500;
}

.eyebrow {
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 14px;
}
.eyebrow a { text-decoration: none; }
.eyebrow a:hover { color: var(--accent); }

.lede { font-size: clamp(18px, 1.7vw, 21px); line-height: 1.6; color: var(--ink-2); margin: 0 0 6px; }
.lede-sm { color: var(--ink-2); margin: 8px 0 20px; }
.dateline { font-family: var(--sans); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-3); margin: 0 0 18px; }

/* Avoids single-word last lines in body copy. Ignored where unsupported. */
.prose p, .lede, .lede-sm, .ex-note, .s-row-text { text-wrap: pretty; }
.prose p { margin: 0 0 1.15em; max-width: var(--measure); }
.prose p:last-child { margin-bottom: 0; }

.note { font-family: var(--sans); font-size: 13px; color: var(--ink-3); font-style: normal; }
.placeholder {
  border-left: 2px solid var(--rule);
  padding: 4px 0 4px 14px;
  margin: 0 0 1.15em;
  max-width: var(--measure);
}
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 0.86em; background: var(--paper-deep); padding: 1px 5px; }

.link-arrow {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-2);
  white-space: nowrap;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 3px;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease);
}
.link-arrow::after { content: " \2192"; }
.link-arrow:hover { color: var(--accent); border-color: var(--accent); }

.btn {
  display: inline-block;
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 13px 26px;
  border: 1px solid var(--ink);
  color: var(--ink);
  transition: background 0.25s var(--ease), color 0.25s var(--ease);
}
.btn:hover { background: var(--ink); color: var(--paper); }

/* ------------------------------------------------------------------ hero */

/* The opener fills the viewport, but the photograph is never cropped to do it —
   it is contained and centred, the way a print is hung. svh rather than vh so
   mobile browser chrome collapsing doesn't jump the layout. */
.hero {
  display: flex;
  flex-direction: column;
  /* The now-showing ribbon sits above this in normal flow, so a flat 100svh
     makes hero + ribbon taller than the screen and clips the intro line and the
     scroll cue off the bottom.
     The fallback is 80px, not 0, and that direction matters: guessing too high
     just leaves a little extra paper under the name, while guessing too low
     clips the text. 80px covers the ribbon even wrapped to two lines on a phone.
     An inline script after the ribbon sets the real value before this element is
     parsed, so the first paint is already correct; app.js then keeps it in step
     when the ribbon rewraps or a webfont swaps in. */
  min-height: calc(100svh - var(--ribbon-h, 80px));
  max-width: var(--shell);
  margin: 0 auto;
  padding: clamp(96px, 13vh, 150px) var(--pad) clamp(22px, 3vh, 40px);
}
.hero-stack {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.hero-frame {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.9s var(--ease-long);
  pointer-events: none;
}
/* Sequential, not simultaneous: the outgoing frame fades to paper first, and the
   incoming one waits for it. A true cross-fade looked broken here, because the
   photographs are wildly different shapes — dissolving a portrait into a
   landscape left a pale ghost of the wider image spilling out either side of the
   narrower one, which reads as a rendering fault rather than a transition.
   The delay is on the incoming frame only, so removing .is-on still fades out
   immediately. */
.hero-frame.is-on { opacity: 1; transition-delay: 0.9s; }
/* height:100% matters: <picture> is content-sized by default, and a percentage
   max-height on the <img> inside resolves against its parent's height. Against an
   auto height that is indeterminate and the browser drops the constraint
   entirely — which is why the image kept rendering at its full pixel cap and
   spilling past the rule. The frame is inset:0 on the stack, so this chain gives
   the img a definite height to measure against. */
.hero-frame .pic {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-height: 0;
}
.hero-frame img {
  /* Bounded by the stack, not by the viewport. A vh cap here is guesswork: the
     space actually available is the viewport minus the hero's padding and the
     name block below, so any fixed vh value overflows at some window height and
     the photograph spills out under the rule. The frame is inset:0 on the stack,
     so 100% is exactly the room there is. The px cap stops the image growing
     absurdly large relative to the type on a very tall display. */
  max-height: min(100%, 1000px);
  width: auto;
  max-width: 100%;
  object-fit: contain;
  box-shadow: var(--lift);
}

.hero-text {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 14px 40px;
  margin-top: clamp(22px, 3.4vh, 38px);
  padding-top: clamp(16px, 2vh, 22px);
  border-top: 1px solid var(--rule);
}
.hero-lines { min-width: 0; }
.hero-name {
  font-size: clamp(30px, 4.6vw, 58px);
  letter-spacing: -0.014em;
  line-height: 1.05;
  margin: 0;
}
.hero-intro {
  font-style: italic;
  color: var(--ink-2);
  margin: 8px 0 0;
  max-width: 44ch;
  font-size: clamp(15px, 1.25vw, 18px);
}
.hero-aside {
  display: flex;
  align-items: center;
  gap: 26px;
  padding-bottom: 4px;
}

/* A quiet way down. The rule breathes rather than bouncing — a bounce would be
   the one genuinely tacky thing on the page. */
.hero-cue {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--ink-3);
  transition: color 0.3s var(--ease);
}
.hero-cue:hover { color: var(--ink); }
.hero-cue-rule {
  display: block;
  width: 1px;
  height: 34px;
  background: linear-gradient(var(--rule-strong), transparent);
  transform-origin: top;
  animation: cue 2.8s var(--ease-long) infinite;
}
.hero-cue-word { writing-mode: vertical-rl; }
@keyframes cue {
  0%, 100% { transform: scaleY(0.55); opacity: 0.5; }
  50%      { transform: scaleY(1);    opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-cue-rule { animation: none; transform: none; opacity: 1; }
}
/* Hidden until app.js has populated the frames the dots switch between. */
.hero-dots { display: none; gap: 2px; margin: -6px -6px -6px 0; }
.hero-dots.is-ready { display: flex; }
.hero-dot {
  /* 7px visual mark inside a 24px target (WCAG 2.5.8). */
  position: relative;
  width: 24px; height: 24px; padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.hero-dot::before {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 7px; height: 7px;
  margin: -4px 0 0 -4px;
  border: 1px solid var(--ink-3);
  border-radius: 50%;
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
}
.hero-dot.is-on::before { background: var(--ink); border-color: var(--ink); }

@media (max-width: 700px) {
  .hero { padding-top: clamp(78px, 11vh, 104px); }
  .hero-text { align-items: flex-start; }
  .hero-aside { width: 100%; justify-content: space-between; padding-bottom: 0; }
}
/* Short landscape windows: the opener would otherwise push everything below the
   fold with nothing but a sliver of photograph visible.
   Dropping min-height means the stack no longer gets a definite height from
   `flex: 1`, and every frame inside it is absolutely positioned — so it collapses
   to zero and the images overflow onto the name. Give the stack the height
   directly, and the percentage cap on the image resolves as it does elsewhere. */
@media (max-height: 560px) {
  .hero { min-height: auto; padding-top: 92px; }
  .hero-stack { height: 62vh; flex: none; }
}

/* --------------------------------------------------------------- gallery
   Pure-CSS justified rows: flex-basis and flex-grow both proportional to the
   aspect ratio, so final widths stay proportional and every row lands on a
   common height. No cropping, no JavaScript.                            */

.gallery {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap);
}
.gallery::after {
  content: "";
  flex-grow: 1000000;
  min-width: 0;
}
/* A handful of photographs would otherwise sit in one short row with a lot of
   empty paper to the right. Taller rows plus a weak filler fills the width. */
.gallery.is-short { --row: 380px; }
.gallery.is-short::after { flex-grow: 2; }
@media (max-width: 1000px) { .gallery.is-short { --row: 300px; } }
.tile {
  position: relative;
  display: block;
  flex: var(--ar) 1 calc(var(--ar) * var(--row));
  min-width: 0;
  text-decoration: none;
  background: var(--paper-deep);
  overflow: hidden;
  box-shadow: var(--lift);
  transition: box-shadow 0.45s var(--ease), transform 0.45s var(--ease);
}
.tile .pic { width: 100%; }
.tile img {
  width: 100%;
  height: auto;
  transition: opacity 0.5s var(--ease), transform 1.1s var(--ease);
}
.tile-veil {
  position: absolute;
  inset: 0;
  background: var(--ink);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
  pointer-events: none;
}
.tile:hover { box-shadow: var(--lift-hi); transform: translateY(-2px); }
.tile:hover img { transform: scale(1.014); }
.tile:hover .tile-veil { opacity: 0.05; }
.tile.is-hidden { display: none; }

@media (max-width: 1000px) { :root { --row: 230px; } }
@media (max-width: 640px) {
  .gallery { display: block; }
  .gallery::after { display: none; }
  .tile { width: 100%; margin-bottom: var(--gap); }
  .tile:hover img { transform: none; }
}

/* A live region, so it stays in the tree and only its text changes. */
.empty-note:empty { display: none; }
.empty-note {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--ink-3);
  text-align: center;
  padding: 40px 0;
}

/* ----------------------------------------------------------------- chips */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 22px 0 26px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--rule);
}
.chip {
  font-family: var(--sans);
  font-size: 11.5px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  padding: 9px 17px;
  background: none;
  border: 1px solid var(--rule-strong);
  color: var(--ink-2);
  cursor: pointer;
  transition: all 0.25s var(--ease);
}
.chip:hover { border-color: var(--ink); color: var(--ink); }
.chip.is-on { background: var(--ink); border-color: var(--ink); color: var(--paper); }

/* ---------------------------------------------------------------- series */

.s-cards {
  display: grid;
  /* 210px rather than 240px so five series fit on one row from about 1230px up,
     which covers most laptops. At 240px a 1280px window gave four across and
     left the fifth alone on its own row, which reads as a mistake. */
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: clamp(20px, 2.4vw, 34px);
}
.s-card { text-decoration: none; display: block; }
.s-card-img {
  overflow: hidden;
  background: var(--paper-deep);
  aspect-ratio: 4 / 3;
  box-shadow: var(--lift);
  transition: box-shadow 0.45s var(--ease);
}
.s-card-img img { width: 100%; height: 100%; object-fit: cover; transition: transform 1.1s var(--ease); }
.s-card:hover .s-card-img { box-shadow: var(--lift-hi); }
.s-card:hover .s-card-img img { transform: scale(1.03); }
.s-card-title { font-size: 20px; margin: 14px 0 3px; }
.s-card:hover .s-card-title { color: var(--accent); }
.s-card-meta { font-family: var(--sans); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); margin: 0; }

.s-rows { display: flex; flex-direction: column; }
.s-row {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: clamp(22px, 4vw, 60px);
  align-items: center;
  padding: clamp(30px, 4vw, 56px) 0;
  border-bottom: 1px solid var(--rule);
}
.s-row:first-child { padding-top: 8px; }
.s-row:nth-child(even) .s-row-img { order: 2; }
.s-row-img { display: block; overflow: hidden; background: var(--paper-deep); box-shadow: var(--lift); transition: box-shadow 0.45s var(--ease); }
.s-row-img img { transition: transform 1.2s var(--ease); }
.s-row-img:hover { box-shadow: var(--lift-hi); }
.s-row-img:hover img { transform: scale(1.025); }
.s-row-body h2 a { text-decoration: none; }
.s-row-body h2 a:hover { color: var(--accent); }
.s-row-meta { font-family: var(--sans); font-size: 12px; letter-spacing: 0.11em; text-transform: uppercase; color: var(--ink-3); margin: 0 0 14px; }
.s-row-text { color: var(--ink-2); margin: 0 0 18px; }

@media (max-width: 800px) {
  .s-row { grid-template-columns: 1fr; gap: 20px; }
  .s-row:nth-child(even) .s-row-img { order: 0; }
}

/* ------------------------------------------------------------ show note */

.show-note {
  background: var(--paper-deep);
  max-width: none;
  padding-left: 0; padding-right: 0;
}
.show-inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0 var(--pad);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 28px;
  align-items: start;
}
/* A struck rule in the accent, standing in for the red thread on a gallery
   card. Decorative only — the "Now showing" eyebrow carries the meaning. */
.show-mark {
  width: 2px;
  align-self: stretch;
  min-height: 84px;
  background: var(--accent);
}
.show-body { max-width: var(--measure); }
.show-note .h-md { margin-bottom: 10px; }
.show-meta { font-family: var(--sans); font-size: 14px; color: var(--ink-2); margin: 0 0 18px; line-height: 1.75; }

/* ------------------------------------------------------------- chapter
   A series opens like a chapter: full-width plate, then title and statement
   side by side under a rule. */

.chapter {
  max-width: var(--shell);
  margin: 0 auto;
  padding: clamp(26px, 4vw, 48px) var(--pad) 0;
}
.chapter-plate {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--paper-deep);
  padding: clamp(14px, 2.2vw, 30px);
}
.chapter-plate img {
  max-height: min(62vh, 640px);
  width: auto;
  max-width: 100%;
  object-fit: contain;
  box-shadow: var(--lift);
}
.chapter-head {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: clamp(20px, 4vw, 64px);
  align-items: start;
  margin-top: clamp(26px, 3.4vw, 44px);
  padding-top: clamp(20px, 2.4vw, 28px);
  border-top: 1px solid var(--rule);
}
.chapter-title .h-lg { margin-bottom: 10px; }
.chapter-title .dateline { margin: 0; }
.chapter-text {
  margin: 0;
  color: var(--ink-2);
  font-size: clamp(17px, 1.5vw, 19px);
  line-height: 1.7;
  text-wrap: pretty;
}
.band-tight { padding-top: clamp(28px, 4vw, 52px); }

@media (max-width: 800px) {
  .chapter-head { grid-template-columns: 1fr; gap: 18px; }
  .chapter-plate img { max-height: 52vh; }
}
@media (max-width: 620px) {
  .show-inner { grid-template-columns: 1fr; gap: 18px; }
  .show-mark { width: 44px; height: 2px; min-height: 0; align-self: start; }
}

/* ------------------------------------------------------------ photo page */

.page-photo main { padding-bottom: 0; }
.photo-page {
  max-width: var(--shell);
  margin: 0 auto;
  padding: clamp(24px, 3.4vw, 46px) clamp(18px, 4vw, 52px) clamp(40px, 6vw, 80px);
}
.ph-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: min(76vh, 800px);
  background: var(--paper-deep);
  padding: clamp(16px, 3vw, 44px);
}
/* The white mat is what makes this read as a hung print rather than a picture in
   a box: a generous even border, a hairline frame, and the photograph inset. */
.ph-frame {
  background: #fbf9f6;
  padding: clamp(10px, 1.8vw, 26px);
  border: 1px solid var(--rule);
  box-shadow: var(--lift-hi);
  max-width: 100%;
}
.ph-stage img { max-height: min(68vh, 720px); width: auto; max-width: 100%; object-fit: contain; }
.ph-meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 24px 0 0;
  max-width: var(--measure);
}
.ph-title { font-size: clamp(23px, 2.4vw, 31px); line-height: 1.2; }
.ph-untitled { color: var(--ink-3); }
.ph-line { font-family: var(--sans); font-size: 13.5px; letter-spacing: 0.05em; color: var(--ink-3); margin: 0; }
.ph-caption { color: var(--ink-2); margin: 10px 0 0; }
.ph-series { font-family: var(--sans); font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; margin: 12px 0 0; }
.ph-series a { color: var(--ink-2); text-decoration: none; border-bottom: 1px solid var(--rule); padding-bottom: 2px; }
.ph-series a:hover { color: var(--accent); border-color: var(--accent); }
.ph-inquire { margin: 18px 0 0; }

.ph-nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  margin-top: clamp(34px, 5vw, 58px);
  padding-top: 20px;
  border-top: 1px solid var(--rule);
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}
.ph-nav a { color: var(--ink-2); text-decoration: none; }
.ph-nav a:hover { color: var(--accent); }
.ph-nav-index { color: var(--ink-3) !important; }

/* ----------------------------------------------------------------- about */

.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
  gap: clamp(26px, 5vw, 72px);
  align-items: start;
}
.portrait { background: var(--paper-deep); }
.portrait-empty {
  display: flex; align-items: center; justify-content: center;
  aspect-ratio: 4 / 5; padding: 24px; text-align: center;
  border: 1px dashed var(--rule);
}
.bio { margin-top: clamp(30px, 4vw, 48px); padding-top: 22px; border-top: 1px solid var(--rule); }
.bio p { margin: 12px 0 0; max-width: var(--measure); color: var(--ink-2); }
.about-cta { margin-top: 30px; }

@media (max-width: 800px) {
  .about-grid { grid-template-columns: 1fr; }
  .portrait { max-width: 340px; }
}

/* ----------------------------------------------------------- exhibitions */

.ex-list { list-style: none; margin: 26px 0 0; padding: 0; }
.ex-row {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr);
  gap: 20px;
  padding: 26px 0;
  border-top: 1px solid var(--rule);
}
.ex-row.is-current { background: var(--paper-deep); padding-left: 18px; padding-right: 18px; margin-left: -18px; margin-right: -18px; }
.ex-year { font-family: var(--sans); font-size: 13px; letter-spacing: 0.1em; color: var(--ink-3); padding-top: 5px; }
.ex-title { font-size: 22px; line-height: 1.3; margin-bottom: 5px; }
.badge {
  display: inline-block;
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 3px 9px;
  margin-left: 8px;
  background: var(--accent);
  color: var(--paper);
  vertical-align: 3px;
}
.ex-venue { margin: 0; color: var(--ink-2); }
.ex-dates, .ex-open { font-family: var(--sans); font-size: 13.5px; color: var(--ink-3); margin: 4px 0 0; }
.ex-note { color: var(--ink-2); margin: 12px 0 0; max-width: var(--measure); font-style: italic; }

.cv { margin-top: clamp(40px, 5vw, 66px); display: grid; gap: 34px; }
.cv-block { padding-top: 18px; border-top: 1px solid var(--rule); }
.cv-list { list-style: none; margin: 14px 0 0; padding: 0; }
.cv-list li { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 20px; padding: 7px 0; }
.cv-year { font-family: var(--sans); font-size: 13px; color: var(--ink-3); }
.cv-detail { color: var(--ink-2); }

@media (max-width: 620px) {
  .ex-row, .cv-list li { grid-template-columns: 1fr; gap: 4px; }
}

/* ------------------------------------------------------- prints, contact */

.prints-grid, .contact-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: clamp(30px, 5vw, 76px);
  align-items: start;
}
@media (max-width: 860px) {
  .prints-grid, .contact-grid { grid-template-columns: 1fr; }
}

.sizes { margin-top: 34px; padding-top: 20px; border-top: 1px solid var(--rule); }
.sz-list { list-style: none; margin: 14px 0 0; padding: 0; }
.sz-list li { display: flex; justify-content: space-between; gap: 20px; padding: 10px 0; border-bottom: 1px solid var(--rule); }
.sz-label { font-size: 17px; }
.sz-note { font-family: var(--sans); font-size: 13.5px; color: var(--ink-3); }

.c-lines { margin-top: 30px; }
.c-line { display: flex; gap: 16px; align-items: baseline; padding: 11px 0; border-bottom: 1px solid var(--rule); margin: 0; }
.c-key { font-family: var(--sans); font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase; color: var(--ink-3); min-width: 90px; }
.c-line a { text-decoration: none; border-bottom: 1px solid var(--rule); }
.c-line a:hover { color: var(--accent); border-color: var(--accent); }

/* ------------------------------------------------------------------ form */

.form { display: flex; flex-direction: column; gap: 4px; }
.form label {
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-top: 16px;
}
.form input, .form textarea, .form select {
  font-family: var(--serif);
  font-size: 16px;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--rule-strong);
  padding: 9px 2px;
  border-radius: 0;
  -webkit-appearance: none;
  appearance: none;
}
.form select { background-image: none; }
.form textarea { resize: vertical; }
.form input:focus, .form textarea:focus, .form select:focus {
  outline: none;
  border-bottom-color: var(--ink);
}
/* :user-invalid rather than :invalid — it only applies once the visitor has
   actually interacted, so empty required fields aren't flagged on arrival. */
.form input:user-invalid,
.form textarea:user-invalid {
  /* Weight as well as colour, so the error isn't conveyed by hue alone. */
  border-bottom-color: var(--accent);
  border-bottom-width: 2px;
  padding-bottom: 8px;
}
.form button {
  align-self: flex-start;
  margin-top: 28px;
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 13px 30px;
  background: var(--ink);
  color: var(--paper);
  border: 1px solid var(--ink);
  cursor: pointer;
  transition: background 0.25s var(--ease), color 0.25s var(--ease);
}
.form button:hover { background: transparent; color: var(--ink); }
.form-note { margin-top: 14px; }

/* ------------------------------------------------------------ newsletter */

.newsletter { background: var(--paper-deep); max-width: none; padding-left: 0; padding-right: 0; }
.newsletter .measure { padding: 0 clamp(18px, 4vw, 52px); max-width: 56ch; }
.signup { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 22px; }
.signup input {
  font-family: var(--serif);
  font-size: 16px;
  padding: 12px 14px;
  min-width: 240px;
  flex: 1 1 240px;
  max-width: 340px;
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  border-radius: 0;
  color: var(--ink);
}
.signup input:focus { outline: none; border-color: var(--ink); }
.signup button {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 12px 26px;
  background: var(--ink);
  color: var(--paper);
  border: 1px solid var(--ink);
  cursor: pointer;
}
.signup button:hover { background: transparent; color: var(--ink); }
.signup-fallback { margin-top: 20px; }

/* ---------------------------------------------------------------- footer */

.footer {
  border-top: 1px solid var(--rule);
  margin-top: clamp(50px, 7vw, 96px);
}
.foot-inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 36px clamp(18px, 4vw, 52px) 52px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.foot-name { font-size: 16px; letter-spacing: 0.05em; margin: 0; }
.foot-links { display: flex; gap: 20px; margin: 0; font-family: var(--sans); font-size: 12px; letter-spacing: 0.13em; text-transform: uppercase; }
.foot-links a { color: var(--ink-2); text-decoration: none; border-bottom: 1px solid var(--rule); padding-bottom: 2px; }
.foot-links a:hover { color: var(--accent); border-color: var(--accent); }
.foot-fine { font-family: var(--sans); font-size: 11.5px; line-height: 1.7; color: var(--ink-3); margin: 12px 0 0; max-width: 60ch; }

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

/* A modal <dialog> is promoted to the top layer, so no z-index is needed and
   nothing on the page can paint over it. */
.viewer {
  width: 100%;
  max-width: 100%;
  height: 100%;
  max-height: 100%;
  padding: 0;
  border: 0;
  margin: 0;
  background: var(--void);
  color: var(--on-void);
  overscroll-behavior: contain;
  opacity: 0;
  /* allow-discrete lets display and overlay animate, so the dialog can fade
     out instead of vanishing. Unsupported browsers simply snap. */
  transition: opacity 0.35s var(--ease),
              display 0.35s var(--ease) allow-discrete,
              overlay 0.35s var(--ease) allow-discrete;
}
.viewer[open] {
  opacity: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* The entry animation needs a defined "before" state. */
@starting-style {
  .viewer[open] { opacity: 0; }
}
.viewer:not([open]) { pointer-events: none; }
/* The backdrop must fade on the same curve. Left opaque, it snaps away at the
   end of the dialog's own transition and the fade is never seen. */
.viewer::backdrop {
  background: var(--void);
  opacity: 0;
  transition: opacity 0.35s var(--ease),
              display 0.35s var(--ease) allow-discrete,
              overlay 0.35s var(--ease) allow-discrete;
}
.viewer[open]::backdrop { opacity: 1; }
@starting-style {
  .viewer[open]::backdrop { opacity: 0; }
}

.viewer-figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  max-width: 92vw;
  max-height: 92vh;
}
.viewer-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  max-height: 80vh;
  min-height: 40vh;
}
.viewer-stage img {
  max-height: 80vh;
  max-width: 92vw;
  width: auto;
  object-fit: contain;
  opacity: 0;
  transition: opacity 0.45s var(--ease);
}
.viewer-stage img.is-loaded { opacity: 1; }

.viewer-caption {
  font-family: var(--sans);
  font-size: 12.5px;
  letter-spacing: 0.08em;
  color: var(--on-void);
  text-align: center;
  line-height: 1.7;
  min-height: 1.7em;
}
.viewer-caption .vc-title { font-family: var(--serif); font-style: italic; font-size: 15.5px; letter-spacing: 0; color: #e8e2d8; display: block; }
.viewer-caption .vc-count { display: block; margin-top: 5px; font-size: 11px; letter-spacing: 0.16em; color: var(--on-void); }

.viewer button {
  position: fixed;
  background: none;
  border: 0;
  color: var(--on-void);
  cursor: pointer;
  transition: color 0.25s var(--ease);
  line-height: 1;
}
.viewer button:hover { color: #f2ece2; }
.viewer-close { top: 18px; right: 22px; font-size: 34px; width: 46px; height: 46px; }
.viewer-prev, .viewer-next { top: 50%; transform: translateY(-50%); font-size: 46px; width: 62px; height: 92px; }
.viewer-prev { left: 6px; }
.viewer-next { right: 6px; }

body.viewer-open { overflow: hidden; }

@media (max-width: 700px) {
  .viewer-prev, .viewer-next { font-size: 34px; width: 46px; }
  .viewer-stage img { max-height: 70vh; }
  .viewer-figure { max-width: 96vw; }
}

/* ------------------------------------------------------------ print CSS */

@media print {
  .masthead, .nav, .footer, .viewer, .chips, .ph-nav, .newsletter { display: none !important; }
  body { background: #fff; }
}

/* ---------------------------------------------------------- transitions */

/* Cross-document view transitions: a soft cross-fade between pages, and the
   photograph itself morphs when stepping through with Previous/Next. Purely
   additive — browsers without support navigate normally. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }

  .ph-stage img { view-transition-name: photo; }

  /* Never longer than the root fade: navigating from a photograph to the grid
     there is no incoming `photo`, and a slower group would leave the outgoing
     photograph floating over the already-painted grid. */
  ::view-transition-group(photo) { animation-duration: 0.2s; }
  ::view-transition-old(root),
  ::view-transition-new(root) { animation-duration: 0.22s; }
  /* Stops portrait↔landscape pairs stretching as the group is interpolated. */
  ::view-transition-old(photo),
  ::view-transition-new(photo) {
    height: 100%;
    object-fit: contain;
  }
}
