/* ==========================================================================
   TRISHA ATELIER
   One stylesheet. No framework, no build step, no dependencies.

   Layout and component language follows the Webild "coffee shop" template:
   a handwritten marker display face, pill buttons in dark/light pairs,
   rounded image cards with a small label pill overlapping the bottom edge,
   very generous vertical air, and a dark footer slab carrying a giant
   wordmark. Palette is pine-teal on pale celadon rather than coffee brown.

   Contents
     01  Fonts
     02  Design tokens
     03  Reset
     04  Base typography
     05  Layout primitives
     06  Navigation
     07  Buttons, pills, chips
     08  Hero
     09  Interlude
     10  Work cards + grid
     11  Feature cards
     12  Contact panel
     13  Footer
     14  Gallery page
     15  Lightbox
     16  About page
     17  Image frames
     18  Motion + page transitions
     19  Responsive
   ========================================================================== */


/* 01  Fonts --------------------------------------------------------------- */

/* Katales Broken by Rahagita Studio, self-hosted like everything else here.
   Converted from the supplied .otf to woff2 and otherwise untouched — its
   licence permits format conversion but not modification, so this is the
   whole font rather than a latin subset, and it carries no unicode-range for
   the same reason: nothing here should narrow what the file may be used for.
   That costs about 60KB, which is why it is the only file preloaded. */
@font-face {
  font-family: 'Katales Broken';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/katales-broken.woff2') format('woff2');
}
/* The body face is Avenir Next, used from the reader's own machine — there is
   no @font-face for it and no file in assets/fonts. It is a licensed Monotype
   typeface that ships with macOS and iOS; self-hosting a copy would mean
   redistributing it, which the licence does not allow. So the stack below
   asks for what is installed and degrades on the way down: Avenir Next, then
   plain Avenir (older Macs), then whatever the platform calls its UI sans. */


/* 02  Design tokens ------------------------------------------------------- */

:root {
  /* Pine-teal on pale celadon, with terracotta as the single accent. */
  --canvas:    #e9ede7;
  --canvas-2:  #dfe5db;   /* recessed panels */
  --surface:   #f5f7f2;   /* cards, light pills */
  --ink:       #17302b;   /* text, dark pills, footer slab */
  --ink-70:    rgba(23, 48, 43, 0.70);
  --ink-45:    rgba(23, 48, 43, 0.45);
  --ink-25:    rgba(23, 48, 43, 0.25);
  --line:      rgba(23, 48, 43, 0.12);
  --line-soft: rgba(23, 48, 43, 0.07);
  --accent:    #c96f4a;   /* terracotta — availability, small marks */
  --on-ink:    #e9ede7;

  --font-hand: 'Katales Broken', 'Comic Sans MS', ui-rounded, cursive;
  --font-text: 'Avenir Next', 'Avenir', system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* Type. The display face carries the whole personality, so it runs big. */
  --t-eyebrow:  0.75rem;                          /* 12px sans */
  --t-pill:     0.8125rem;                        /* 13px sans — button and nav labels */
  --t-meta:     0.875rem;
  --t-body:     1rem;
  --t-lead:     clamp(0.95rem, 1.1vw, 1.0625rem);
  /* The display sizes below are all set in the hand face, and that face draws
     very large on the em: Katales Broken's caps fill 92% of it against the
     sans's 73%, so the same font-size lands about a quarter taller. These are
     scaled down to compensate. Change the face and they want re-checking
     against the sans beside them. */
  --t-h3:       clamp(1.1rem, 1.8vw, 1.55rem);
  --t-h2:       clamp(1.55rem, 3.75vw, 2.9rem);
  --t-h2-big:   clamp(1.7rem, 4.4vw, 3.45rem);
  /* Sized so the studio name sits on ONE line at desktop, as in the template */
  --t-hero:     clamp(2.1rem, 7.6vw, 6.1rem);
  --t-wordmark: clamp(2.25rem, 8.9vw, 7.3rem);

  --gutter:  clamp(1.25rem, 4vw, 3rem);
  --measure: 1200px;
  --section: clamp(4rem, 9vw, 7rem);
  --air:     clamp(6rem, 16vw, 14rem);   /* the template's huge breathers */

  /* Every image wears the same mat: a wide band around the picture, made of
     glass rather than paint. It is a diagonal wash of the surface colour that
     fades from 80% to 40% opacity over a blurred backdrop, so the band picks
     up whatever passes behind it as the page scrolls instead of sitting there
     as a flat edge. Its outer radius is the picture's radius plus the band,
     which keeps the two curves concentric. */
  --mat:      clamp(10px, 1.3vw, 18px);
  --mat-sm:   clamp(9px, 1vw, 13px);    /* the small floating snapshots */
  --mat-bg:   var(--surface);
  --mat-glass: linear-gradient(to bottom right,
                 color-mix(in srgb, var(--mat-bg) 80%, transparent),
                 color-mix(in srgb, var(--mat-bg) 40%, transparent));
  /* The pale buttons are cut from the same glass, only denser — they have to
     stay legible over anything, so the far corner goes fully opaque. */
  --btn-glass: linear-gradient(to bottom right,
                 color-mix(in srgb, var(--surface) 80%, transparent),
                 var(--surface));

  --r-card:  20px;
  --r-img:   16px;
  --r-pill:  999px;
  --r-slab:  clamp(24px, 4vw, 44px);

  --shade:      0 1px 2px rgba(23, 48, 43, 0.06),
                0 10px 30px rgba(23, 48, 43, 0.07);
  --shade-soft: 0 1px 2px rgba(23, 48, 43, 0.05),
                0 6px 18px rgba(23, 48, 43, 0.05);
  /* Framed pictures use a longer ramp instead: six near-invisible layers that
     each reach further than the last. No single one of them reads as an edge,
     which is the point — the frame lifts off the page without a shadow line. */
  --shade-card: 0 0.7px 0.7px -0.7px   rgba(23, 48, 43, 0.04),
                0 1.8px 1.8px -1.3px   rgba(23, 48, 43, 0.03),
                0 3.6px 3.6px -2px     rgba(23, 48, 43, 0.02),
                0 6.9px 6.9px -2.7px   rgba(23, 48, 43, 0.02),
                0 13.6px 13.6px -3.3px rgba(23, 48, 43, 0.015),
                0 24px 24px -4px       rgba(23, 48, 43, 0.015),
                0 1px 2px              rgba(23, 48, 43, 0.04);
  /* The template's dark pills have a bright lip along the top edge. Kept to a
     hairline: any thicker and the pill starts to look moulded. */
  --lip: inset 0 1.5px 0.5px 0 color-mix(in srgb, var(--on-ink) 14%, transparent);
  /* …and they cast their own colour rather than a grey shadow: six layers of
     the ink itself, each reaching further and fainter than the last, so the
     pill sits in a soft pool of its own green instead of on a hard edge. The
     template runs these at 13% and out to 50px, which reads as moulded
     plastic at our sizes; 6% and a shorter reach keeps the tint, loses the
     bulge. */
  --glow-ink: 0 0.8px 0.5px  -0.3px color-mix(in srgb, var(--ink) 6%, transparent),
              0 2px 1.2px    -0.6px color-mix(in srgb, var(--ink) 6%, transparent),
              0 3.6px 2.2px  -0.9px color-mix(in srgb, var(--ink) 5%, transparent),
              0 6px 3.6px    -1.3px color-mix(in srgb, var(--ink) 5%, transparent),
              0 10px 6px     -1.6px color-mix(in srgb, var(--ink) 4%, transparent),
              0 18px 12px    -2px   color-mix(in srgb, var(--ink) 4%, transparent);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur: 220ms;

  /* Entrances get their own curve and their own clock, kept apart from --ease
     and --dur above. Those two belong to things the reader is doing — a hover,
     a button, the chip pill — and have to answer immediately; these belong to
     the page arriving, which should not look like it is in a hurry. The curve
     is a plain cubic ease-out rather than the near-exponential one above: at
     this length the exponential spends its first fifth of a second almost
     finished and the rest of the time barely moving, which reads as a snap
     followed by a wait rather than as one unhurried movement. */
  --ease-reveal: cubic-bezier(0.33, 1, 0.68, 1);
  --dur-reveal: 1100ms;
  --reveal-rise: 28px;
}


