/* =========================================================================
   matthewarcher.ca — dark gallery
   Cascade layers keep specificity flat; everything is overridable from
   `utilities` without a single !important.
   ========================================================================= */

@layer reset, base, layout, components, utilities;

/* --- Design tokens ---------------------------------------------------- */
:root {
  /* Near-black rather than pure black: photographs keep their shadow
     detail against it, and it reads as paper rather than void. */
  --bg:        #0b0b0c;
  --bg-elev:   #141416;
  --bg-sunk:   #080809;
  --line:      rgb(255 255 255 / 10%);
  --line-soft: rgb(255 255 255 / 6%);

  --text:      #f2efea;   /* warm off-white, not #fff — easier on the eye */
  --text-dim:  #a5a19a;
  --text-faint:#6f6c67;

  /* Aurora green, heavily desaturated. Used only for focus and hover so it
     never competes with the photographs. */
  --accent:    #8fd6b4;
  --accent-dim: color-mix(in oklab, var(--accent) 35%, var(--bg));

  --font-display: "Instrument Serif", ui-serif, Georgia, "Times New Roman", serif;
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               Helvetica, Arial, sans-serif;

  /* Fluid type — scales continuously between 360px and 1440px viewports,
     so there are no jarring breakpoint jumps. */
  --step--1: clamp(0.83rem, 0.79rem + 0.18vw, 0.94rem);
  --step-0:  clamp(1rem, 0.95rem + 0.24vw, 1.15rem);
  --step-1:  clamp(1.25rem, 1.15rem + 0.45vw, 1.55rem);
  --step-2:  clamp(1.6rem, 1.4rem + 0.95vw, 2.3rem);
  --step-3:  clamp(2.1rem, 1.7rem + 1.9vw, 3.4rem);
  --step-4:  clamp(2.7rem, 1.9rem + 3.6vw, 5.5rem);

  --gutter: clamp(1.15rem, 0.8rem + 1.8vw, 2.75rem);
  --measure: 62ch;
  --radius: 3px;

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

  --header-h: 4.25rem;
  color-scheme: dark;
}

/* --- Reset ------------------------------------------------------------ */
@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
  * { margin: 0; }

  html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    scroll-padding-top: calc(var(--header-h) + 1rem);
  }

  body {
    min-height: 100svh;
    font-family: var(--font-body);
    font-size: var(--step-0);
    line-height: 1.65;
    color: var(--text);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
  }

  img, picture, svg, video {
    display: block;
    max-width: 100%;
    height: auto;
  }

  input, button, textarea, select { font: inherit; color: inherit; }
  button { background: none; border: 0; cursor: pointer; }

  h1, h2, h3, h4 {
    font-weight: 400;
    line-height: 1.08;
    text-wrap: balance;      /* avoids orphaned words in headings */
  }
  p { text-wrap: pretty; }

  ul[role="list"] { list-style: none; padding: 0; }

  @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;
    }
  }
}

/* --- Base ------------------------------------------------------------- */
@layer base {
  h1, h2, .display {
    font-family: var(--font-display);
    font-weight: 400;
    letter-spacing: -0.015em;
  }

  a { color: inherit; text-decoration-color: rgb(255 255 255 / 28%); text-underline-offset: 0.25em; }
  a:hover { text-decoration-color: var(--accent); }

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

  ::selection { background: var(--accent); color: #06120c; }

  .skip-link {
    position: absolute;
    left: 0.5rem;
    top: -100%;
    z-index: 200;
    padding: 0.7rem 1.1rem;
    background: var(--text);
    color: var(--bg);
    border-radius: var(--radius);
    transition: top 180ms var(--ease);
  }
  .skip-link:focus { top: 0.5rem; }

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

/* --- Layout ----------------------------------------------------------- */
@layer layout {
  .wrap {
    width: min(100% - (var(--gutter) * 2), 1320px);
    margin-inline: auto;
  }
  .wrap--narrow { width: min(100% - (var(--gutter) * 2), 760px); }
  .wrap--wide   { width: min(100% - (var(--gutter) * 2), 1560px); }

  .section { padding-block: clamp(3.5rem, 2rem + 7vw, 8rem); }
  .section--tight { padding-block: clamp(2.5rem, 1.5rem + 4vw, 5rem); }

  /* Half the usual gap below the header. A full section's worth of air under
     the menu bar pushed the page heading too far down the screen. */
  main > .section:first-child { padding-top: clamp(1.75rem, 1rem + 3.5vw, 4rem); }

  .stack > * + * { margin-top: var(--flow, 1.1em); }
}

/* --- Header ----------------------------------------------------------- */
@layer components {
  .site-header {
    position: sticky;
    top: 0;
    z-index: 100;
    height: var(--header-h);
    display: flex;
    align-items: center;
    /* Translucent over the photographs, opaque enough to stay legible. */
    /* Solid first: if color-mix() isn't understood the declaration is dropped,
       and a header with no background at all is unreadable over a photograph. */
    background: rgb(11 11 12 / 78%);
    background: color-mix(in oklab, var(--bg) 72%, transparent);
    backdrop-filter: blur(14px) saturate(140%);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    border-bottom: 1px solid transparent;
    transition: border-color 300ms var(--ease), background 300ms var(--ease);
  }
  .site-header[data-scrolled="true"] {
    border-bottom-color: var(--line-soft);
    background: rgb(11 11 12 / 94%);
    background: color-mix(in oklab, var(--bg) 92%, transparent);
  }

  .site-header__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
    width: min(100% - (var(--gutter) * 2), 1560px);
    margin-inline: auto;
  }

  .brand {
    font-family: var(--font-display);
    /* Fluid, so the full wordmark and the nav don't collide just above the
       mobile breakpoint. */
    font-size: clamp(1.05rem, 0.85rem + 0.7vw, 1.32rem);
    letter-spacing: 0.01em;
    text-decoration: none;
    white-space: nowrap;
  }
  .brand span { color: var(--text-faint); }

  @media (max-width: 46rem) {
    /* The business name can't share a 375px header with the menu button, and
       shrinking it far enough to fit would make it unreadable. The personal
       name carries the header on phones instead. */
    .brand span { display: none; }
  }

  .nav { display: flex; align-items: center; gap: clamp(1rem, 2.4vw, 2.2rem); }
  .nav a {
    position: relative;
    font-size: var(--step--1);
    letter-spacing: 0.055em;
    text-transform: uppercase;
    text-decoration: none;
    color: var(--text-dim);
    padding-block: 0.5rem;
    transition: color 220ms var(--ease);
  }
  .nav a::after {
    content: "";
    position: absolute;
    left: 0; bottom: 0.15rem;
    width: 100%; height: 1px;
    background: var(--accent);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform 320ms var(--ease);
  }
  .nav a:hover, .nav a[aria-current] { color: var(--text); }
  .nav a:hover::after, .nav a[aria-current]::after { transform: scaleX(1); }

  /* Mobile drawer */
  .nav-toggle { display: none; padding: 0.6rem; margin-right: -0.6rem; }
  .nav-toggle svg { width: 22px; height: 22px; stroke: currentColor; fill: none; stroke-width: 1.5; }

  @media (max-width: 46rem) {
    .nav-toggle { display: block; }
    .nav {
      position: fixed;
      inset: var(--header-h) 0 auto 0;
      flex-direction: column;
      align-items: flex-start;
      gap: 0;
      padding: 1rem var(--gutter) 2rem;
      /* Fully opaque on purpose. A drawer sitting over a photograph has to be
         readable, and translucency buys nothing at this size. */
      background: var(--bg);
      border-bottom: 1px solid var(--line-soft);
      box-shadow: 0 18px 40px rgb(0 0 0 / 55%);
      /* Collapsed by default; JS flips the attribute. */
      display: none;
    }
    .nav[data-open="true"] { display: flex; }
    .nav a { width: 100%; padding-block: 0.95rem; font-size: var(--step-0); }
    .nav a + a { border-top: 1px solid var(--line-soft); }
  }

  /* --- Hero ----------------------------------------------------------- */
  .hero {
    position: relative;
    /* svh so mobile browser chrome doesn't crop the image. */
    min-height: min(92svh, 900px);
    display: grid;
    align-items: end;
    isolation: isolate;
    margin-top: calc(var(--header-h) * -1);
    padding-top: var(--header-h);
    overflow: clip;
  }
  .hero__media { position: absolute; inset: 0; z-index: -2; }
  .hero__media img { width: 100%; height: 100%; object-fit: cover; }
  .hero::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    /* Two-stop scrim: keeps type legible without flattening the photo. */
    background:
      linear-gradient(to top, var(--bg) 2%, rgb(11 11 12 / 45%) 38%, rgb(11 11 12 / 12%) 70%),
      linear-gradient(to right, rgb(11 11 12 / 55%), transparent 65%);
  }
  .hero__inner { padding-block: clamp(3rem, 8vw, 7rem); }
  .hero__title {
    font-size: var(--step-4);
    max-width: 18ch;
    white-space: pre-line;   /* honours the line break in the copy */
  }
  .hero__sub {
    margin-top: 1.4rem;
    max-width: 46ch;
    color: var(--text-dim);
    font-size: var(--step-1);
    line-height: 1.5;
  }
  .hero__scroll {
    margin-top: 2.6rem;
    display: inline-flex;
    align-items: center;
    gap: 0.7rem;
    font-size: var(--step--1);
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--text-dim);
    text-decoration: none;
  }
  .hero__scroll svg { width: 1rem; height: 1rem; stroke: currentColor; fill: none; stroke-width: 1.5; }
  .hero__scroll:hover { color: var(--text); }
  .hero__scroll:hover svg { animation: nudge 900ms var(--ease) infinite; }
  @keyframes nudge { 50% { transform: translateY(3px); } }

  /* --- Homepage intro ------------------------------------------------- */
  /* Portrait left, words right on desktop. The image comes first in the
     markup, so on a phone it stacks above the text without any reordering. */
  .intro-grid {
    display: grid;
    gap: clamp(1.75rem, 4vw, 4rem);
    align-items: center;
  }
  @media (min-width: 62rem) {
    .intro-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
    .intro__body { justify-self: end; }
  }
  .intro__media img {
    width: 100%;
    height: auto;
    border-radius: var(--radius);
  }
  .intro__body { max-width: var(--measure); }

  /* --- Section furniture ---------------------------------------------- */
  .eyebrow {
    font-size: var(--step--1);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-faint);
    margin-bottom: 1rem;
  }
  .section-title { font-size: var(--step-3); }
  .lede { font-size: var(--step-1); color: var(--text-dim); line-height: 1.55; max-width: var(--measure); }
  .prose { max-width: var(--measure); color: var(--text-dim); }
  .prose p + p { margin-top: 1.15em; }
  .prose strong { color: var(--text); font-weight: 500; }

  .rule { height: 1px; background: var(--line-soft); border: 0; }

  /* --- Buttons -------------------------------------------------------- */
  .btn {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.85rem 1.6rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    font-size: var(--step--1);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: none;
    color: var(--text);
    background: transparent;
    transition: background 260ms var(--ease), border-color 260ms var(--ease),
                transform 260ms var(--ease);
  }
  .btn:hover {
    background: rgb(255 255 255 / 6%);
    border-color: rgb(255 255 255 / 26%);
    transform: translateY(-2px);
  }
  .btn--solid { background: var(--text); color: var(--bg); border-color: var(--text); }
  .btn--solid:hover { background: #fff; color: var(--bg); }

  /* --- Category cards ------------------------------------------------- */
  .cat-grid {
    display: grid;
    gap: clamp(0.6rem, 1.4vw, 1.15rem);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
  }
  /* First card spans two columns where there's room — breaks the monotony
     of an even grid without needing a bespoke layout per page. */
  @media (min-width: 60rem) {
    .cat-grid > :first-child { grid-column: span 2; }
  }

  .cat-card {
    position: relative;
    display: block;
    overflow: hidden;
    border-radius: var(--radius);
    background: var(--bg-elev);
    text-decoration: none;
    isolation: isolate;
    aspect-ratio: 3 / 2;
  }
  .cat-card img {
    width: 100%; height: 100%;
    object-fit: cover;
    transition: transform 900ms var(--ease), filter 600ms var(--ease);
    filter: saturate(0.9) brightness(0.82);
  }
  .cat-card:hover img, .cat-card:focus-visible img {
    transform: scale(1.045);
    filter: saturate(1) brightness(0.95);
  }
  .cat-card__body {
    position: absolute;
    inset: auto 0 0 0;
    padding: clamp(1.1rem, 2.4vw, 2rem);
    background: linear-gradient(to top, rgb(8 8 9 / 88%), rgb(8 8 9 / 35%) 55%, transparent);
  }
  .cat-card__title { font-family: var(--font-display); font-size: var(--step-2); }
  .cat-card__meta { font-size: var(--step--1); color: var(--text-dim); letter-spacing: 0.06em; text-transform: uppercase; }
  .cat-card__blurb {
    margin-top: 0.5rem;
    font-size: var(--step--1);
    color: var(--text-dim);
    max-width: 48ch;
    line-height: 1.5;
  }

  /* --- Masonry gallery ------------------------------------------------ */
  /* CSS multi-column gives true masonry with no JS and no layout thrash.
     Each figure carries an explicit aspect-ratio so nothing shifts on load. */
  .gallery {
    columns: 1;
    column-gap: clamp(0.55rem, 1.2vw, 1rem);
  }
  @media (min-width: 34rem) { .gallery { columns: 2; } }
  @media (min-width: 62rem) { .gallery { columns: 3; } }
  @media (min-width: 100rem) { .gallery { columns: 4; } }

  .shot {
    break-inside: avoid;
    margin-bottom: clamp(0.55rem, 1.2vw, 1rem);
    position: relative;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg-elev);
  }
  .shot__btn {
    display: block;
    width: 100%;
    position: relative;
    cursor: zoom-in;
  }
  .shot img {
    width: 100%;
    height: auto;
    transition: transform 800ms var(--ease), opacity 500ms var(--ease);
  }
  .shot:hover img { transform: scale(1.035); }

  /* Blur-up: the LQIP data-URI sits behind, revealed until the real file
     decodes. `.is-loaded` is set from JS on the image's load event. */
  .shot__ph {
    position: absolute;
    inset: 0;
    background-size: cover;
    background-position: center;
    filter: blur(14px);
    transform: scale(1.1);
    transition: opacity 600ms var(--ease);
  }
  .shot img.is-loaded + .shot__ph { opacity: 0; }

  .shot__cap {
    position: absolute;
    inset: auto 0 0 0;
    padding: 1.5rem 1rem 0.9rem;
    display: flex;
    justify-content: flex-end;   /* settings only — no title beside them */
    align-items: flex-end;
    gap: 1rem;
    background: linear-gradient(to top, rgb(8 8 9 / 85%), transparent);
    opacity: 0;
    transform: translateY(6px);
    transition: opacity 320ms var(--ease), transform 320ms var(--ease);
    pointer-events: none;
  }
  .shot:hover .shot__cap,
  .shot__btn:focus-visible .shot__cap { opacity: 1; transform: none; }
  .shot__exif { font-size: 0.72rem; color: var(--text-dim); font-variant-numeric: tabular-nums; white-space: nowrap; }

  @media (hover: none) {
    /* No hover on touch — keep captions permanently visible but quiet. */
    .shot__cap { opacity: 1; transform: none; }
  }

  /* --- Filter bar ----------------------------------------------------- */
  .subnav {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 2.5rem;
  }
  .subnav a {
    padding: 0.5rem 1.05rem;
    border: 1px solid var(--line-soft);
    border-radius: 100px;
    font-size: var(--step--1);
    text-decoration: none;
    color: var(--text-dim);
    transition: all 240ms var(--ease);
  }
  .subnav a:hover { color: var(--text); border-color: var(--line); }
  .subnav a[aria-current] {
    background: var(--text);
    color: var(--bg);
    border-color: var(--text);
  }

  /* --- Lightbox ------------------------------------------------------- */
  .lightbox {
    position: fixed;
    inset: 0;
    z-index: 300;
    display: grid;
    grid-template-rows: auto 1fr auto;
    background: rgb(6 6 7 / 97%);
    backdrop-filter: blur(6px);
    opacity: 0;
    visibility: hidden;
    transition: opacity 300ms var(--ease), visibility 300ms;
  }
  .lightbox[data-open="true"] { opacity: 1; visibility: visible; }

  .lightbox__bar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
    padding: 1rem var(--gutter);
    font-size: var(--step--1);
    color: var(--text-dim);
  }
  .lightbox__stage {
    display: grid;
    place-items: center;
    padding: 0 clamp(0.5rem, 4vw, 4rem);
    min-height: 0;   /* lets the image shrink inside the grid row */
    overflow: hidden;
  }
  .lightbox__img {
    /* An explicit box plus object-fit, rather than max-width/max-height.
       A percentage max-height against a grid-stretched parent doesn't
       resolve here, so tall images kept their intrinsic height and ran off
       the bottom of a wide desktop window — narrow screens hid the bug
       because max-width bound first. Sizing the box and letting object-fit
       letterbox inside it is reliable at every viewport. */
    width: 100%;
    height: 100%;
    min-height: 0;
    object-fit: contain;
    opacity: 0;
    transition: opacity 320ms var(--ease);
  }
  .lightbox__img.is-ready { opacity: 1; }

  .lightbox__foot {
    display: flex;
    justify-content: center;   /* settings alone, centred under the photograph */
    align-items: baseline;
    gap: 1.5rem;
    flex-wrap: wrap;
    padding: 1rem var(--gutter) 1.6rem;
  }
  .lightbox__meta { font-size: var(--step--1); color: var(--text-faint); font-variant-numeric: tabular-nums; }

  .lb-btn {
    display: grid;
    place-items: center;
    width: 2.9rem; height: 2.9rem;
    border-radius: 50%;
    border: 1px solid var(--line-soft);
    color: var(--text);
    transition: background 220ms var(--ease), border-color 220ms var(--ease);
  }
  .lb-btn:hover { background: rgb(255 255 255 / 9%); border-color: var(--line); }
  .lb-btn svg { width: 1.15rem; height: 1.15rem; stroke: currentColor; fill: none; stroke-width: 1.6; }
  .lb-btn[disabled] { opacity: 0.28; pointer-events: none; }

  .lightbox__nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
  }
  .lightbox__nav--prev { left: clamp(0.4rem, 1.6vw, 1.4rem); }
  .lightbox__nav--next { right: clamp(0.4rem, 1.6vw, 1.4rem); }

  /* --- Forms ---------------------------------------------------------- */
  .field { display: grid; gap: 0.5rem; }
  .field label { font-size: var(--step--1); letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-dim); }
  .field :is(input, textarea, select) {
    width: 100%;
    padding: 0.85rem 1rem;
    background: var(--bg-elev);
    border: 1px solid var(--line-soft);
    border-radius: var(--radius);
    transition: border-color 220ms var(--ease), background 220ms var(--ease);
  }
  .field :is(input, textarea, select):focus {
    outline: none;
    border-color: var(--accent);
    background: var(--bg-sunk);
  }
  .field textarea { resize: vertical; min-height: 9rem; }
  .form-grid { display: grid; gap: 1.35rem; }
  @media (min-width: 44rem) { .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.35rem; } }

  .form-note {
    padding: 0.9rem 1.1rem;
    border: 1px dashed var(--line);
    border-radius: var(--radius);
    font-size: var(--step--1);
    color: var(--text-dim);
    background: var(--bg-elev);
  }
  .form-status { font-size: var(--step--1); min-height: 1.4em; }
  .form-status[data-state="error"] { color: #ff9b8a; }
  .form-status[data-state="ok"] { color: var(--accent); }

  /* Honeypot — bots fill it, humans never see it. */
  .hp { position: absolute; left: -9999px; opacity: 0; pointer-events: none; }

  /* --- Print carousel --------------------------------------------------- */
  .carousel { position: relative; }

  /* Coverflow: the front photograph square-on, its neighbours falling back
     into the scene at an angle. Each slide occupies the same box and the
     photograph is fitted inside it whole, so nothing is cropped and the row
     stays visually regular despite ratios running 5:4 to 42:11.
     JS sets the per-slide transform; the CSS just establishes the space. */
  .carousel__stage {
    position: relative;
    height: clamp(320px, 58vh, 640px);
    perspective: 1600px;
    perspective-origin: 50% 50%;
    overflow: hidden;
  }
  .carousel__slide {
    position: absolute;
    top: 50%;
    left: 50%;
    width: min(56%, 760px);
    height: 88%;
    margin: 0;
    transform-style: preserve-3d;
    /* JS overrides transform/opacity/z-index per slide each time it moves. */
    transform: translate(-50%, -50%);
    opacity: 0;
    pointer-events: none;
    transition: transform 700ms var(--ease), opacity 700ms var(--ease);
    will-change: transform, opacity;
  }
  .carousel__slide.is-visible { pointer-events: auto; }
  .carousel__slide img {
    /* Absolutely positioned against the slide, not sized by percentages.
       Percentage width/height on a centred grid item does not reliably
       resolve — portrait frames computed 1140px tall inside a 459px box and
       overflowed the stage. inset:0 always resolves, and object-fit then
       letterboxes every ratio from 5:4 to 42:11 inside the same footprint. */
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    border-radius: 2px;
    filter: drop-shadow(0 24px 40px rgb(0 0 0 / 55%));
  }
  /* Everything behind the front slide is dimmed so the eye knows where to go. */
  .carousel__slide::after {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--bg);
    opacity: 0;
    transition: opacity 700ms var(--ease);
    pointer-events: none;
  }
  .carousel__slide:not(.is-active)::after { opacity: 0.55; }

  @media (max-width: 46rem) {
    /* Less room to play with — bring the neighbours in tighter. */
    .carousel__slide { width: min(74%, 420px); }
  }

  .carousel__nav {
    position: absolute;
    top: clamp(150px, 28vh, 310px);
    transform: translateY(-50%);
    z-index: 2;
    background: color-mix(in oklab, var(--bg) 70%, transparent);
    background: rgb(11 11 12 / 70%);
  }
  .carousel__nav--prev { left: clamp(0.4rem, 1.5vw, 1.1rem); }
  .carousel__nav--next { right: clamp(0.4rem, 1.5vw, 1.1rem); }

  .carousel__bar {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1.5rem;
    flex-wrap: wrap;
    margin-top: 1rem;
  }
  .carousel__caption { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; }

  /* --- Cover variant (portfolio collections) ----------------------------
     Same coverflow depth as the prints carousel, but each slide is a single
     category cover with its title and description laid over the image, and
     the whole slide is the link into that collection. */
  .carousel--cover .carousel__stage { height: clamp(340px, 62vh, 660px); }
  .carousel--cover .carousel__slide {
    overflow: hidden;
    border-radius: var(--radius);
    isolation: isolate;
  }
  .carousel--cover .carousel__slide img {
    /* Cover, not contain: a category image is a doorway rather than the
       artwork, so filling its frame reads better than letterboxing it. */
    object-fit: cover;
    filter: saturate(0.92) brightness(0.82);
    transition: transform 900ms var(--ease), filter 600ms var(--ease);
    border-radius: 0;
  }
  .carousel--cover .carousel__slide.is-active img { filter: saturate(1) brightness(0.9); }
  .carousel--cover .carousel__slide.is-active:hover img { transform: scale(1.04); }
  /* The dimming veil is on the image itself here, so drop the shared one. */
  .carousel--cover .carousel__slide::after { content: none; }
  .carousel--cover .carousel__slide .cat-card__body {
    z-index: 2;
    padding: clamp(1rem, 2.5vw, 2rem);
  }
  /* Only the front slide's caption is legible; the rest would be noise. */
  .carousel--cover .carousel__slide:not(.is-active) .cat-card__body { opacity: 0; }
  .carousel--cover .carousel__slide .cat-card__body { transition: opacity 500ms var(--ease); }
  .carousel--cover .carousel__slide .cat-card__blurb { max-width: 48ch; }
  .carousel__title {
    font-family: var(--font-display);
    font-size: var(--step-2);
    text-decoration: none;
  }
  .carousel__price { font-size: var(--step--1); color: var(--text-dim); white-space: nowrap; }
  .carousel__pause {
    font-size: var(--step--1);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-faint);
    padding: 0.3rem 0;
  }
  .carousel__pause:hover { color: var(--text); }

  /* Thumbnails double as navigation and as a way to see the whole selection
     at once — a carousel on its own is a poor way to browse 22 things. */
  .carousel__thumbs {
    display: flex;
    gap: 0.5rem;
    margin-top: 1rem;
    padding-bottom: 0.5rem;
    overflow-x: auto;
    scrollbar-width: thin;
    scroll-snap-type: x proximity;
    /* Centred when the row fits, still scrollable when it doesn't —
       justify-content alone would clip the overflowing end. */
    justify-content: safe center;
  }
  /* Keeps the strip centred as a block once it overflows its container. */
  .carousel__thumbs > * { margin-block: 0; }
  .carousel__thumb {
    flex: 0 0 auto;
    width: 74px;
    height: 56px;
    border-radius: 2px;
    overflow: hidden;
    opacity: 0.4;
    transition: opacity 240ms var(--ease), outline-color 240ms var(--ease);
    outline: 2px solid transparent;
    scroll-snap-align: start;
  }
  .carousel__thumb img { width: 100%; height: 100%; object-fit: cover; }
  .carousel__thumb:hover { opacity: 0.8; }
  .carousel__thumb.is-active { opacity: 1; outline-color: var(--accent); }

  @media (prefers-reduced-motion: reduce) {
    .carousel__slide { transition: none; }
  }

  /* First paint only: the markup ships with slide 0 active and JS immediately
     moves to a random one. Animating that swap made the caption flash in and
     out, so the opening placement is instant. */
  .carousel.is-initialising .carousel__slide,
  .carousel.is-initialising .cat-card__body { transition: none; }

  /* --- Print ordering -------------------------------------------------- */
  .print-grid {
    display: grid;
    gap: clamp(1rem, 2.2vw, 1.8rem);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
  }
  .print-card { display: block; text-decoration: none; }
  .print-card__media {
    overflow: hidden;
    border-radius: var(--radius);
    background: var(--bg-elev);
    aspect-ratio: 4 / 5;
  }
  .print-card__media img {
    width: 100%; height: 100%;
    object-fit: cover;
    transition: transform 800ms var(--ease);
  }
  .print-card:hover .print-card__media img { transform: scale(1.04); }
  .print-card__body {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    margin-top: 0.7rem;
  }
  .print-card__title { font-family: var(--font-display); font-size: var(--step-1); }
  .print-card__price { font-size: var(--step--1); color: var(--text-dim); white-space: nowrap; }

  .print-detail { display: grid; gap: clamp(1.75rem, 4vw, 3.5rem); }
  @media (min-width: 62rem) {
    .print-detail { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); align-items: start; }
    .print-detail__media { position: sticky; top: calc(var(--header-h) + 2rem); }
  }
  .print-detail__media img { width: 100%; height: auto; border-radius: var(--radius); }

  .opt-set { border: 0; padding: 0; margin: 0; }
  .opt-set legend {
    padding: 0;
    font-size: var(--step--1);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-faint);
    margin-bottom: 0.7rem;
  }
  .opt-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
  /* The radio itself is hidden but still focusable, so keyboard and screen
     reader users get real radio-group semantics rather than styled divs. */
  .opt input { position: absolute; opacity: 0; width: 0; height: 0; }
  .opt span {
    display: block;
    padding: 0.55rem 1rem;
    border: 1px solid var(--line-soft);
    border-radius: 100px;
    font-size: var(--step--1);
    color: var(--text-dim);
    cursor: pointer;
    transition: all 200ms var(--ease);
    font-variant-numeric: tabular-nums;
  }
  .opt input:checked + span {
    background: var(--text);
    color: var(--bg);
    border-color: var(--text);
  }
  .opt input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
  .opt span:hover { border-color: var(--line); color: var(--text); }
  .opt-note { font-size: var(--step--1); color: var(--text-faint); margin-top: 0.6rem; }

  .print-price {
    font-family: var(--font-display);
    font-size: var(--step-3);
    line-height: 1;
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
  }
  .print-price__cur {
    font-family: var(--font-body);
    font-size: var(--step--1);
    color: var(--text-faint);
    letter-spacing: 0.08em;
  }

  /* --- Booking block --------------------------------------------------- */
  .booking {
    padding: clamp(1.25rem, 3vw, 1.9rem);
    border: 1px solid var(--line-soft);
    border-radius: var(--radius);
    background: var(--bg-elev);
  }
  .booking__title {
    font-family: var(--font-display);
    font-size: var(--step-2);
  }

  /* --- Contact / meta list -------------------------------------------- */
  .meta-list { display: grid; gap: 1.5rem; }
  .meta-list dt {
    font-size: var(--step--1);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-faint);
    margin-bottom: 0.3rem;
  }
  .meta-list dd { margin: 0; }
  .meta-list a { text-decoration: none; border-bottom: 1px solid var(--line); padding-bottom: 1px; }
  .meta-list a:hover { border-color: var(--accent); color: var(--accent); }

  /* --- Social links --------------------------------------------------- */
  /* Held back at 62% so the marks read as quiet wayfinding next to the
     photographs, then come up to full on hover and keyboard focus. */
  .social-link {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    text-decoration: none;
    border: 0;
    opacity: 0.62;
    transition: opacity 240ms var(--ease), color 240ms var(--ease),
                transform 240ms var(--ease);
  }
  .social-link svg {
    width: 1.2rem;
    height: 1.2rem;
    flex: none;
  }
  .social-link:hover,
  .social-link:focus-visible {
    opacity: 1;
    color: var(--accent);
    transform: translateY(-1px);
  }
  /* The meta list underlines its links; these carry an icon instead. */
  .meta-list .social-link { border-bottom: 0; padding-bottom: 0; }

  .footer-nav .social-link { opacity: 0.5; }
  .footer-nav .social-link:hover,
  .footer-nav .social-link:focus-visible { opacity: 1; }

  /* --- About ---------------------------------------------------------- */
  .about-grid { display: grid; gap: clamp(2rem, 5vw, 4.5rem); }
  @media (min-width: 60rem) {
    .about-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); align-items: start; }
    .about-grid__media { position: sticky; top: calc(var(--header-h) + 2rem); }
  }
  .about-grid__media img { width: 100%; border-radius: var(--radius); }

  .spec {
    display: grid;
    gap: 0.35rem 1.5rem;
    grid-template-columns: auto 1fr;
    font-size: var(--step--1);
  }
  .spec dt { color: var(--text-faint); }
  .spec dd { margin: 0; color: var(--text-dim); font-variant-numeric: tabular-nums; }

  /* --- Stats ---------------------------------------------------------- */
  .stats { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); }
  .stat__n { font-family: var(--font-display); font-size: var(--step-3); line-height: 1; }
  .stat__l { font-size: var(--step--1); color: var(--text-faint); letter-spacing: 0.08em; text-transform: uppercase; margin-top: 0.4rem; }

  /* --- Footer --------------------------------------------------------- */
  .site-footer {
    border-top: 1px solid var(--line-soft);
    padding-block: clamp(2.5rem, 5vw, 4rem);
    color: var(--text-faint);
    font-size: var(--step--1);
  }
  .site-footer__inner { display: flex; flex-wrap: wrap; gap: 1.5rem 2.5rem; justify-content: space-between; align-items: baseline; }
  .site-footer a { text-decoration: none; color: var(--text-dim); }
  .site-footer a:hover { color: var(--text); }
  .footer-nav { display: flex; flex-wrap: wrap; gap: 1.4rem; }

  /* --- TK marker ------------------------------------------------------ */
  /* Unanswered interview copy. Loud on purpose — impossible to ship blind. */
  .tk {
    display: inline-block;
    padding: 0.15em 0.5em;
    border-radius: 2px;
    background: #4a2f00;
    color: #ffce7a;
    border: 1px dashed #a8742a;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 0.82em;
  }
}