/* 03  Reset --------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body, h1, h2, h3, h4, p, figure, blockquote, dl, dd, ul, ol {
  margin: 0; padding: 0;
}
ul { list-style: none; }
img, svg, video { display: block; max-width: 100%; height: auto; }
button, input, textarea, select { font: inherit; color: inherit; background: none; border: 0; }
button { cursor: pointer; }
a { color: inherit; text-decoration: none; }

:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 4px; }
::selection { background: var(--ink); color: var(--canvas); }


/* 04  Base typography ----------------------------------------------------- */

body {
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--font-text);
  font-size: var(--t-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* Every heading is handwritten. That single decision is the whole look. */
h1, h2, h3, .hand {
  font-family: var(--font-hand);
  font-weight: 700;
  line-height: 1.14;
  /* The hand face is drawn with wide sidebearings and reads as written rather
     than set; pulling it tighter fights the letterforms. */
  letter-spacing: 0;
  text-wrap: balance;
}

h1 { font-size: var(--t-h2-big); }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-h3); }

p { text-wrap: pretty; }

.lead {
  font-size: var(--t-lead);
  line-height: 1.65;
  color: var(--ink-70);
  max-width: 46ch;
}

.eyebrow {
  display: block;
  font-family: var(--font-text);
  font-size: var(--t-eyebrow);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--ink-45);
  margin-bottom: 0.875rem;
}