/* --- Utilities & motion ------------------------------------------------ */
@layer components {
  /* --- Print storefront grid ------------------------------------------
     A justified grid: every row fills the full width, and every image keeps
     its own shape. Each tile's flex-grow AND flex-basis are proportional to
     its aspect ratio, which makes the widths within a row proportional to
     the ratios — so the heights come out equal without being told to.

     This is why nothing is cropped: aspect-ratio sets the box from the real
     dimensions rather than object-fit trimming the picture to a shape.

     The ::after absorbs leftover space on the final row; without it, two
     stragglers would stretch across the whole width. */
  .pgrid {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
  }

  .pgrid::after {
    content: "";
    flex-grow: 1e6;
  }

  .ptile {
    position: relative;
    display: block;
    aspect-ratio: var(--ar);
    flex: var(--ar) 1 calc(var(--ar) * 13rem);
    overflow: hidden;
    border-radius: var(--radius, 2px);
    background: color-mix(in oklab, var(--bg) 85%, #fff);
    text-decoration: none;
  }

  .ptile img {
    width: 100%;
    height: 100%;
    object-fit: cover;          /* box already matches the ratio, so no crop */
    display: block;
    transition: transform .5s cubic-bezier(.2,.7,.3,1);
  }

  @media (hover: hover) {
    .ptile:hover img,
    .ptile:focus-visible img { transform: scale(1.04); }
  }

  /* Caption sits on a gradient rather than a solid bar, so it stays legible
     over a bright sky without putting a hard edge across the picture. */
  .ptile__meta {
    position: absolute;
    inset: auto 0 0 0;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0 .75rem;
    padding: 2.5rem .85rem .7rem;
    background: linear-gradient(to top, rgb(0 0 0 / .78), rgb(0 0 0 / 0));
    color: #fff;
    opacity: 0;
    transition: opacity .3s ease;
  }

  /* Touch screens have no hover, so the caption must always be visible. */
  @media (hover: none) {
    .ptile__meta { opacity: 1; }
  }

  .ptile:hover .ptile__meta,
  .ptile:focus-visible .ptile__meta { opacity: 1; }

  .ptile__title {
    font-family: var(--font-display, inherit);
    font-size: 1.05rem;
    line-height: 1.2;
  }

  .ptile__price {
    font-size: .82rem;
    letter-spacing: .04em;
    color: rgb(255 255 255 / .8);
    white-space: nowrap;
  }

  @media (prefers-reduced-motion: reduce) {
    .ptile img, .ptile__meta { transition: none; }
    .ptile:hover img { transform: none; }
  }

}

@layer components {
  /* A quiet aside under a heading — the Dene name notes on print pages, and
     the provenance line on the portfolio. Deliberately smaller and dimmer
     than the copy around it: context, not part of the message. */
  .note-quiet {
    margin-top: .35rem;
    font-size: .85rem;
    line-height: 1.5;
    max-width: 42ch;
    color: var(--text-dim);
    border-left: 2px solid var(--line);
    padding-left: .7rem;
  }

}

@layer utilities {
  .text-center { text-align: center; }
  .dim { color: var(--text-dim); }
  .faint { color: var(--text-faint); }
  .mt-s { margin-top: 1rem; }
  .mt-m { margin-top: 2rem; }
  .mt-l { margin-top: clamp(2.5rem, 5vw, 4rem); }
  .flow-tight { --flow: 0.6em; }
  .cluster { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }

  /* Scroll-driven reveal. Uses the native scroll timeline where supported
     and simply shows content everywhere else — no JS, no FOUC. */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .reveal {
        animation: reveal-in linear both;
        animation-timeline: view();
        animation-range: entry 5% cover 26%;
      }
      @keyframes reveal-in {
        from { opacity: 0; transform: translateY(24px); }
        to   { opacity: 1; transform: none; }
      }
    }
  }
}

/* Cross-document view transitions — a soft cut between pages in Chrome/Edge,
   ignored entirely elsewhere. */
@view-transition { navigate: auto; }

@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) { animation: vt-out 180ms var(--ease) both; }
  ::view-transition-new(root) { animation: vt-in 320ms var(--ease) both; }
  @keyframes vt-out { to { opacity: 0; } }
  @keyframes vt-in { from { opacity: 0; } }
}

@media print {
  .site-header, .site-footer, .lightbox, .hero__scroll { display: none !important; }
  body { background: #fff; color: #000; }
}