.skip-link {
  position: absolute; top: 0; left: 50%;
  transform: translate(-50%, -140%);
  z-index: 200;
  padding: 0.75rem 1.25rem;
  background: var(--ink); color: var(--on-ink);
  font-size: var(--t-pill);
  border-radius: 0 0 12px 12px;
  transition: transform var(--dur) var(--ease);
}
.skip-link:focus { transform: translate(-50%, 0); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.only-mobile { display: none; }


/* 05  Layout primitives --------------------------------------------------- */

.wrap {
  width: 100%; max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: var(--section); }
.section--air { padding-block: var(--air); }

/* Centred eyebrow + handwritten h2 + sans subcopy — the template's
   standard section opener. */
.head {
  text-align: center;
  max-width: 46ch;
  margin: 0 auto clamp(2.25rem, 4vw, 3.25rem);
}
.head .lead { margin: 0.875rem auto 0; }
.head h2 { margin-inline: auto; max-width: 18ch; }


/* 06  Navigation ----------------------------------------------------------
   Deliberately not a floating card: bare mark on the left, a dark pill and a
   round icon button on the right, sitting straight on the page. */

.nav-shell {
  position: sticky; top: 0; z-index: 100;
  padding: clamp(1rem, 2.2vw, 1.75rem) var(--gutter);
  pointer-events: none;
  background: linear-gradient(var(--canvas) 55%, transparent);
}

.nav {
  pointer-events: auto;
  position: relative;
  display: flex; align-items: center; gap: 0.625rem;
  width: 100%; max-width: var(--measure); margin-inline: auto;
}

.nav__mark {
  margin-right: auto;
  font-family: var(--font-hand);
  font-size: 1.1875rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

/* Text only — no plate, no pill. The padding is still there so the whole 44px
   box is clickable rather than just the word, it simply never paints. Both
   hover and the current page are marked the same way, by darkening the label
   from --ink-70 to full --ink; the weight stays put so nothing reflows. */
.nav__links {
  display: flex; align-items: center; gap: 0.125rem;
  margin-right: clamp(0.5rem, 1.4vw, 1.25rem);
  font-size: var(--t-pill);   /* same as the Enquire pill sitting beside them */
}
.nav__links a {
  display: flex; align-items: center; gap: 0.4375rem;
  min-height: 44px; padding-inline: 0.8125rem;
  border-radius: var(--r-pill);   /* shapes the focus ring, nothing else */
  color: var(--ink-70);
  white-space: nowrap;
  transition: color var(--dur) var(--ease);
}
.nav__links a:hover,
.nav__links a[aria-current='page'] { color: var(--ink); }

/* Small line drawings, one per destination. They repeat the label rather than
   replacing it, so they stay decorative — hidden from assistive tech, and the
   nav still reads correctly if they fail to paint. */
.nav__icon {
  flex: 0 0 auto;
  width: 17px; height: 17px;
  fill: none;
  stroke: currentColor; stroke-width: 1.55;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform var(--dur) var(--ease);
}
.nav__icon .dot { fill: currentColor; stroke: none; }
.nav__links a:hover .nav__icon { transform: rotate(-6deg) scale(1.08); }

.nav__toggle {
  display: none;
  flex: 0 0 auto;
  width: 44px; height: 44px;
  align-items: center; justify-content: center;
  border-radius: var(--r-pill);
  background: var(--ink);
  box-shadow: var(--lip), var(--glow-ink);
}
.nav__toggle span { display: block; width: 16px; height: 1.5px; background: var(--on-ink); position: relative; }
.nav__toggle span::before,
.nav__toggle span::after {
  content: ''; position: absolute; left: 0;
  width: 16px; height: 1.5px; background: var(--on-ink);
  transition: transform var(--dur) var(--ease);
}
.nav__toggle span::before { top: -5px; }
.nav__toggle span::after  { top: 5px; }
.nav__toggle[aria-expanded='true'] span { background: transparent; }
.nav__toggle[aria-expanded='true'] span::before { transform: translateY(5px) rotate(45deg); }
.nav__toggle[aria-expanded='true'] span::after  { transform: translateY(-5px) rotate(-45deg); }


/* 07  Buttons, pills, chips ----------------------------------------------- */

.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 0.5rem;
  height: 46px; padding-inline: 1.625rem;
  border-radius: var(--r-pill);
  font-size: var(--t-pill); font-weight: 500;
  white-space: nowrap;
  transition: transform var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn--sm { height: 40px; padding-inline: 1.25rem; }

/* The two button treatments, following the template: the dark one is solid
   ink lit from above by the inset lip and pooling its own colour underneath;
   the pale one is glass, like the picture frames and the label pills. */
.btn--dark {
  background: var(--ink); color: var(--on-ink);
  box-shadow: var(--lip), var(--glow-ink);
}
.btn--light {
  background: var(--btn-glass) border-box; color: var(--ink);
  border: 1px solid var(--surface);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  box-shadow: 0 1px 2px rgba(23, 48, 43, 0.05);
}

/* Round icon button — the contact block's Whatsapp / Email / Phone trio */
.icon-btn {
  display: grid; place-items: center;
  width: 62px; height: 62px;
  border-radius: var(--r-pill);
  background: var(--ink); color: var(--on-ink);
  box-shadow: var(--lip), var(--glow-ink);
  transition: transform var(--dur) var(--ease);
}
.icon-btn:hover { transform: translateY(-2px); }
.icon-btn svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.6; }

.icon-link { display: grid; justify-items: center; gap: 0.5rem; }
/* The label under the circle only — matching every span would also catch the
   .icon-btn and win on specificity, leaving the icon stroked in --ink-45 on
   a dark pill, which is all but invisible. */
.icon-link > span:last-child { font-size: var(--t-eyebrow); color: var(--ink-45); }

/* Filter chips sit inside one light pill-shaped rail.

   The rail scrolls sideways rather than wrapping. Five chips do not fit a
   phone, and wrapping them left a centred orphan on a second line inside a
   pill-shaped rail that had grown too tall to read as a rail at all. One line
   that runs off the edge keeps the shape, keeps every filter one tap away, and
   keeps the sliding pill — which is the piece of this design worth protecting.
   A chip cut off at the edge is also the affordance: it says there is more.

   No media query: fit-content hugs the chips when they fit and collapses to
   the available width when they do not, so the rail is centred and static on a
   desktop and scrollable on a phone from the same rule. justify-content stays
   at the start for the same reason — centred content in an overflowing scroller
   puts its first item off the scrollable origin, where it cannot be reached. */
.chips {
  position: relative;
  display: flex; flex-wrap: nowrap; justify-content: flex-start; gap: 0.25rem;
  width: fit-content; max-width: 100%;
  margin: 0 auto clamp(2rem, 3.5vw, 2.75rem);
  /* The 5px inset around the chips is padding vertically but margin on the
     end chips horizontally. A scroller clips overflow at its padding box while
     anything positioned inside it is held to the content box, so horizontal
     padding here would leave a 5px lane the chips scroll through but the fade
     at the end of the rail can never cover — a sliver of live label beside the
     chevron. With the lane gone the two edges coincide. */
  padding: 0.3125rem 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  /* The end inset is the width of the fade. Choosing a chip scrolls it into
     view, and "into view" without this means flush with the right edge —
     which is under the fade, so the chip you just selected would arrive
     washed out beneath it. */
  scroll-padding-inline: 0.3125rem 3.25rem;
  scrollbar-width: none;
  background: var(--btn-glass) border-box;
  border: 1px solid var(--surface);
  border-radius: var(--r-pill);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  box-shadow: 0 1px 2px rgba(23, 48, 43, 0.05);
}
/* A scrollbar across the rail would sit on top of the chips and break the
   pill shape, so the rail says it scrolls with a soft edge and a chevron
   instead — pinned to the right of the scrollport with position:sticky, and
   pulled back over the last chip with a negative margin so it occupies no
   width of its own. gallery.js only decides whether it is there; a rail with
   nothing more to show fades it out, which includes every desktop. */
.chips::-webkit-scrollbar { width: 0; height: 0; }

.chips__more {
  position: sticky; right: 0; z-index: 2;
  flex: 0 0 auto; align-self: stretch;
  width: 3rem; margin-inline-start: -3rem;
  border-radius: var(--r-pill);
  /* Opaque well before the chevron. The fade has two jobs — saying the rail
     runs on, and clearing the label underneath — and a gentle ramp all the way
     across only does the first: the chevron landed mid-word and read as a
     letter in it. The rail's own glass is nearly this colour at its right end,
     so the opaque part disappears into it. */
  background: linear-gradient(to right, transparent, var(--surface) 55%);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}
.chips.has-more .chips__more { opacity: 1; }
.chips__more::after {
  content: '';
  position: absolute; top: 50%; right: 0.625rem;
  width: 6px; height: 6px; margin-top: -4px;
  border-right: 1.5px solid var(--ink-45);
  border-bottom: 1.5px solid var(--ink-45);
  transform: rotate(-45deg);
}

/* The horizontal half of that inset. :first/last-of-type, not :first/last-child
   — the pill and the scroll hint are both siblings here. */
.chip:first-of-type { margin-inline-start: 0.3125rem; }
.chip:last-of-type { margin-inline-end: 0.3125rem; }

.chip {
  position: relative; z-index: 1;
  flex: 0 0 auto;
  scroll-snap-align: start;
  height: 38px; padding-inline: 1.125rem;
  border-radius: var(--r-pill);
  font-size: var(--t-pill); color: var(--ink-70);
  white-space: nowrap;
  /* The label crosses over on the same clock as the pill: any quicker and the
     incoming chip turns pale before the dark pill is under it. */
  transition: color 380ms var(--ease), background-color var(--dur) var(--ease);
}
.chip:hover { color: var(--ink); }
.chip[aria-pressed='true'] {
  background: var(--ink); color: var(--on-ink);
}

/* With scripting the selected chip's background is lifted out into one pill
   that slides between them, so the chips themselves stay flat and only their
   text colour changes. gallery.js measures the chip and writes the width,
   height and offset; the rail only says how it should move. It is added and
   sized before .is-ready goes on, so the first paint does not animate in from
   the corner. */
.chips__pill {
  position: absolute; top: 0; left: 0; z-index: 0;
  width: 0; height: 0;
  border-radius: var(--r-pill);
  background: var(--ink);
  pointer-events: none;
}
.chips.is-ready .chips__pill {
  transition: transform 380ms var(--ease),
              width 380ms var(--ease),
              height 380ms var(--ease);
}
.chips.is-ready .chip[aria-pressed='true'] { background: transparent; }

/* Small label pill that overlaps the bottom edge of an image card */
.tag {
  display: inline-flex; align-items: center; gap: 0.375rem;
  height: 30px; padding-inline: 0.875rem;
  border-radius: var(--r-pill);
  background: var(--surface);
  box-shadow: var(--shade-soft);
  font-family: var(--font-hand);
  font-size: 0.8125rem; font-weight: 600;
  white-space: nowrap;
}
.tag--dot::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: var(--ink-25);
}
[data-status='available'] .tag--dot::before { background: var(--accent); }


/* 08  Hero ----------------------------------------------------------------
   Giant handwritten name across the top, supporting copy pushed to the right,
   then a wide image plate with two smaller cards floating over its edges. */

.hero { padding-top: clamp(0.5rem, 2vw, 1.5rem); padding-bottom: var(--section); }

.hero__title {
  font-size: var(--t-hero);
  line-height: 0.9;
  text-align: center;
  letter-spacing: -0.02em;
  overflow-wrap: break-word;
}

.hero__aside {
  display: flex; flex-direction: column; align-items: flex-end;
  gap: 1.125rem;
  max-width: 30rem;
  margin: clamp(1.5rem, 3vw, 2.5rem) 0 0 auto;
  text-align: right;
}
.hero__aside p { font-size: var(--t-lead); line-height: 1.65; color: var(--ink-70); }
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.625rem; justify-content: flex-end; }

.plate {
  position: relative;
  margin-top: clamp(2.5rem, 5vw, 4rem);
}
.plate__main {
  border-radius: var(--r-card);
  overflow: hidden;
  box-shadow: var(--shade);
}
.plate__main img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }

/* The two small cards that hang off the plate's left and right edges */
/* Half in, half out — they should read as snapshots dropped on the plate. */
.plate__float {
  position: absolute;
  width: clamp(96px, 12vw, 152px);
}
.plate__float img {
  width: 100%; aspect-ratio: 3 / 4; object-fit: cover;
  border-radius: 14px;
  box-shadow: var(--shade);
}
/* The single column is pinned to the figure's own width. Left implicit it is
   an `auto` track, which floors at the widest item's min-content — and the
   label is nowrap, so a long one ("Saltwater Study") widened the track past
   the width set above and took the image with it: that card rendered wider
   than its neighbour and reached further over the plate's edge. Pinned, the
   image is the authored width and the label is free to overhang it, centred on
   the card the way the ones on the gallery frames are. */
.plate__float { display: grid; grid-template-columns: 100%; justify-items: center; }
.plate__float figcaption { position: relative; z-index: 2; margin-top: calc(-15px - var(--mat-sm)); }
/* Both hang high on the plate, the right one still the lower of the two. They
   are placed against opposite edges — one from the top, one from the bottom —
   so the pair keeps its offset as the plate changes height with the viewport
   instead of drifting towards each other. */
.plate__float--l { left: -4%; top: 15%; }
.plate__float--r { right: -4%; bottom: 7%; }

/* Depth on scroll ---------------------------------------------------------
   The snapshots read as sitting in front of the plate, so as the page scrolls
   they should travel further than it does. site.js writes --lift on each
   [data-parallax] element; the value is authored as 0 so the composition at
   the top of the page is exactly the one laid out above, and everything below
   is pure enhancement — with the script off, or under reduced motion, nothing
   here moves and the hero is unchanged.

   The transform lives on a custom property rather than being written straight
   into style.transform so the rule, not the script, owns the composition.

   Only the snapshots move. The plate image behind them is full-bleed and the
   largest thing on the page, and repainting it every scroll frame cost Firefox
   whole frames — hitches around 100ms, which disappeared once it held still.
   Depth reads from the *gap* between the layers rather than from any one of
   them travelling, so the snapshots took over the plate's share of the drift
   (see the numbers in index.html) and the composition separates exactly as it
   did before, for two small layers a frame instead of two plus a full-width
   image. The plate keeps no transform at all here: an unmoving layer is still
   a layer, and it does not need to be one. */
.plate__float {
  --lift: 0px;
  transform: translate3d(0, var(--lift), 0);
}
.plate.is-parallax .plate__float { will-change: transform; }

.scroll-cue {
  display: block; width: fit-content;
  margin: clamp(2rem, 4vw, 3rem) auto 0;
  font-size: var(--t-eyebrow); color: var(--ink-45);
}


/* 09  Interlude -----------------------------------------------------------
   The template's big empty breather: two lines and a pair of pills, floating
   in a lot of nothing. */

.interlude { text-align: center; }
.interlude__kicker {
  font-family: var(--font-hand);
  font-size: clamp(1rem, 1.6vw, 1.25rem);
  color: var(--ink-45);
  margin-bottom: 0.75rem;
}
.interlude h2 { font-size: var(--t-h2-big); max-width: 20ch; margin-inline: auto; }
.interlude .hero__actions { justify-content: center; margin-top: clamp(1.75rem, 3vw, 2.5rem); }


/* 10  Work cards + grid --------------------------------------------------- */

.grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(1.5rem, 2.4vw, 2.25rem) clamp(0.875rem, 1.6vw, 1.5rem);
}

/* Each card carries a translucent label pill that blurs the artwork behind
   it, and a blur is re-sampled every frame its backdrop moves — i.e. the
   whole way down a scroll, once per card on screen. content-visibility lets
   the browser skip a card entirely — layout, paint and that blur — until it
   is near the viewport, so a scroll only pays for the few cards actually in
   view rather than all fifteen. `auto` means the real height is remembered
   after a card's first render, so the scrollbar does not shift as cards come
   and go. The card still lays out for measurement (the filter's FLIP reads
   its box) and find-in-page still reaches it.

   Height only, and deliberately not the contain-intrinsic-size shorthand: a
   single value there applies to *both* axes, so it also declares a 360px
   intrinsic width. A skipped card would then contribute a 360px min-content
   floor to its `1fr` column, the four tracks would refuse to shrink below
   360px each, and the grid would overflow the viewport with oversized cards
   until a scroll rendered them and replaced the guess with a real measurement.
   The column width is the grid's business; only the height needs guessing. */
.work {
  position: relative; margin: 0;
  content-visibility: auto;
  contain-intrinsic-height: auto 360px;
}

.work__frame {
  display: block; width: 100%; padding: 0;
  border-radius: var(--r-img);
  overflow: hidden;
  background: var(--canvas-2);
  box-shadow: var(--shade-soft);
  cursor: pointer;
}
.work__frame img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 700ms var(--ease);
}
.work__frame:hover img { transform: scale(1.04); }

/* Label pill riding the bottom edge of the picture, as in the menu grid — it
   sits inside the mat rather than on the frame's outer edge, so the band
   still reads all the way round. Kept in normal flow and pulled up by its own
   height plus the band; anchoring it with position:absolute would measure
   from the figure's bottom, which includes the meta line below. */
.work figcaption {
  position: relative; z-index: 2;
  display: grid; justify-items: center;
}
.work figcaption .tag { margin-top: calc(-24px - var(--mat)); }

.work__meta {
  margin-top: 0.75rem;
  font-size: var(--t-eyebrow);
  color: var(--ink-45);
  text-align: center;
}

.grid .work:nth-child(4n + 1) .work__frame { aspect-ratio: 4 / 5; }
.grid .work:nth-child(4n + 2) .work__frame { aspect-ratio: 4 / 5; }
.grid .work:nth-child(4n + 3) .work__frame { aspect-ratio: 4 / 5; }
.grid .work:nth-child(4n + 4) .work__frame { aspect-ratio: 4 / 5; }

.work[hidden] { display: none; }

/* Filtering. A work leaving the archive fades and settles down by the same
   small distance the reveal animation brings it up by, so the grid reads as
   one motion language whichever way a card is travelling. gallery.js hides it
   once the fade is done, then slides the survivors into their new places. */
.work {
  transition: opacity 240ms var(--ease), transform 240ms var(--ease);
}
.work.is-out { opacity: 0; transform: translateY(10px); pointer-events: none; }

.grid-footer { display: flex; justify-content: center; margin-top: clamp(2.5rem, 4.5vw, 3.5rem); }


/* 11  Feature cards -------------------------------------------------------
   Image on top, handwritten title and sans copy beneath, all inside one
   rounded surface. Two up, then an asymmetric text/image pair. */

.features {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(1rem, 2vw, 1.75rem);
}

.feature {
  background: var(--surface);
  border-radius: var(--r-card);
  overflow: hidden;
  box-shadow: var(--shade-soft);
}
.feature img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; }
.feature__body { padding: clamp(1.25rem, 2vw, 1.75rem); }
.feature__body h3 { margin-bottom: 0.5rem; }
.feature__body p { font-size: var(--t-meta); color: var(--ink-70); line-height: 1.65; }

.feature--split {
  grid-column: 1 / -1;
  display: grid; grid-template-columns: 1fr 1fr;
  align-items: stretch;
}
.feature--split .feature__body { display: flex; flex-direction: column; justify-content: center; gap: 0.75rem; align-items: flex-start; }
/* A definite ratio is required here: with height:100% in an auto-height grid
   row the image would fall back to its intrinsic portrait size and stretch
   the whole row to ~760px. */
.feature--split img { aspect-ratio: 3 / 2; height: 100%; object-fit: cover; }


/* 12  Contact panel ------------------------------------------------------- */

.panel {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: clamp(1.75rem, 4vw, 3rem);
  padding: clamp(1.75rem, 4vw, 3.25rem);
  background: var(--surface);
  border-radius: var(--r-card);
  box-shadow: var(--shade-soft);
}
.panel h2 { max-width: 14ch; }
.panel__links { display: flex; gap: clamp(1rem, 2.4vw, 2rem); }

/* The form sits under the panel and takes the same full measure. A 30rem
   column here left two thirds of the row empty, which read as a gap in the
   page rather than as breathing room. Two columns keep the fields from
   stretching into unusably wide single lines at that width: the two short
   answers stack down the left, the message holds the right on its own, and
   the actions run along the bottom. */
.form {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.875rem clamp(1rem, 2.5vw, 1.5rem);
  width: 100%;
}
.form__actions { grid-column: 1 / -1; }

/* Placed rather than flowed: the message has to sit beside both short fields,
   not after them. Spanning their two rows also gives the textarea a height to
   fill, so the column ends level instead of leaving a stub of empty panel. */
.field--message {
  grid-column: 2; grid-row: 1 / span 2;
  grid-template-rows: auto 1fr;
}
.field--message textarea { height: 100%; }

/* The button keeps its own width instead of spanning the measure — a pill
   1000px wide stops reading as a button. The note sits beside it and takes
   the space that would otherwise be empty. */
.form__actions {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 0.75rem clamp(1rem, 2.5vw, 1.5rem);
  margin-top: 0.25rem;
}
.form__actions .btn { flex: 0 0 auto; min-width: 13rem; }

.field { display: grid; gap: 0.375rem; }
.field label { font-size: var(--t-eyebrow); color: var(--ink-45); }
.field input, .field textarea {
  width: 100%; padding: 0.8125rem 1rem;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  font-size: var(--t-body);
  transition: border-color var(--dur) var(--ease);
}
.field input:focus, .field textarea:focus { outline: none; border-color: var(--ink); }
.field textarea { resize: vertical; min-height: 110px; }
.form__note { margin: 0; font-size: var(--t-eyebrow); color: var(--ink-45); }


/* 13  Footer --------------------------------------------------------------
   Full-bleed dark slab with rounded top corners and an oversized wordmark. */

.footer {
  margin-top: var(--section);
  background: var(--ink);
  color: var(--on-ink);
  border-radius: var(--r-slab) var(--r-slab) 0 0;
  padding-block: clamp(2.5rem, 5vw, 4rem) clamp(1.5rem, 3vw, 2.5rem);
}

.footer__wordmark {
  font-family: var(--font-hand);
  font-weight: 600;
  font-size: var(--t-wordmark);
  line-height: 1;
  text-align: center;
  letter-spacing: -0.02em;
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
}

.footer__cols {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: clamp(1.75rem, 4vw, 3rem);
  max-width: 820px; margin-inline: auto;
}
.footer__col li + li { margin-top: 0.625rem; }
.footer__col a {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-size: var(--t-meta);
  color: rgba(233, 237, 231, 0.72);
  transition: color var(--dur) var(--ease);
}
.footer__col a::before { content: '\203A'; color: rgba(233, 237, 231, 0.4); }
.footer__col a:hover { color: var(--on-ink); }

.footer__base {
  display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; justify-content: space-between;
  max-width: 820px; margin: clamp(2.5rem, 5vw, 4rem) auto 0;
  padding-top: 1.5rem;
  border-top: 1px solid rgba(233, 237, 231, 0.14);
  font-size: var(--t-eyebrow);
  color: rgba(233, 237, 231, 0.5);
}


/* 14  Gallery page -------------------------------------------------------- */

.page-head { text-align: center; padding-top: clamp(1rem, 3vw, 2.5rem); }
.page-head h1 { max-width: 16ch; margin-inline: auto; }
.page-head .lead { margin: 1rem auto 0; }

.archive-count { text-align: center; font-size: var(--t-eyebrow); color: var(--ink-45); margin-bottom: 1.75rem; }
.archive-empty { padding-block: 4rem; text-align: center; color: var(--ink-45); }


/* 15  Lightbox ------------------------------------------------------------ */

.lightbox {
  position: fixed; inset: 0; z-index: 150;
  display: none;
  padding: clamp(1rem, 3vw, 2rem);
  background: color-mix(in srgb, var(--canvas) 92%, var(--ink));
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  overflow-y: auto; overscroll-behavior: contain;
}
/* Opening, closing and stepping between works.

   Closing needs the panel to stay on screen while it fades, so `display` can
   only drop once the animation is done — hence the separate is-closing state,
   which gallery.js removes on animationend. It is pointer-events:none and
   inert for that window so nothing can be clicked or tabbed into on its way
   out. Under reduced motion the global rule collapses every duration here to
   nothing and the whole thing becomes the instant swap it was before. */

.lightbox.is-open {
  display: grid;
  animation: lb-in 220ms var(--ease) both;
}
.lightbox.is-closing {
  display: grid;
  pointer-events: none;
  animation: lb-out 150ms ease-in both;
}
.lightbox.is-open .lightbox__inner { animation: lb-plate 340ms var(--ease) both; }

@keyframes lb-in    { from { opacity: 0; } }
@keyframes lb-out   { to   { opacity: 0; } }
@keyframes lb-plate { from { opacity: 0; transform: translateY(14px) scale(0.985); } }

/* Stepping. The artwork slides in from the side you are heading towards; the
   metadata just lifts, so the two do not compete. gallery.js waits for the new
   image to decode before starting this, otherwise the fade would reveal a
   frame of blank while a lazy-loaded work was still arriving. */
.lightbox__inner.is-stepping .lightbox__stage img,
.lightbox__inner.is-stepping .lightbox__panel { opacity: 0; }

.lightbox__inner.is-step-next .lightbox__stage img { animation: lb-next 340ms var(--ease) both; }
.lightbox__inner.is-step-prev .lightbox__stage img { animation: lb-prev 340ms var(--ease) both; }
.lightbox__inner.is-step-next .lightbox__panel,
.lightbox__inner.is-step-prev .lightbox__panel { animation: lb-meta 300ms var(--ease) both; }

@keyframes lb-next { from { opacity: 0; transform: translateX(28px) scale(0.985); } }
@keyframes lb-prev { from { opacity: 0; transform: translateX(-28px) scale(0.985); } }
@keyframes lb-meta { from { opacity: 0; transform: translateY(8px); } }

.lightbox__inner {
  display: grid; grid-template-columns: minmax(0, 1fr) 310px;
  gap: clamp(1.5rem, 4vw, 3rem); align-items: center;
  width: 100%; max-width: 1080px; margin: auto;
}
.lightbox__stage {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 0.75rem;
  min-height: 0;
}
.lightbox__stage img {
  max-height: min(76vh, 780px); width: auto;
  border-radius: var(--r-img);
  box-shadow: var(--shade);
}

.lightbox__panel { display: grid; gap: 1rem; align-content: center; }
.lightbox__panel h2 { font-size: clamp(1.6rem, 3vw, 2.4rem); }
.lightbox__panel dl { display: grid; border-top: 1px solid var(--line); }
.lightbox__panel dl > div {
  display: flex; justify-content: space-between; gap: 1rem;
  padding-block: 0.625rem;
  border-bottom: 1px solid var(--line);
  font-size: var(--t-meta);
}
.lightbox__panel dt { color: var(--ink-45); }
.lightbox__panel dd { margin: 0; text-align: right; }
.lightbox__actions { display: flex; flex-wrap: wrap; gap: 0.625rem; margin-top: 0.375rem; }

.lightbox__close, .lightbox__nav {
  position: absolute; z-index: 2;
  width: 46px; height: 46px;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--btn-glass) border-box;
  border: 1px solid var(--surface);
  box-shadow: 0 1px 2px rgba(23, 48, 43, 0.05);
  font-size: 1.0625rem; line-height: 1;
  transition: transform var(--dur) var(--ease);
}
.lightbox__close:hover, .lightbox__nav:hover { transform: scale(1.06); }
.lightbox__close { top: clamp(1rem, 3vw, 2rem); right: clamp(1rem, 3vw, 2rem); }
.lightbox__nav--prev { left: clamp(1rem, 3vw, 2rem); top: 50%; transform: translateY(-50%); }
.lightbox__nav--next { right: clamp(1rem, 3vw, 2rem); top: 50%; transform: translateY(-50%); }
.lightbox__nav--prev:hover, .lightbox__nav--next:hover { transform: translateY(-50%) scale(1.06); }

/* Under the artwork, in flow, at every width. It used to be parked at the foot
   of the viewport with position:absolute, which is not available to it here:
   .lightbox__inner's entrance animates transform with fill:both, and an element
   with a filling transform animation is the containing block for every
   absolutely positioned descendant. Anything inside the inner measures its
   offsets from the inner, not from the dialog — so `bottom` put the count on
   top of the picture rather than at the bottom of the screen.

   Being in flow is the better answer anyway. The count belongs to the work it
   counts, and reads as its caption rather than as a label for whatever the
   layout happens to end with. */
.lightbox__counter { font-size: var(--t-eyebrow); color: var(--ink-45); }
body.is-locked { overflow: hidden; }


/* 16  About page ---------------------------------------------------------- */

.portrait { border-radius: var(--r-card); overflow: hidden; box-shadow: var(--shade); }
.portrait img { width: 100%; aspect-ratio: 21 / 9; object-fit: cover; }

.bio { display: grid; grid-template-columns: 1fr 1.7fr; gap: clamp(2rem, 5vw, 4rem); align-items: start; }
.bio__facts { display: grid; }
.bio__facts > div {
  display: flex; justify-content: space-between; gap: 1rem;
  padding-block: 0.875rem;
  border-bottom: 1px solid var(--line);
  font-size: var(--t-meta);
}
.bio__facts > div:first-child { border-top: 1px solid var(--line); }
.bio__facts dt { color: var(--ink-45); }
.bio__facts dd { margin: 0; text-align: right; }
.bio__body { display: grid; gap: 1.125rem; max-width: 60ch; }
.bio__body p { color: var(--ink-70); font-size: var(--t-lead); line-height: 1.7; }
.bio__body p:first-child {
  font-family: var(--font-hand); font-weight: 600;
  font-size: var(--t-h3); line-height: 1.3; color: var(--ink);
}

.ledger { max-width: 860px; margin-inline: auto; }
.ledger__row {
  display: grid; grid-template-columns: 5rem 1fr auto;
  gap: 1rem; align-items: baseline;
  padding-block: 1.125rem;
  border-bottom: 1px solid var(--line);
}
.ledger__row:first-child { border-top: 1px solid var(--line); }
.ledger__year { font-size: var(--t-meta); color: var(--ink-45); font-variant-numeric: tabular-nums; }
.ledger__title { font-family: var(--font-hand); font-weight: 600; font-size: 1.0625rem; }
.ledger__venue { font-size: var(--t-meta); color: var(--ink-45); text-align: right; }


/* 17  Image frames --------------------------------------------------------
   One rule for every picture on the site. It sits after the component
   sections because it restates their background, radius and shadow.

   Three things together make the band read as glass rather than a border:
   the diagonal wash, which is lighter at the top-left corner than at the
   bottom-right; the blurred backdrop showing through it; and --shade-card,
   whose six faint layers replace the single shadow line. Nothing here draws
   a hard edge — the frame is defined by falloff.

   The band is a transparent border rather than padding, because a border's
   inner edge is where overflow is clipped: the picture, and the slow zoom on
   hover, stay off the band while the element's background still paints
   underneath it. The inner corner radius is the outer radius minus the band,
   so writing each outer radius as picture radius + mat curves the picture by
   exactly --r-img inside the frame. */

.plate__main,
.work__frame,
.portrait,
.plate__float img,
.lightbox__stage img {
  border: var(--mat) solid transparent;
  border-radius: calc(var(--r-img) + var(--mat));
  background: var(--mat-glass) border-box;
  box-shadow: var(--shade-card);
}

/* The blur belongs only where the band has something to pick up. Blurring a
   uniform backdrop returns that same uniform colour, so on the frames that
   stand on the page — the plate, the fifteen gallery frames, the portrait —
   it cost a backdrop readback per element per frame and drew nothing. On the
   gallery that was fifteen full-size readbacks following the scroll, which is
   most of what made scrolling drag in Firefox, where these are not cheap.
   The lightbox frame is the same case for a different reason: the lightbox
   already blurs the whole page behind itself, so the frame inside it sits on
   a wash that is uniform by the time it gets there — and a backdrop filter
   nested in another one also made the open animation flicker.

   The snapshots below are the one place it reads: they hang over the plate
   image, so their band genuinely carries the picture behind it, softened.
   The label pills keep theirs for the same reason — they sit on the artwork.

   The snapshots are also a sixth of the size, so they take a proportionally
   narrower band — wide enough, though, to read as a card held in front of the
   plate rather than a picture printed on it. And for the same reason they
   carry a longer shadow than the flat frames: the card ramp, plus two wide
   low-opacity layers for the extra distance to the surface behind them. */
.plate__float img {
  border-width: var(--mat-sm);
  border-radius: calc(var(--r-img) + var(--mat-sm));
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  box-shadow: var(--shade-card),
              0 16px 30px -12px rgba(23, 48, 43, 0.14),
              0 32px 56px -24px rgba(23, 48, 43, 0.12);
}

/* Inside a feature card the card itself is the mat, so it takes the glass and
   the picture is inset by the band. The card's radius follows the same sum to
   keep the two corners concentric. */
.feature {
  background: var(--mat-glass) border-box;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border-radius: calc(var(--r-img) + var(--mat));
  box-shadow: var(--shade-card);
}
.feature img {
  margin: var(--mat);
  width: calc(100% - var(--mat) * 2);
  border-radius: var(--r-img);
}
.feature--split img { height: calc(100% - var(--mat) * 2); }

/* The label pills sit on the pictures, so they are cut from the same glass —
   the work beneath shows through them. */
.tag {
  background: var(--mat-glass) border-box;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  box-shadow: var(--shade-card);
}


/* 18  Motion + page transitions ------------------------------------------- */

[data-reveal] {
  opacity: 0; transform: translateY(var(--reveal-rise));
  transition: opacity var(--dur-reveal) var(--ease-reveal),
              transform var(--dur-reveal) var(--ease-reveal);
}
[data-reveal].is-visible { opacity: 1; transform: none; }
html:not(.js) [data-reveal] { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important; scroll-behavior: auto !important;
    /* The reveal stagger is written as an inline transition-delay, so zeroing
       durations alone would still leave elements waiting their turn. */
    animation-delay: 0s !important; transition-delay: 0s !important;
  }
  [data-reveal] { opacity: 1; transform: none; }
}


/* --- Page transitions ----------------------------------------------------
   The whole effect is these two keyframes: the outgoing page dips out fast,
   the incoming one settles up into place. Everything below is just two ways
   of driving them. */

@keyframes page-out { to   { opacity: 0; } }
@keyframes page-in  { from { opacity: 0; transform: translateY(12px); } }
/* Opacity only, for the fallback path — see the long note on Route 2. */
@keyframes page-in-fade { from { opacity: 0; } }

/* Route 1 — native cross-document view transitions (Chrome, Edge, Safari).
   The browser snapshots the old page, loads the new one, and animates
   between them. No JavaScript, and it cannot delay navigation: a browser
   that does not understand this block simply swaps pages as it always did.

   Nested in a media query so that reduced motion gets a plain, instant
   swap rather than a shortened animation. */

@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }

  /* --ease is spelled out as a fallback here. These pseudo-elements live in
     their own tree, and if a browser ever fails to inherit the custom
     property into it, an unresolvable var() would invalidate the whole
     shorthand and leave the page with no transition at all. */
  ::view-transition-old(root) { animation: page-out 130ms ease-in both; }
  ::view-transition-new(root) {
    animation: page-in 400ms var(--ease, cubic-bezier(0.22, 1, 0.36, 1)) both;
  }

  /* Naming the nav lifts it out of the full-page snapshot into its own
     pair, so it cross-fades in place instead of riding the 12px rise with
     the rest of the page. The bar is sticky and identically positioned on
     every page, so there is nothing for the browser to move it between —
     it simply looks like the chrome stayed put while the content changed. */
  .nav-shell { view-transition-name: site-nav; }

  ::view-transition-old(site-nav),
  ::view-transition-new(site-nav) { animation-duration: 180ms; }
}

/* Route 2 — the fallback, for browsers without cross-document transitions
   (Firefox today). `pt` is set on <html> before first paint by the inline
   script in each page head, so the entry fade starts from opacity 0 with no
   flash of finished content first. The exit fade is driven from site.js.

   Deliberately shorter than the native version: this one is real time added
   between the click and the navigation, so it has to stay nearly free. */

/* Two rules govern the fallback fade, and both come from the same fact: here
   the animation runs on the *real* <body>, not on a snapshot the way the
   native route does. So it must stay cheap on live content.

   1. Opacity only — no transform. The native route can afford the 12px rise
      because it animates a static ::view-transition snapshot; a transform
      there is free. On the real body it is not: Firefox re-rasterises the
      whole document — images, sticky nav, the blurred pills — every frame it
      is transformed, which is the transition jank. Worse, an element with a
      running *or filling* transform animation becomes the containing block
      for its fixed descendants (the current value being `none` does not save
      you), and that is what made the lightbox — position: fixed; inset: 0 —
      size itself to the whole document instead of the viewport. Fading
      opacity alone sidesteps both: body is never re-rastered under a
      transform and never becomes a containing block.

   2. `backwards`, not `both`. `backwards` holds opacity 0 before the
      animation starts — the flash this is here to prevent — then lets go once
      it ends. `both` would keep the animation active on body for the whole
      visit, holding it on its own compositor layer and stuttering every later
      scroll. Chrome and Safari never see any of this: they take the native
      route above and never set `pt`. */
html.pt body        { animation: page-in-fade 240ms var(--ease) backwards; }
html.pt-out body    { animation: page-out     140ms ease-in     both; }

@media (prefers-reduced-motion: reduce) {
  html.pt body, html.pt-out body { animation: none; }
}


/* 19  Responsive ---------------------------------------------------------- */

@media (max-width: 1000px) {
  .grid { grid-template-columns: repeat(3, 1fr); }
  .bio { grid-template-columns: 1fr; }
  .lightbox__inner { grid-template-columns: 1fr; }
  .lightbox__stage img { max-height: 50vh; }
  .lightbox__panel { justify-items: center; text-align: center; }
  .lightbox__panel dl { width: 100%; max-width: 420px; }
}

/* The band where the snapshot labels are wider than the snapshots but the
   phone layout has not taken over yet. Centred on a card narrower than itself,
   a label puts half its excess outside the plate, and there the gutter has
   already been spent on the card's own overhang — at 820px the right label
   finished 3px past the viewport, clipped by overflow-x:hidden. The cards hang
   a little less far over the edge through here, which buys the label the room
   to stay centred. Nothing above this width needs it: by 900px the card has
   outgrown its label and the question does not arise. */
@media (max-width: 900px) {
  .plate__float--l { left: -2.5%; }
  .plate__float--r { right: -2.5%; }
}

@media (max-width: 768px) {
  /* The menu is cut from the same glass as the label pills on the artwork:
     the diagonal wash, a blurred backdrop, and --shade-card's falloff instead
     of a shadow line — and the pills' own --mat-glass, not the denser button
     glass. The dense one was the wrong instinct: at 80%-to-opaque the panel
     went flat, an off-white card with no sense of anything behind it, which is
     no more glass than the solid surface it replaced. What makes glass read is
     the backdrop coming through it. This ramp keeps the colour of the page
     visible while the blur keeps its content unreadable, which is the whole
     effect — a tint you can see the page in, not a window you can read it
     through and not a wall.

     Blur is worth paying for here where it is not on the frames: this panel
     genuinely has the page behind it, and there is only ever one of it. */
  .nav__links {
    position: absolute; top: calc(100% + 0.75rem); left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    margin: 0; padding: 0.5rem;
    background: var(--mat-glass) border-box;
    /* Far heavier than the 8px the pills use, and for a different job. On a
       pill the blur is a texture — you are meant to read the artwork through
       it. Here it has to take the backdrop past reading while leaving its
       colour: the wash above is transparent enough that the page would
       otherwise be legible straight through the menu, and two sets of words in
       the same space is not a design. At this radius what comes through is
       tone and nothing else, which is what the transparency is for. */
    -webkit-backdrop-filter: blur(28px);
    backdrop-filter: blur(28px);
    border-radius: var(--r-img);
    box-shadow: var(--shade-card);

    /* Closed, not absent. display:none cannot be animated, so the panel stays
       in the box tree and is hidden by visibility — which, unlike opacity
       alone, still takes it out of the tab order and the accessibility tree.
       Being absolutely positioned it holds no space either way.

       visibility is not interpolable, so it is switched at one end of the
       transition or the other: instantly on opening, so the panel is there to
       be seen fading in, and after the full duration on closing, so it does
       not vanish mid-fade. That is the whole trick. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px) scale(0.98);
    transform-origin: top right;   /* it belongs to the toggle it grew from */
    pointer-events: none;
    transition: opacity 160ms var(--ease),
                transform 200ms var(--ease),
                visibility 0s linear 200ms;
  }
  .nav__links.is-open {
    opacity: 1;
    visibility: visible;
    transform: none;
    pointer-events: auto;
    /* Opening runs a little longer than closing: a menu should arrive with
       some weight and get out of the way promptly. */
    transition: opacity 180ms var(--ease),
                transform 260ms var(--ease),
                visibility 0s;
  }
  .nav__links a {
    min-height: 52px; padding: 0.5rem 0.875rem;
    gap: 0.6875rem;
    border-radius: 12px;
    font-size: 1.0625rem;
  }
  .nav__icon { width: 21px; height: 21px; }
  /* The blur above only works because of this. view-transition-name makes an
     element a backdrop root, and a backdrop root is precisely what a
     backdrop-filter inside it cannot see past: with the name on the shell the
     menu was sampling an empty backdrop and painting nothing, so the page read
     straight through the glass however high the radius went. No radius could
     have fixed it — the filter was not failing, it had nothing to filter.

     So at this width the name moves off the menu's ancestor and onto the two
     things the bar actually shows here. They still cross-fade in place, which
     is all the name was ever for; the shell itself carries only a gradient of
     the page colour, and nothing about it riding the page rise is visible.
     Naming a sibling rather than the parent keeps this declarative — the
     alternative, dropping the name while the menu is open, has to survive the
     closing animation and a link click that navigates, and gets the flicker
     back if it mistimes either. */
  .nav-shell { view-transition-name: none; }
  .nav__mark { view-transition-name: site-mark; }
  .nav__toggle { view-transition-name: site-toggle; }

  .nav__toggle { display: flex; }
  .nav > .btn { display: none; }
  .only-mobile { display: block; }

  .grid { grid-template-columns: repeat(2, 1fr); }
  .features, .feature--split { grid-template-columns: 1fr; }
  .feature--split img { min-height: 200px; }
  .hero__aside { align-items: center; text-align: center; margin-inline: auto; }
  .hero__actions { justify-content: center; }
  .panel { flex-direction: column; align-items: flex-start; text-align: left; }
  .footer__cols { grid-template-columns: repeat(2, 1fr); }
  .ledger__row { grid-template-columns: 4rem 1fr; }
  .ledger__venue { grid-column: 2; text-align: left; }
}

@media (max-width: 560px) {
  .grid { grid-template-columns: 1fr 1fr; }
  /* Pulled fully inside the plate, the pair stopped reading as snapshots
     hanging off its edges and started reading as stickers dropped on the
     photograph — sat over the middle of it, covering the subject.

     What forced them inward was never the card: it was the label, which used
     to widen the card's own grid track and then hang past the gutter, where
     overflow-x:hidden cuts it. That is fixed at the rule itself — see the
     pinned column there — so the cards can go back on the edges.

     The overhang is a length rather than the desktop rule's percentage: what
     it has to clear is the gutter, which is a fixed 20px below 500px wide, so
     a percentage would be at its widest exactly where there is least room —
     around 21px of overhang at 560, against 22px of gutter. Twelve keeps the
     same clearance at every width down to the narrowest phone.

     Vertically they move to opposite ends rather than the middle. The left one
     is not taken all the way up: the drift is upward, and a card that starts
     flush with the top edge climbs off the plate while the hero is still in
     full view. */
  .plate__float { width: 84px; }
  /* The one width where the labels cannot be centred on their cards: they run
     half as wide again as an 84px card, and centred, half of that overhang
     lands outside the plate on top of a 20px gutter. Leaning each label to its
     card's inward edge puts all of it over the plate instead. */
  .plate__float--l { left: -12px; top: 10%; justify-items: start; }
  .plate__float--r { right: -12px; bottom: 8%; justify-items: end; }
  .plate__float .tag { font-size: 0.6875rem; padding-inline: 0.625rem; }
  /* Two columns rather than one. Stacked, the nine links became a single
     column most of a screen tall, which put the copyright line well below the
     fold and made the footer read as another section instead of the end of
     the page. Two rows fold it back into one view.

     The third group takes the second row to itself: three groups across two
     columns otherwise leaves it alone in a half-width cell, which reads as a
     wrap that went wrong rather than as a shape. */
  .footer__cols { grid-template-columns: repeat(2, 1fr); text-align: center; }
  .footer__col:last-child { grid-column: 1 / -1; }
  .footer__col a { justify-content: center; }
  .footer__base { justify-content: center; text-align: center; }
  .panel__links { flex-wrap: wrap; }
  /* Two input columns stop being readable well before this width. Placement
     has to be undone with it, or the message would keep a column that no
     longer exists. */
  .form { grid-template-columns: 1fr; }
  .field--message { grid-column: auto; grid-row: auto; }
  .form__actions .btn { flex: 1 1 auto; }
  .icon-btn { width: 54px; height: 54px; }

  .lightbox__close, .lightbox__nav { position: fixed; }
  .lightbox__nav { top: auto; bottom: 1rem; transform: none; }
  .lightbox__nav--prev { left: 1rem; }
  .lightbox__nav--next { right: 1rem; }
  .lightbox__nav--prev:hover, .lightbox__nav--next:hover { transform: none; }
  .lightbox__inner { padding-bottom: 4.5rem; }
}

@media print {
  .nav-shell, .lightbox, .scroll-cue { display: none; }
  body { background: #fff; }
  .footer { background: none; color: #000; }
}
