/* Stephan Louis - "Typographic Anchor"
   Warm paper ground, one typeface, zero ornament. Pictures carry everything. */

:root {
  --page: #F3F2F2;
  --ink: #201E1D;
  --grey: #605D5D;
  --mute: #9B9797;
  --rule: rgba(32, 30, 29, 0.4);
  --rule-soft: rgba(32, 30, 29, 0.25);
  --surface: #EAE9E9;
  --body-copy: #444141;

  --font: 'Archivo', sans-serif;

  --sp-1: 8px;
  --sp-2: 16px;
  --sp-3: 24px;
  --sp-4: 40px;  /* column gap */
  --sp-5: 56px;  /* page margin, row gap */
  --sp-6: 80px;  /* section break */

  --page-pad: var(--sp-5);
  /* The approved "Typographic Anchor" homepage design uses a flat 32px side
     inset (not the rest of the site's wider --page-pad). Scoped to its own
     token so it stays isolated to the .gw-* (homepage) rules below and
     never touches the other pages' --page-pad. */
  --gw-pad: 32px;
  --content-max: 1440px;
  /* Work/About/Shop ran genuinely full-bleed (no cap) at first, which reads
     fine up to ~1600-1700px wide, but past that the fixed-aspect-ratio
     imagery (.gw-tile-frame, .gw-about-portrait, .home-print-frame) keeps
     growing with viewport width with nothing to stop it, so on a 1920/2560
     desktop each section's total height outgrows the viewport it opens
     into - see .gw-tiles/.gw-about/.gw-shop below. Capped wide enough to
     still read as large, full-bleed photography on any realistic desktop
     width, just not unbounded.
     1680 was tuned against a clean, idealized 900/1080/1440px-tall headless
     browser - real browser windows are shorter than that (tab strip,
     bookmarks bar, OS menu bar all eat into the usable height a maximized
     window actually gets), so that only left ~40px of margin at 900 and
     went negative well before any genuinely short-but-realistic window
     (confirmed overflowing by 120px+ at 1920x880). Pulled in further so the
     fit holds with real margin at heights well below the clean numbers
     it was first checked against. */
  --home-max: 1400px;
  --gallery-max: 2200px; /* shoot pages run wider than other pages so a wide
                            monitor can fit an extra image per row instead
                            of sitting in unused margin */

  /* ---- Gallery system ----
     Two tokens own every gap in every gallery on the site - the Automotive
     grid, an Automotive shoot, Lifestyle and Events alike. They exist
     because those pages were previously spaced out of the general-purpose
     --sp-* scale, which meant "the gutter between two photographs" had no
     single definition: the Automotive grid used 24px across and 40px down,
     the justified galleries read the same two tokens from JavaScript, and
     the mobile breakpoint overrode one of them for the grid but not for the
     galleries. Changing photographic spacing meant editing four places and
     getting them to agree.

     --gallery-gap is the gutter BETWEEN photographs, deliberately equal in
     both directions: unequal gaps make a justified layout read as separate
     horizontal bands rather than one continuous wall of pictures. Small
     enough that the photographs dominate and the page reads as editorial
     rather than as a widget grid, large enough that two images never touch
     or appear to bleed into one another.

     --gallery-top is the breathing room between the header rule and the
     first row, applied to whichever block a gallery page opens with, so
     Automotive, Lifestyle, Events and a shoot page all start at the same
     distance below the divider. */
  --gallery-gap: 12px;
  --gallery-top: 24px;
}

* { box-sizing: border-box; }

/* Reserve the scrollbar's width always (on browsers/platforms where it
   takes layout space at all - many don't). Without this, a short page
   measures wider than a tall one once a scrollbar appears, which matters
   here because the gallery's justified layout (js/shoot.js) measures its
   container width in pixels - a page that's short before the gallery
   renders and tall after would otherwise get measured too wide on that
   first pass, leaving every row's images a few pixels short of the true
   edge. */
html { overflow-y: scroll; }

/* The `hidden` attribute hides an element through a UA-stylesheet rule
   (`[hidden] { display: none }`), and author styles beat the UA stylesheet
   no matter how weak their specificity - so ANY author `display` on the same
   element silently cancels it. Several elements here set a display AND are
   toggled by `hidden` from JavaScript: .lightbox (flex), .soon-main (flex),
   .work-grid (grid) and the gallery containers once they're given a class.

   That is not hypothetical. #work-empty - the "coming soon" copy on
   lifestyle.html and events.html - is a .soon-main with `display: flex`, so
   marking it hidden did nothing: on a populated Lifestyle page it kept
   rendering, 572px of "a curated gallery is on its way", underneath the
   gallery that was already there. .lightbox had run into the same thing and
   carried its own one-off `.lightbox[hidden]` guard; this states the rule
   once, with the priority needed to actually win, instead of waiting for
   each new component to rediscover it. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--page);
  color: var(--ink);
  overflow-x: hidden;
}

body {
  font-family: var(--font);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* Photography deterrent. This is not real protection - a screenshot or the
   network tab still gets the picture, and nothing here pretends otherwise.
   It removes the one-click save path for casual visitors, which is the whole
   goal, and it does so with zero effect on how the photograph looks:
   no overlay, no canvas, no watermark, no resolution change.

   -webkit-touch-callout is the mobile half, and the one that was actually
   missing: on iOS, a long-press on any <img> raises the "Save Image" sheet.
   Setting it to none on the image suppresses that sheet. It does NOT touch
   pinch-to-zoom, scrolling, or tapping - those are viewport/gesture level
   and keep working normally.

   user-select stops the image being swept into a text selection and dragged
   out that way. It is scoped to images only, so captions, the bio and every
   other piece of text on the site stay selectable and copyable as before. */
img {
  display: block;
  max-width: 100%;
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* The gallery tiles are <button>s wrapping the photo, and iOS can raise the
   callout from the button rather than the image inside it, so the same
   suppression is needed one level up. Kept to the photographic tiles - nav,
   links and form controls are untouched. */
.work-tile,
.frame-tile,
.gw-tile,
.gwm-tile,
.lightbox-image-wrap {
  -webkit-touch-callout: none;
}

a {
  color: var(--ink);
  text-decoration: none;
}

button { font-family: var(--font); }

:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 3px;
}

main { flex: 1; }

/* The Automotive/Lifestyle/Events gallery grid runs wider than the other
   content pages (same --gallery-max a shoot's own page already uses, so
   the listing isn't capped tighter than what you land on from it) - the
   homepage's own Work section is the visual benchmark for scale here,
   and full-bleed edge to edge is what makes that section's photography
   read as large as it does. */
.work-grid {
  max-width: var(--gallery-max);
  margin-left: auto;
  margin-right: auto;
}

.contact-grid {
  max-width: var(--content-max);
  margin-left: auto;
  margin-right: auto;
}

/* Shoot pages get their own (wider) cap - see --gallery-max above. */
.set-header,
.set-single,
.set-gallery,
.next-set-row {
  max-width: var(--gallery-max);
  margin-left: auto;
  margin-right: auto;
}

/* ---------- Type ---------- */

.wordmark {
  font-weight: 800;
  font-size: 18px;
  letter-spacing: 0.02em;
  cursor: pointer;
  white-space: nowrap;
}

.page-title {
  margin: 0;
  font-size: 22px;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.body-lead {
  margin: 0;
  font-size: 17px;
  line-height: 1.65;
  text-wrap: pretty;
}

.body-copy {
  margin: 0;
  font-size: 15px;
  line-height: 1.7;
  color: var(--body-copy);
}

.label-caps {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--grey);
}

.nav-caps {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* ---------- Nav ---------- */

.site-nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 40px var(--page-pad) 48px;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

.nav-links {
  display: flex;
  gap: 32px;
}

.nav-link {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--grey);
  padding-bottom: 3px;
  border-bottom: 1px solid transparent;
}

.nav-link:hover { color: var(--ink); }

.nav-link[aria-current="page"] {
  color: var(--ink);
  border-bottom-color: var(--ink);
}

/* ---------- Buttons ---------- */

.btn {
  display: inline-block;
  align-self: flex-start;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 13px 26px;
  border: 1px solid var(--ink);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
}

.btn:hover {
  background: var(--ink);
  color: var(--page);
}

/* ---------- Work grid ---------- */

.work-grid {
  /* --gw-pad (32px, vs. --page-pad's 56px) - the same tighter inset the
     homepage's own Work section uses, so the gallery isn't sitting in
     more margin than its own benchmark. Top padding comes from
     --gallery-top, shared with every other gallery page. */
  padding: var(--gallery-top) var(--gw-pad) 64px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-flow: dense;
  align-items: center;
  gap: var(--gallery-gap);
}

.work-tile { min-width: 0; }

.work-tile a { display: flex; flex-direction: column; gap: 10px; }

.work-tile .frame {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--surface);
}

.work-tile.landscape {
  grid-column: 1 / -1;
}

/* A landscape that lands right after exactly one orphaned portrait fills
   the remaining 2 columns of that row instead of forcing a new one. */
.work-tile.landscape.fill-gap {
  grid-column: span 2;
}

.work-tile.landscape .frame {
  aspect-ratio: 16 / 9;
}

.landscape-group {
  grid-column: 1 / -1;
  display: grid;
  gap: var(--gallery-gap);
}

.landscape-group .work-tile.landscape {
  grid-column: auto;
}

.work-tile .frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: filter 0.25s ease;
}

.work-tile .frame .caption-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 24px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #FFFFFF;
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
}

.tile-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}

.tile-meta .caption {
  font-size: 6px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--grey);
}

@media (hover: hover) and (pointer: fine) {
  .tile-meta { display: none; }
  .work-tile a:hover .frame img { filter: brightness(0.78); }
  .work-tile a:hover .frame .caption-overlay { opacity: 1; }
}

/* The exact complement of the query above: the devices where .tile-meta is
   actually on screen. Only the Automotive grid prints a title under each
   photograph, and a title needs more clearance below it than a bare gutter
   gives - at a flat --gallery-gap the caption ends up closer to the tile
   beneath it than to the one it names. So the vertical gutter (and only the
   vertical one) opens up, and only where a caption exists to justify it.
   Lifestyle and Events have no titles and are untouched by this. */
@media (hover: none), (pointer: coarse) {
  .work-grid,
  .landscape-group { row-gap: calc(var(--gallery-gap) + 8px); }
}

/* ---------- Set page ---------- */

/* These four blocks use --gw-pad, the gallery inset (32px desktop / 12px
   mobile), not --page-pad (56px) which is for ordinary page content - nav,
   prose, forms. They used to use --page-pad on desktop and were switched to
   --gw-pad only at the mobile breakpoint, so a shoot's photographs sat in
   24px more margin than the Automotive grid they were opened from, on
   exactly the screens with the most width to give them. Now the inset
   matches everywhere and the override at the bottom of this file is gone. */

.set-header {
  padding: var(--gallery-top) var(--gw-pad) 28px;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

.back-link {
  margin-left: auto;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--grey);
}

.back-link:hover { color: var(--ink); }

.set-single {
  padding: var(--gallery-top) var(--gw-pad) 64px;
  display: flex;
  justify-content: center;
}

.set-single .frame-tile {
  min-width: 0;
  width: 100%;
  max-width: 720px;
  cursor: zoom-in;
  background: none;
  border: none;
  padding: 0;
  display: block;
}

.set-single .frame-tile .frame {
  aspect-ratio: 3 / 4;
  overflow: hidden;
}

.set-single .frame-tile.landscape {
  max-width: 960px;
}

.set-single .frame-tile.landscape .frame {
  aspect-ratio: 16 / 9;
}

/* Justified layout: js/shoot.js groups photos into rows and sets each
   tile's exact pixel width/height inline so a row's images - scaled
   together, never cropped - fill the container edge to edge. .set-gallery
   just stacks those rows; .gallery-row lays its tiles out side by side. */
.set-gallery {
  padding: var(--gallery-top) var(--gw-pad) 64px;
  display: flex;
  flex-direction: column;
  gap: var(--gallery-gap);
}

/* Every gallery page opens the same distance below the header rule -
   --gallery-top, above - whatever its first block happens to be: the
   Automotive grid of shoots, a shoot page's back link, or (on Lifestyle
   and Events, which have no header row at all) the photographs
   themselves. Lifestyle and Events used to get nothing, because on a
   shoot page that spacing came from .set-header's own padding and those
   two pages have no .set-header, so their first row sat hard against the
   divider while Automotive had 24px of air.

   The flip side: a shoot page must not take it twice. Its back-link
   header has already opened the page, so the photographs below inherit
   no further top padding. This has to sit after the rules above to win
   the cascade - all three selectors are the same specificity. */
.set-header ~ .set-gallery,
.set-header ~ .set-single {
  padding-top: 0;
}

.gallery-row {
  display: flex;
  max-width: 100%;
  overflow: hidden;
}

.set-gallery .frame-tile {
  min-width: 0;
  flex: 0 0 auto;
  cursor: zoom-in;
  background: none;
  border: none;
  padding: 0;
  display: block;
}

.set-gallery .frame-tile .frame {
  overflow: hidden;
}

.set-gallery .frame-tile .frame img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  transition: opacity 0.2s ease;
}

.set-gallery .frame-tile:hover .frame img { opacity: 0.9; }

/* An Events photograph's optional description, printed under the picture.
   Only ever emitted when the photo actually has one (js/gallery.js writes
   no element at all otherwise), so a gallery without descriptions - and
   every Lifestyle and Automotive gallery, which have no description field -
   renders exactly as it did before this existed.

   No new type style: this is the site's existing caption idiom, the same
   size/weight/tracking/colour already used by .back-link and the "Next set"
   link, so it reads as part of the same system rather than as a new
   component. */
.photo-caption {
  display: block;
  margin-top: 10px;
  text-align: left;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--grey);
}

.set-single .frame-tile .frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.next-set-row {
  padding: 0 var(--gw-pad) 64px;
  display: flex;
  justify-content: flex-end;
}

.next-set-row a {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--grey);
}

.next-set-row a:hover { color: var(--ink); }

/* ---------- About ----------
   Built from the approved "About Desktop Final" / "About Mobile Final"
   design references. Everything here is prefixed .ab- and is used by
   about.html alone.

   Do not confuse these with .gw-about-* / .gwm-about-* further down this
   file: those are the HOMEPAGE's About teaser (desktop and mobile
   respectively) and are a different component on a different page.

   The desktop reference is drawn at 1440px against a 12-column grid with
   32px gutters and a 40px page inset. The inset here is --page-pad
   instead, so the About page's rules and text columns line up with the
   site header and footer (which are not part of this redesign and keep
   their own spacing) rather than sitting 16px inside them. The grid,
   the column spans and every type size are the reference's own. */

/* The reference's design system sets `body { line-height: 1.55 }`, and every
   label, byline and link on the page that does not name its own leading
   inherits it - the numerals, the eyebrows, the signature, the two contact
   rows and the enquiry link. This site's body has no such base, so those
   elements were coming out on `normal` (about 1.2) and each contact row was
   measuring 45px against the reference's 52px. Scoped to the About page
   rather than added to body, which would re-space every other page.
   Anything that sets its own line-height - the lead, the body copy, the
   pull quote, the display type - is unaffected. */
.ab {
  line-height: 1.55;
  /* The numeral's own column. Narrow and fixed rather than one twelfth of the
     page: a grid column is 77px at 1440 and the "i" is about four of them
     wide, so the numeral sat alone at the far left with the paragraph
     starting 113px away, reading as a separate column rather than as a
     margin note against it. */
  --ab-marker-col: 22px;
}

/* Sections carry the width cap; their children carry the side inset. That
   split is what lets the mobile layout run one photograph out to the right
   edge (see .ab-figure below) without fighting a padded parent. */
.ab-bio,
.ab-clients-head,
.ab-clients-track,
.ab-clients-foot,
.ab-contact {
  max-width: var(--content-max);
  margin-left: auto;
  margin-right: auto;
}

/* --sp-4, not a viewport fraction. 9vh reads as 81px on a 900-tall window
   but 130px on a 1440-tall display, and the site header already contributes
   48px of its own below the nav links - so on a large screen the page opened
   on nearly 180px of nothing before the first rule. A fixed step off the
   spacing scale keeps the breathing room the same on every screen. */
.ab-bio { padding-top: var(--sp-4); }

.ab-bio-head {
  padding: 0 var(--page-pad) 14px;
  border-bottom: 2px solid var(--rule);
}

.ab-eyebrow {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.26em;
  text-transform: uppercase;
}

.ab-eyebrow--quiet {
  letter-spacing: 0.24em;
  color: var(--grey);
}

/* One 12-column row: a roman numeral in the margin, the prose in columns
   3-7. The empty second column is the reference's, and is what gives the
   numerals their hanging-margin look. */
/* One template for the prose rows and the photograph: the numeral's column,
   then the page's own twelve. Both the paragraphs and the picture start at
   column 2, which is what gives the section a single left edge - before this
   the numeral, the text and the photograph each began somewhere different
   (56 / 169 / 283 at 1440) and nothing lined up with anything. */
.ab-row,
.ab-figure {
  display: grid;
  grid-template-columns: var(--ab-marker-col) repeat(12, minmax(0, 1fr));
  column-gap: 32px;
}

/* baseline, so the numeral sits ON the first line of the prose beside it
   rather than above it. Both are grid items, and a grid item defaults to
   stretch: each box starts at the top of the row, and the numeral's 10px
   type then sits at the top of ITS box while the 15px/1.8 prose carries 6px
   of half-leading above its first line. That put the numerals 7.25px high -
   close enough to look like a mistake rather than a hanging margin note.
   Aligning the baselines is exact at any size, and needs no numbers here
   that would go stale if either block were ever reset. */
.ab-row {
  padding: 6vh var(--page-pad) 0;
  align-items: baseline;
}

.ab-row:first-of-type { padding-top: 34px; }

.ab-marker {
  grid-column: 1;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2em;
  color: var(--mute);
}

/* Column 2, not 3. The numbered margin keeps column 1 to itself and the
   grid's own 32px gutter separates them, so the marker still reads as a
   margin note - but the prose now begins near the left edge of the content
   grid instead of a third of the way across it.

   The column is given six of the twelve and then the line is held to 560px,
   which is about 74 characters at this size. Six columns is 648px at 1440
   and would run to 85 characters a line - wide enough that the eye starts
   losing its place returning to the left edge. The cap is a shared px value
   rather than a ch measure so the three blocks, which are not all set at the
   same size, still align down their right edge. */
.ab-lead,
.ab-body,
.ab-close {
  grid-column: 2 / span 6;
  max-width: 560px;
}

/* Set exactly as .ab-body p below, which is itself the site's body copy
   (15px/1.8/--body-copy, the same as .gw-about-copy p on the homepage). It
   has been a display size, then a lead size; both made the opening
   paragraph read as a headline sitting on top of the page rather than as
   the first thing you read. The hierarchy on this page comes from the
   numbered margin and the space between the blocks, not from setting the
   first paragraph larger than the second. */
.ab-lead {
  margin: 0;
  font-size: 15px;
  font-weight: 400;
  line-height: 1.8;
  color: var(--body-copy);
  text-wrap: pretty;
}

.ab-body {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.ab-body p {
  margin: 0;
  font-size: 15px;
  line-height: 1.8;
  color: var(--body-copy);
  text-wrap: pretty;
}

.ab-close {
  padding-top: 20px;
  border-top: 1px solid var(--rule-soft);
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.ab-pull {
  margin: 0;
  font-size: clamp(17px, 1.25vw, 21px);
  line-height: 1.5;
  letter-spacing: -0.014em;
  text-wrap: pretty;
}

.ab-sign {
  align-self: flex-start;
  padding-top: 14px;
  border-top: 2px solid var(--rule);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.ab-figure {
  margin-top: 6vh;
  padding: 0 var(--page-pad);
}

/* Hung from the LEFT, starting on column 2 - the same line the prose starts
   on, so the numeral margin, the paragraphs and the photograph all share one
   left edge and the empty columns fall on the right. The width is unchanged
   at every breakpoint; only which end of the grid it hangs from. */
.ab-figure-frame {
  grid-column: 2 / span 8;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--surface);
}

/* Sized exactly as .gw-about-portrait img on the homepage - 100%/100% and
   cover, nothing else - because it is the same photograph in the same 16/10
   frame and the two were visibly cropped differently. This one used to stand
   112% tall inside the frame so the parallax had something to travel
   through, which meant the frame clipped 6% off the top and bottom that the
   homepage kept, and the phone cut into his head.

   The drift has not gone: the picture is 1.5 against a 1.6 frame, so cover
   already discards a band of it, and js/about.js pans through that band with
   object-position instead of moving the element. Same effect, and the crop at
   rest is now the homepage's to the pixel. */
.ab-figure-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- Selected clients ----
   The row is deliberately wider than the page (width: max-content) and is
   translated left as the section is scrolled through - see the data-ab-drift
   handler in js/about.js. The track clips it; nothing here scrolls
   horizontally by hand, so a trackpad's sideways gesture and the page's own
   scrolling are both unaffected.

   This is the original treatment, restored. It was briefly replaced by a
   hand-scrolled strip and then by a static wrapping row; both fit more logos
   on screen and neither had the thing that made this section work, which is
   that the marks move because the reader is moving down the page.

   The logos themselves stay CMS-driven - height and baseline per logo from
   Sanity, no per-logo rule here - so the row still takes however many the
   document holds. */

/* --sp-6 is the spacing scale's own "section break", and a fixed step rather
   than a viewport fraction for the same reason .ab-bio's top padding is:
   22vh measured 190px on a 900-tall window but 230px on a 1440-tall display,
   which left the clients section reading as a separate page rather than as
   the end of this one.

   It has to stay larger than .ab-clients-track's padding below, or the
   heading ends up nearer the paragraph above it than the logos it belongs
   to - 80 above, 56 below, so the heading and its marks group together. */
.ab-clients {
  padding-top: var(--sp-6);
  overflow: hidden;
}

.ab-clients-head {
  padding: 0 var(--page-pad) 20px;
  border-bottom: 2px solid var(--rule);
}

.ab-clients-track {
  padding: var(--sp-5) 0;
  overflow: hidden;
}

.ab-clients-row {
  display: flex;
  align-items: center;
  gap: 13vw;
  width: max-content;
  padding: 0 var(--page-pad);
}

/* Each mark is set at its own optical size and baseline rather than to one
   shared box, because the lockups are genuinely different shapes. Both come
   from Sanity per logo (see clientLogo.js): the height as --ab-logo-h, the
   baseline as one of the two modifier classes.

   --ab-logo-h is a plain number, so it is multiplied by 1px here; that is
   what lets --ab-logo-scale step the whole row down together on smaller
   screens while every logo keeps its relationship to the others. At desktop
   the scale is 1, so the four marks stand at exactly the sizes the approved
   design gives them. */
.ab-logo {
  width: auto;
  height: calc(var(--ab-logo-h, 72) * 1px * var(--ab-logo-scale, 1));
  object-fit: contain;
  flex: none;
}

.ab-logo--top { align-self: flex-start; }
.ab-logo--bottom { align-self: flex-end; }

.ab-clients-foot { border-top: 1px solid var(--rule-soft); }

/* ---- Contact ----
   The About page's own closing panel. It repeats the site's real contact
   details; it is not a replacement for /contact or for .site-footer, both
   of which are untouched.

   Every value below is the HOMEPAGE's Contact section (.gw-contact, further
   down this file) rather than the design reference's own: the same 12-column
   split at 1-6 / 8-12, the same --sp-3 gutter, the same
   clamp(32px, 3.2vw, 70px)/500/1.04/-0.02em heading, the same 28px note at
   44ch, the same rows, and the same .gw-contact-cta treatment on the link.
   The reference drew a heavier version of this - 96px at weight 400 with
   -0.038em tracking over a 32px gutter - and shipping it meant the site had
   two different "Let's work together" blocks. This is the site's one.

   The homepage's own rules are NOT reused directly, and must not be: those
   are scoped to .gw-* inside index.html's desktop structure, which this page
   does not have. These mirror their values on the About page's own classes.
   If the homepage's treatment is retuned, retune these to match. */

.ab-contact {
  padding: 12vh var(--page-pad) 16vh;
}

.ab-contact-head { padding-bottom: 7vh; }

.ab-contact-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 0 var(--sp-3);
  align-items: start;
  /* The rest of this page inherits line-height 1.55 from .ab, which is the
     design reference's base. The homepage's Contact block has no such base,
     so its rows and its CTA sit on `normal` - and matching that block means
     matching its line boxes too, or the rows come out 52px against the
     homepage's 46px. Only this block is reset; the eyebrow above it and
     every other passage on the page keep the page's own leading, and the
     heading and note here set their own either way. */
  line-height: normal;
}

.ab-contact-lead { grid-column: 1 / span 6; }

.ab-display {
  margin: 0;
  font-size: clamp(22px, 2.2vw, 48px);
  font-weight: 500;
  line-height: 1.04;
  letter-spacing: -0.015em;
  text-transform: uppercase;
  color: var(--ink);
  text-wrap: pretty;
}

.ab-contact-note {
  margin: 28px 0 0;
  max-width: 44ch;
  font-size: 15px;
  line-height: 1.8;
  color: var(--body-copy);
  text-wrap: pretty;
}

.ab-contact-panel {
  grid-column: 8 / span 5;
  display: flex;
  flex-direction: column;
}

.ab-meta-row {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: 14px 0;
  border-top: 1px solid var(--rule-soft);
  font-size: 15px;
}

.ab-meta-row--last { border-bottom: 1px solid var(--rule-soft); }

.ab-meta-key {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--grey);
}

.ab-meta-val { font-size: 15px; }
.ab-meta-val:hover { color: var(--grey); }

/* .gw-contact-cta, value for value and built the same way - the rule sits
   on the anchor itself rather than on an inner span, which is what makes
   the two line boxes identical rather than a pixel apart. */
.ab-enquiry {
  display: inline-block;
  align-self: flex-start;
  margin-top: 26px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 2px solid var(--ink);
  padding-bottom: 6px;
}

.ab-enquiry:hover { color: var(--grey); border-color: var(--grey); }

/* ---- Entrance start-state ----
   Declared here, and switched on by the inline <head> script in about.html,
   so the first painted frame already matches what js/about.js is about to
   animate FROM. Setting it from the script at the end of <body> instead
   would paint the finished page first and then yank it backwards - the same
   flash .motion-pending exists to prevent on the homepage.

   Under prefers-reduced-motion js/about.js reveals nothing and animates
   nothing, so the start-state must not apply either: everything sits at its
   natural, final appearance as though none of this ran. */
html.ab-motion-pending .ab-bio-head,
html.ab-motion-pending .ab-row,
html.ab-motion-pending .ab-figure,
html.ab-motion-pending .ab-clients-head,
html.ab-motion-pending .ab-contact-head,
html.ab-motion-pending .ab-contact-grid { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  html.ab-motion-pending .ab-bio-head,
  html.ab-motion-pending .ab-row,
  html.ab-motion-pending .ab-figure,
  html.ab-motion-pending .ab-clients-head,
  html.ab-motion-pending .ab-contact-head,
  html.ab-motion-pending .ab-contact-grid { opacity: 1; }
}

/* ---------- Contact ---------- */

.contact-grid {
  padding: 0 var(--page-pad) var(--sp-6);
  display: grid;
  grid-template-columns: 5fr 7fr;
  gap: var(--sp-5);
  align-items: start;
}

.contact-info {
  display: flex;
  flex-direction: column;
  gap: 22px;
}

.contact-info .ig-link { font-size: 15px; }

.contact-info .note {
  font-size: 13px;
  line-height: 1.7;
  color: var(--grey);
  max-width: 300px;
}

.check-note {
  margin: 0;
  font-size: 13px;
  line-height: 1.7;
  color: var(--mute);
  max-width: 300px;
}

.contact-form {
  display: flex;
  flex-direction: column;
  gap: 22px;
  max-width: 520px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.field label {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--grey);
}

.field input,
.field textarea {
  font-family: var(--font);
  font-size: 15px;
  padding: 12px 0;
  border: none;
  border-bottom: 1px solid var(--rule);
  outline: none;
  background: transparent;
  color: var(--ink);
  width: 100%;
}

.field textarea { resize: vertical; }

.field input:focus,
.field textarea:focus {
  border-bottom: 1px solid var(--ink);
}

.field-hp {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}

.send-note {
  font-size: 12px;
  color: var(--mute);
  min-height: 1.2em;
}

/* ---------- Footer ---------- */

.site-footer {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 0 var(--page-pad) 48px;
}

.footer-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  gap: var(--sp-3);
}

.footer-row .ig-link { color: var(--grey); }
.footer-row .ig-link:hover { color: var(--ink); }
.footer-row .copyright { color: var(--mute); }

.footer-notice {
  max-width: 640px;
  font-size: 10px;
  letter-spacing: 0.01em;
  line-height: 1.6;
  color: var(--mute);
}

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

.lightbox {
  position: fixed;
  inset: 0;
  background: var(--page);
  display: flex;
  flex-direction: column;
  z-index: 40;
}

/* .lightbox[hidden] used to be guarded here; the global [hidden] rule near
   the top of this file covers it (and every other component) now. */

.lightbox-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 28px 40px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--grey);
}

.lightbox-close {
  cursor: pointer;
  background: none;
  border: none;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  padding: 0;
}

.lightbox-close:hover { color: var(--ink); }

.lightbox-body {
  flex: 1;
  display: flex;
  align-items: stretch;
  gap: 32px;
  padding: 0 40px 48px;
  min-height: 0;
}

.lightbox-nav {
  align-self: center;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--grey);
  cursor: pointer;
  user-select: none;
  background: none;
  border: none;
  font-family: var(--font);
  padding: var(--sp-2);
}

.lightbox-nav:hover { color: var(--ink); }

.lightbox-image-wrap {
  flex: 1;
  position: relative;
  min-width: 0;
}

.lightbox-image-wrap img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* ---------- Homepage: entrance start-state ----------

   js/motion.js animates [data-enter] (the name lockup) and [data-rev]
   (each section) in from opacity:0 + a small translate. It used to set
   that start-state itself, in JS - but it only runs once the end-of-<body>
   scripts have all loaded, which is far later than the stylesheet lands
   (measured live: CSS done ~394ms, homepage.js ~789ms). The browser paints
   the finished page in that ~400ms gap, so those elements appeared
   normally and were then yanked to opacity:0 / 44px down to begin
   animating - a visible drop-and-fade jump on every desktop refresh.

   Declaring the start-state here instead means the very first paint
   already matches what motion.js expects, so the entrance just plays
   forward from a still frame. The .motion-pending class is added by an
   inline <head> script in index.html (before first paint) and removed
   again if motion.js never initializes, so a script failure can't leave
   the page permanently blank.

   motion.js skips its animations entirely under prefers-reduced-motion,
   never revealing anything - so that mode must not start hidden either. */
html.motion-pending [data-enter],
html.motion-pending [data-rev] { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  html.motion-pending [data-enter],
  html.motion-pending [data-rev] { opacity: 1; }
}

/* Set by index.html's <head> script only while the page is landing on a
   #section deep link, and cleared by js/homepage.js the moment the jump has
   happened - see that script for the full reasoning. Short version: the
   browser paints at scroll 0 before it can scroll to the fragment, and the
   hero is the one element the rules above never hide, so without this a
   deep link flashes a full-bleed hero photograph for ~150ms before jumping
   to the section that was actually asked for.

   visibility rather than display: the first screen keeps its box, so the
   target section's document position is identical either way and the
   browser has nothing to re-measure. Deliberately not scoped to
   prefers-reduced-motion - this is not motion, it is a wrong-content flash,
   and it is worth removing in every mode. */
html.hash-landing .gw-first-screen,
html.hash-landing .gwm-first-screen { visibility: hidden; }

/* ---------- Homepage: "Typographic Anchor" ---------- */

/* Home reuses .site-nav/.wordmark/.nav-links/.nav-link from Nav, above -
   one nav component for the whole site, home included - but the approved
   design's header type is lighter/smaller than the rest of the site's
   wordmark, so it's overridden here, scoped to .gw-nav only. Every other
   page keeps the original .wordmark/.nav-link sizing untouched. Same for
   the side padding - the design's 32px inset (--gw-pad), not the rest of
   the site's wider --page-pad, so the header lines up with the name/section
   content below it without changing any other page's nav. */

.gw-nav,
.gw-footer {
  padding-left: var(--gw-pad);
  padding-right: var(--gw-pad);
}

/* The approved design's footer has its own top/bottom margin, and shows
   the site name + copyright only (Instagram lives in the Contact section's
   own row instead, not repeated here) - scoped to home only. */
.gw-footer { margin-top: 18vh; }
.gw-footer-name { font-weight: 600; }
.gw-footer-copyright { color: var(--mute); }

/* The approved design's header is noticeably shorter than the rest of the
   site's nav (22px/16px vs 40px/48px) - scoped to home only. */
.gw-nav {
  padding-top: 22px;
  padding-bottom: 16px;
}

.gw-nav .nav-links { gap: 30px; }

.gw-nav .wordmark {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

.gw-nav .nav-link {
  font-size: 11px;
  letter-spacing: 0.18em;
}

#top { scroll-margin-top: 60px; }

/* ---------- Hero + name (fills the initial viewport) ----------
   header + hero + name are one flex column pinned to min-height: 100vh,
   so the name lockup can never be clipped below the fold. Header and
   name keep their natural height (flex: 0 0 auto); the hero photo is the
   one flexible element and simply takes whatever's left (flex: 1 1 auto,
   min-height: 420px as a floor).

   This used to be .gw-hero { height: calc(100vh - 260px) } - a fixed
   pixel budget for "everything below the hero". That guessed constant
   was only correct near ~1440px wide: the name lockup's title uses a
   vw-based clamp() that grows with viewport width, so on wider screens
   (1920px+) the name section needs meaningfully more room than 260px
   reserves, and the extra got clipped below the fold. Sizing hero by
   flex-grow instead means it's never a guess - the browser always gives
   header/name exactly what they need and hero gets the remainder, at any
   width. Plain 100vh (not 100dvh) here is deliberate too: this only
   renders above the .gw-desktop/.gwm-page mobile breakpoint, where
   Safari's live-recalculating dvh isn't a concern the way it is for the
   mobile "first screen" below. */
.gw-first-screen {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.gw-first-screen .gw-nav,
.gw-first-screen .gw-name { flex: 0 0 auto; }

.gw-hero {
  position: relative;
  /* flex-basis: 0 (the "0" in "1 1 0"), not auto - the hero img is
     height: 100% of this section, so an "auto" basis falls back to the
     photo's own intrinsic size (its full natural height at the section's
     width) instead of the flex column's available space, which let the
     hero balloon past the viewport entirely. Basis 0 makes flex-grow the
     only thing deciding its height; min-height is still an absolute
     floor either way. */
  flex: 1 1 0;
  min-height: 420px;
  overflow: hidden;
  background: var(--ink);

}

/* The <img> lives inside a <picture>, which is display: inline by default
   and therefore has no definite height of its own. That leaves the img's
   height: 100% below with nothing solid to resolve against, so its box
   falls back to being derived from the image's own intrinsic dimensions.
   Chromium quietly resolves the percentage against .gw-hero anyway and
   looks fine; Safari is stricter, which is why the jump showed up there.

   That matters because js/homepage.js swaps this img's src from the local
   file to the Sanity CDN URL once the content fetch resolves. An
   intrinsically-sized box is recalculated when that happens - naturalWidth
   /naturalHeight reset to 0 while the replacement decodes - so the hero
   briefly resizes mid-load and then settles once the new file is measured.

   Measured in Safari (probe over a real load): the img went 0px -> 1033px
   (its full intrinsic height, blowing the hero out to 1055px) and only
   collapsed back to its correct size at ~719ms. That drop is the "jump".

   Absolutely positioning <picture> fixes it at the source. Percentage
   heights are not enough here: .gw-hero's own height comes from flex
   (flex: 1 1 0), and Safari does not treat a flex-resolved height as
   "definite" for a percentage-height child, so height: 100% still fell
   back to intrinsic sizing and left the photo 612px tall inside a 648px
   hero. inset: 0 sizes the box straight from the containing block
   (.gw-hero is already position: relative) with no percentage to resolve
   and no dependence on the image at all - so the box is exact from the
   first layout pass, identical in every browser, and the src swap can no
   longer move or resize anything. Purely a stability fix: the settled
   appearance is exactly what it already was. */
.gw-hero picture {
  position: absolute;
  inset: 0;
  display: block;
}

/* Back to filling the frame exactly - no overhang, no oversize, no zoom.
   The parallax now comes from the photograph itself: js/homepage.js asks
   Sanity for the hero at its own 16:9 source ratio rather than at this
   frame's ratio, so the file that arrives is proportionally taller than the
   frame and object-fit: cover has real spare image left over above and
   below (153.8px at 1440px wide, 295.8px at 1920px). js/motion.js pans
   through that spare image with object-position, which moves the picture
   inside this box without moving or resizing the box itself - so cover
   still fills it at every scroll position and the frame's background can
   never be exposed. Unset, object-position defaults to 50% 50%, the
   natural centred crop: that is what a cold load and a reduced-motion
   visit both get. */
.gw-hero img { width: 100%; height: 100%; object-fit: cover; }

.gw-hero::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 110px;
  background: linear-gradient(to bottom, rgba(32,30,29,0) 0%, rgba(32,30,29,0.42) 100%);
  pointer-events: none;
}

.gw-hero-caption {
  position: absolute;
  left: var(--gw-pad);
  bottom: 20px;
  z-index: 1;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #FFFFFF;
  white-space: nowrap;
}

.gw-name {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 0 var(--sp-3);
  align-items: end;
  padding: 24px var(--gw-pad) 18px;
  border-bottom: 2px solid var(--rule);
}

.gw-name-title {
  grid-column: 1 / span 7;
  margin: 0;
  font-weight: 800;
  font-size: clamp(44px, 6.6vw, 140px);
  line-height: 0.82;
  letter-spacing: -0.03em;
  text-transform: uppercase;
}

.gw-name-sub {
  grid-column: 9 / span 4;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-bottom: 8px;
}

.gw-name-role {
  font-size: clamp(12px, 1.05vw, 17px);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  line-height: 1.3;
}

.gw-name-desc {
  margin: 0;
  font-size: 14px;
  line-height: 1.7;
  color: var(--body-copy);
  text-wrap: pretty;
}

/* Was 22vh/24vh originally, then 9vh/10vh - still not enough once checked
   against a realistic (shorter than a clean idealized headless window)
   browser height, so trimmed further. See --home-max above for the same
   reasoning applied to image size instead of whitespace. */
.gw-section { padding: 4vh var(--gw-pad) 0; }
.gw-section + .gw-section { padding-top: 5vh; }

.gw-section-head {
  border-top: 2px solid var(--rule);
  padding-top: 18px;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

/* Matches --home-max on .gw-about/.gw-shop below each of these -
   Work/About/Shop are the three sections that principle above applies to
   (Contact's own content, .gw-contact, is intentionally still full-bleed
   and untouched, so its section-head is left uncapped too).

   #work is the exception and uses --work-max instead: its own measure is
   derived from the viewport height (see below), and the head has to be capped
   by exactly the same value as the grid or the rule stops lining up with the
   photographs - which is the thing the two-column rebuild was for. */
#about .gw-section-head,
#shop .gw-section-head {
  max-width: var(--home-max);
  margin-left: auto;
  margin-right: auto;
}

/* Landing on #work from the nav must show the whole section: both plates AND
   the titles and descriptions under them. The nav link is a plain anchor jump
   to the top of #work, so what is visible is simply the section's own height
   against the window's - and two 4:5 plates at the full 1400px measure stand
   ~1070px tall, which fits a 1080p monitor and nothing shorter. Measured, the
   captions were cut off by 98px at 2000x969, 139px at 1440x900 and 193px at
   1512x860.

   So the measure is capped by the height available to it rather than being a
   flat 1400px. Everything except the plates is near-constant - 4vh section
   padding + 2vh grid padding (the 6vh below) + a 34px head + a ~125px caption
   - so the height left for a plate is (94vh - 167px), including 8px of
   breathing room under the caption. A plate is 4/5, so its width is 0.8x that,
   and the grid holds two of them plus one --sp-4 gap: x1.6, + the gap.

   Capping the WIDTH and letting aspect-ratio derive the height (rather than
   capping the plate's height directly) is what keeps the crop identical at
   every size: the frame stays exactly 4:5, so the photographs are never
   re-cropped by the window, they are only ever drawn larger or smaller. A
   height cap would have squared the frames off on a short window and cut
   through the model's head.

   The 840px floor stops a pathologically short window from shrinking the
   plates to nothing; below about 700px tall the section simply scrolls again,
   which is the better failure. --home-max is still the ceiling, so nothing
   here can make Work wider than About or Prints - and at 1080p and taller it
   resolves to the full 1400px, so the common desktop case is unchanged.

   167px is measured, not guessed: 34px of section head plus a ~125px caption
   plus 8px of breathing room under it. The stacked-caption range below 1100px
   needs a bigger figure - see the override in that breakpoint. */
#work {
  --work-max: clamp(840px, calc((94vh - 167px) * 1.6 + var(--sp-4)), var(--home-max));
}

#work .gw-section-head {
  max-width: var(--work-max);
  margin-left: auto;
  margin-right: auto;
}

.gw-section-label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--grey);
}

.gw-section-note {
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mute);
}

.gw-section-note--soon {
  font-weight: 600;
  letter-spacing: normal;
}

/* ---------- Homepage: Work tiles ---------- */

.gw-tiles {
  /* Two columns at the full --home-max measure - the same width as #work's
     own .gw-section-head directly above, and as .gw-about / .gw-shop below.
     So "01 - Work", "Two ways in" and the rule they sit on line up with the
     outer edges of the two photographs, and Work occupies the same column of
     the page as every other section.

     This was previously capped at
       calc((var(--home-max) - 2 * var(--sp-3)) / 3 * 2 + var(--sp-3))
     which is exactly two columns' worth of the old THREE-column grid: when
     Events was pulled from the homepage the two survivors were deliberately
     held at the size they had been as thirds, so the section's height would
     not change. It kept the height, but it left the cards 925px wide inside
     a 1400px section head - 218px of dead margin down each side, with the
     rule and both labels overhanging the pictures they label. The cap was
     solving for height and paying for it in the alignment, which is the
     thing this replaces.

     The height it was buying is real, and gets paid here instead: two 680px
     plates stand 850px tall, so #work is ~1050px rather than ~815px and no
     longer fits inside a single 900px viewport the way three small tiles
     did. That is the deliberate trade. These two cards are the primary way
     into the portfolio and are now sized like it - the section head and the
     top of both photographs still arrive together on any realistic desktop,
     and only the captions sit below the fold.

     --home-max still does its original job of bounding growth on a 1920/2560
     monitor (see its own note above); it is only the extra per-column cap
     that has gone. */
  /* --work-max, not --home-max: the same height-derived measure the section
     head above is capped by, so the two can never drift apart. It resolves to
     --home-max whenever the window is tall enough to afford it. */
  max-width: var(--work-max, var(--home-max));
  margin-left: auto;
  margin-right: auto;
  padding-top: 2vh;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* --sp-4 is the design system's own column gap, and at this width it is
     the difference between two plates with a deliberate seam between them
     and a generic 50/50 split down the middle of the page: wide enough to
     separate them, tight enough that they still read as one spread. */
  gap: var(--sp-4);
  align-items: start;
}

.gw-tile { display: block; color: var(--ink); }

.gw-tile-frame {
  /* 4:5, not the 3:4 these used at a third of this width.

     Both covers are portrait in Sanity and composed vertically - the
     Automotive frame (2400x3200) puts the pass above the car and the road
     below it, the Lifestyle frame (2133x3200) is a standing three-quarter
     portrait - so a landscape plate is not on the table here: it would crop
     straight through both subjects, and the only way to get one honestly
     would be re-cropping the covers in Sanity.

     4:5 is the shallowest portrait that still holds both compositions
     (object-fit: cover trims ~3% off the top and bottom of the Automotive
     cover and ~8% off the Lifestyle one), and at 680px wide it stands 57px
     shorter than 3:4 would. Mobile keeps 3:4 - see .gwm-tile-frame, which is
     a separate DOM and deliberately not affected by any of this. (The print
     surfaces are 3:4 and 6:5 instead, for a different reason entirely: those
     frames hold an uncropped mockup rather than a cropped cover. See the
     uncropped image system near the bottom of this file.) */
  aspect-ratio: 4 / 5;
  /* The positioning context for .gw-tile-label, which lies across it. */
  position: relative;
  overflow: hidden;
  background: var(--surface);
}

.gw-tile-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 700ms cubic-bezier(.22,.61,.36,1), filter 450ms ease;
}

/* "View Gallery", white, centred across the photograph and faded in on
   hover - the treatment these tiles have always had. It briefly became a
   permanently-visible line under the description; that added a third block
   of type beneath every plate and left the caption hanging below its own
   title, so it is back on the picture.

   pointer-events: none is what keeps it honest. The whole card is - and
   always was - a single <a>, so this span is an affordance, not a target,
   and it must never intercept the click it advertises.

   The z-index is not decoration. js/homepage.js APPENDS the cover <img> to
   this frame (setTileCover), so the photo always lands after this span in
   the DOM; on hover the photo's own filter/transform give it a stacking
   context, and paint-order ties are broken by DOM order - so without an
   explicit z-index the photo paints over the label and hides it completely,
   despite opacity: 1 being applied correctly. */
.gw-tile-label {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 320ms ease;
  color: #FFFFFF;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  pointer-events: none;
  text-align: center;
  padding: 20px;
}

@media (hover: hover) and (pointer: fine) {
  .gw-tile:hover .gw-tile-frame img { filter: brightness(0.86); transform: scale(1.02); }
  .gw-tile:hover .gw-tile-label { opacity: 1; }
}

/* The caption under a plate. Title on the left, description and the gallery
   link on the right, sharing one rule - the same left/right pairing
   .gw-section-head above and the mobile .gwm-tile-meta already use. Stacked
   (title, then copy beneath it) this left most of the right-hand half of a
   680px card empty; that stacking was sized for a 451px third-of-a-row tile.

   .gw-tile-copy is a fixed 58% of the card rather than shrink-to-fit so that
   both cards' copy columns are identical - the two descriptions are
   different lengths, and with content-sized columns their left edges would
   not line up across the spread.

   flex-start, not baseline. Baseline is the usual answer and it WAS being
   applied correctly - the title's baseline and the description's first
   baseline were landing within a third of a pixel of each other. But a big
   title and 14px copy sharing one baseline do not read as level: the
   title's caps rise ~10px above where the small type starts, so the caption
   looked dropped, and its second line then hung below the title with
   nothing beside it.

   Aligning the box tops instead brings the first line of the description up
   level with the top of the title. It is not mathematically exact - the
   title's leading scales with its clamp while the caption's is fixed, so
   the gap between the two ink tops measures 0.9px at the 42px cap, 2.6px at
   a 1600px window and 3.9px at 1100px, just before the meta stacks. Every
   one of those is inside the ~10px stagger baseline alignment was
   producing, and closing the last few would mean restating the title's
   clamp inside a calc() on this element, tuned to Archivo's own ascent -
   precision bought with a rule that silently lies the moment the typeface
   or the clamp changes. */
.gw-tile-meta {
  padding-top: 16px;
  margin-top: 16px;
  border-top: 2px solid var(--rule);
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
}

.gw-tile-copy { flex: 0 0 58%; }

.gw-tile-meta h3 {
  margin: 0;
  flex: 1 1 auto;
  font-size: clamp(20px, 1.9vw, 42px);
  font-weight: 500;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  line-height: 1;
}

.gw-tile-meta p {
  /* No top margin (it leads .gw-tile-copy, rather than following the title)
     and no max-width (the 58% column is the measure).

     There was a min-height of 3.5em here - two lines of this element's own
     line-height - which existed for one reason: to drop the "View Gallery"
     link under Lifestyle's one-line description to the same height as the
     one under Automotive's two-line description. With that link gone from
     under the caption, the min-height only reserves an empty second line
     under the shorter of the two, so it goes too. */
  margin: 0;
  font-size: 14px;
  line-height: 1.75;
  color: var(--body-copy);
  text-wrap: pretty;
}

/* ---------- Homepage: About teaser ---------- */

.gw-about {
  /* See --home-max/.gw-tiles above - same reasoning applies to the
     portrait's fixed 16:10 aspect ratio. */
  max-width: var(--home-max);
  margin-left: auto;
  margin-right: auto;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 0 var(--sp-3);
  align-items: start;
  padding-top: 2vh;
}

.gw-about-portrait {
  grid-row: 1;
  grid-column: 1 / span 6;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--surface);
}

.gw-about-portrait img { width: 100%; height: 100%; object-fit: cover; }

.gw-about-byline {
  grid-row: 2;
  grid-column: 1 / span 6;
  padding-top: 12px;
  margin-top: 12px;
  border-top: 1px solid var(--rule-soft);
  display: flex;
  justify-content: space-between;
  gap: var(--sp-3);
  align-items: baseline;
  font-size: 10px;
}

.gw-about-byline strong { font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; }
.gw-about-byline span { letter-spacing: 0.06em; color: var(--grey); }

.gw-about-copy { grid-row: 1; grid-column: 8 / span 5; padding-top: 1vh; }

/* Sentence case on purpose - this is the one section heading that is a
   sentence rather than a title, and it stays that way. What it borrows from
   .gw-shop-copy h3 ("The print collection") is the SIZE and the INK: it was
   clamp(24px, 2.4vw, 52px), a step larger than that heading at every width,
   which made it the loudest thing in the upper half of the page.

   The colour is explicit and load-bearing, for the same reason it is on the
   contact heading: .gw-about-copy p below sets --body-copy on every paragraph
   in this block, and this heading IS one of those paragraphs - so without a
   colour of its own it inherited the softer body grey while the shop heading
   sat in full ink. That grey is why it read as a different font. */
.gw-about-copy p:first-child {
  margin: 0;
  font-size: clamp(22px, 2.2vw, 48px);
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: -0.015em;
  color: var(--ink);
  text-wrap: pretty;
}

.gw-about-copy p {
  margin: 28px 0 0;
  font-size: 15px;
  line-height: 1.8;
  color: var(--body-copy);
  text-wrap: pretty;
}

.gw-about-copy p:last-of-type { margin-top: 20px; }

/* ---------- Homepage: Shop teaser ---------- */

/* The two-part shape .gw-about uses directly above it: the pictures take the
   left half of twelve columns, the words take columns 8-12. Holding the whole
   thing to --home-max keeps this section's outer edges level with the section
   head, .gw-tiles and .gw-about, so one column runs down the page.

   The prints themselves are .home-prints (near the bottom of this file, with
   the rest of the print surfaces); it spans columns 1-6 and splits in two.
   There is deliberately no frame rule here - a print is fitted, never cropped,
   and every rule that decides that lives with the uncropped image system. */
.gw-shop {
  max-width: var(--home-max);
  margin-left: auto;
  margin-right: auto;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 0 var(--sp-3);
  align-items: start;
  padding-top: 2vh;
}

/* Columns 9-12, so one empty column separates the words from the pictures -
   the same gutter .gw-about and .gw-contact leave between their two halves.

   TOP-aligned, like both of those. It hung from the bottom of the row for a
   while, which put the CTA on the foot of the photographs but opened ~290px
   of dead space between the section rule and the heading - a hole directly
   under the label, which is the first thing the eye lands on. Three short
   lines cannot fill the height of two prints whichever end they sit at, so
   the choice is only where the leftover space falls: at the top it reads as a
   hole, at the bottom it runs into the section break and reads as margin. */
.gw-shop-copy { grid-column: 9 / span 4; }

.gw-shop-copy h3 {
  margin: 0;
  font-size: clamp(22px, 2.2vw, 48px);
  font-weight: 500;
  letter-spacing: -0.015em;
  text-transform: uppercase;
  line-height: 1;
}

.gw-shop-copy p {
  margin: 26px 0 0;
  font-size: 15px;
  line-height: 1.8;
  color: var(--body-copy);
  max-width: 44ch;
  text-wrap: pretty;
}

/* ---------- Homepage: Contact ---------- */

.gw-contact {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 0 var(--sp-3);
  align-items: start;
  padding-top: 7vh;
}

.gw-contact-lead { grid-column: 1 / span 6; }

/* Set to match .gw-shop-copy h3 ("The print collection") - the same
   clamp(22px, 2.2vw, 48px), weight, tracking and ink. It was
   clamp(32px, 3.2vw, 70px), half again as large as that heading at every
   width, which made the closing block read as the loudest thing on the page
   rather than as its last word.

   The colour is explicit and load-bearing. .gw-contact-lead p below sets
   --body-copy on every paragraph in this block, and this heading IS one of
   those paragraphs - so without a colour of its own it inherited the softer
   body grey while the shop heading beside it sat in full ink. That is the
   grey Stephan spotted; it was never a deliberate treatment. */
.gw-contact-lead p:first-child {
  margin: 0;
  font-size: clamp(22px, 2.2vw, 48px);
  font-weight: 500;
  line-height: 1.04;
  letter-spacing: -0.015em;
  text-transform: uppercase;
  color: var(--ink);
  text-wrap: pretty;
}

.gw-contact-lead p {
  margin: 28px 0 0;
  font-size: 15px;
  line-height: 1.8;
  color: var(--body-copy);
  max-width: 44ch;
  text-wrap: pretty;
}

.gw-contact-panel { grid-column: 8 / span 5; display: flex; flex-direction: column; }

.gw-contact-row {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: 14px 0;
  border-top: 1px solid var(--rule-soft);
  font-size: 15px;
}

.gw-contact-panel .gw-contact-row:last-of-type { border-bottom: 1px solid var(--rule-soft); }

.gw-contact-row span:first-child {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--grey);
}

.gw-contact-cta {
  display: inline-block;
  margin-top: 26px;
  align-self: flex-start;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 2px solid var(--ink);
  padding-bottom: 6px;
}

.gw-contact-cta:hover { color: var(--grey); border-color: var(--grey); }

/* The approved design gives each of these three CTAs its own margin-top
   (30 / 28 / 26px) rather than one shared value - 26px above is already
   correct for "Make an enquiry", so only About and Shop need overriding. */
.gw-about-copy .gw-contact-cta { margin-top: 30px; }
.gw-shop-copy .gw-contact-cta { margin-top: 28px; }

/* ---------- Coming-soon pages (Lifestyle / Events) ---------- */

.soon-main {
  padding: 22vh var(--page-pad) 24vh;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
  max-width: 640px;
}

.soon-main h1 {
  margin: 0;
  font-size: clamp(32px, 4vw, 72px);
  font-weight: 500;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  line-height: 1;
}

.soon-main p {
  margin: 0;
  font-size: 15px;
  line-height: 1.8;
  color: var(--body-copy);
}

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

/* The two-part caption (title | copy) needs a card wide enough to hold a
   title and a 58% copy column side by side. The grid is still two columns
   down to 900px, and by ~1100px viewport each card is under 500px, at which
   point "Automotive" and its description start competing for the same line.
   Below here the caption stacks - the arrangement it had before - rather
   than wrapping the title. Above 1100px the side-by-side caption always
   fits (verified at 1024 through 1920). */
@media (max-width: 1100px) {
  /* ---- About: narrow desktop ----
     The reference is drawn at 1440px, where the prose sits in columns 3-7
     of 12 and measures about 550px - a comfortable 60-odd characters at
     the lead's size. Those same five columns are only 393px by 1101px and
     361px by 1024px, and the reference has no drawing below 1440 to
     follow. So the grid, the numeral margin and every type size stay
     exactly as drawn, and only the span widens - the prose and the
     photograph reclaim the empty right-hand columns as the window
     narrows, in two steps rather than one jump (span 9 below 900px, in
     that block). The reference's own relationship is kept as they go: the
     photograph always runs two columns past the prose. */
  .ab-lead,
  .ab-body,
  .ab-close { grid-column: 2 / span 8; }

  .ab-figure-frame { grid-column: 2 / span 9; }

  .gw-tile-meta { display: block; }
  .gw-tile-copy { margin-top: 12px; }

  /* Stacking the caption makes it ~30px taller (the title no longer shares a
     line with the description), so the height left for a plate drops by the
     same amount. Without this the section overflowed again at the narrow end
     - measured 810px inside an 800px window at 1100x800. */
  #work {
    --work-max: clamp(840px, calc((94vh - 200px) * 1.6 + var(--sp-4)), var(--home-max));
  }
}

@media (max-width: 900px) {
  /* --gw-pad mirrors --page-pad's own breakpoint value here (not a mobile
     design decision - just keeping this desktop-only padding fix from
     altering how the homepage already rendered below desktop width). */
  :root { --page-pad: 28px; --gw-pad: 28px; }

  .site-nav { padding-top: 28px; padding-bottom: 32px; }

  .contact-grid {
    grid-template-columns: 1fr;
    gap: var(--sp-4);
  }

  .gw-tiles { grid-template-columns: 1fr; gap: var(--sp-4); }

  .gw-about,
  .gw-shop,
  .gw-contact {
    grid-template-columns: 1fr;
    gap: var(--sp-4);
  }

  .gw-about-portrait,
  .gw-about-byline,
  .gw-about-copy,
  .home-prints,
  .gw-shop-copy,
  .gw-contact-lead,
  .gw-contact-panel { grid-column: 1 / -1; }

  /* Desktop's explicit grid-row (see .gw-about-portrait/-byline/-copy above)
     would otherwise stack portrait and copy into the same cell once they're
     both full-width here - reset to auto so this breakpoint's stacking
     order is unaffected by that desktop-only fix. */
  .gw-about-portrait,
  .gw-about-byline,
  .gw-about-copy { grid-row: auto; }

  /* ---- About: tablet ----
     Second and last step of the widening begun at 1100px - the prose now
     runs to the end of the grid and the photograph is flush with it. The
     vertical rhythm is in vh throughout, so it tightens on its own as the
     window gets shorter; the two largest gaps are pulled in further here
     because a tablet's screen is shorter than the desktop the reference
     was drawn against, and the logos come down with it. */
  .ab-lead,
  .ab-body,
  .ab-close { grid-column: 2 / span 10; }

  .ab-figure-frame { grid-column: 2 / span 10; }

  .ab { --ab-marker-col: 20px; }

  .ab-clients { padding-top: 64px; }
  .ab-clients-track { padding: 48px 0; --ab-logo-scale: 0.78; }
  .ab-clients-row { gap: 11vw; }

  .ab-contact { padding-top: 9vh; padding-bottom: 12vh; }
  .ab-contact-head { padding-bottom: 5vh; }

  .lightbox-body { padding: 0 20px 32px; gap: 16px; }
  .lightbox-header { padding: 20px; }
}

@media (max-width: 600px) {
  :root {
    --page-pad: 20px;
    /* Tighter than --page-pad at this breakpoint (12px, not 20px) - this
       is the "near edge-to-edge, only the deliberate small inset" mobile
       gallery treatment: the Work grid (.work-grid) and the Homepage
       Work section's tiles (#m-work) both key off this same value, so
       photography gets consistently tighter framing than ordinary page
       content (nav, forms, prose) without a separate token to keep in
       sync by hand. */
    --gw-pad: 12px;
    /* Photographs sit closer together on a phone than on a desktop - the
       same gutter that reads as tight at 1440px reads as loose when the
       whole page is 390px across. --gallery-top is deliberately NOT
       reduced: the header rule is a sticky bar here, so the air below it
       matters more, not less. */
    --gallery-gap: 8px;
  }

  .site-nav { flex-wrap: wrap; row-gap: 10px; gap: 12px; }

  /* Home's nav has 5 links (the others have 3) - "Home" is dropped here
     since the wordmark already links to #top, so the row has a fighting
     chance of fitting on one line; .site-nav's wrap above is the fallback
     for narrower phones where even 4 still don't fit. */
  .nav-link[href="#top"] { display: none; }

  .wordmark { font-size: 14px; }
  .page-title { font-size: 20px; }

  .nav-links { gap: 12px; }
  .nav-link { font-size: 10px; letter-spacing: 0.10em; }

  .gw-name { grid-template-columns: 1fr; gap: 16px; }
  .gw-name-title,
  .gw-name-sub { grid-column: 1 / -1; }

  .work-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .work-tile.landscape { grid-column: 1 / -1; }

  .footer-row { flex-direction: column; align-items: flex-start; gap: 8px; }

  /* Prev/Next move out from the sides (where they squeeze the image) to a
     row below it, so the image gets full width. Grid areas let this happen
     without touching the DOM order that desktop's flex layout still relies
     on. */
  .lightbox-body {
    display: grid;
    grid-template-areas:
      "image image"
      "prev  next";
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr auto;
    gap: 12px;
    padding: 0 16px 20px;
  }

  .lightbox-image-wrap {
    grid-area: image;
    min-height: 0;
  }

  .lightbox-prev { grid-area: prev; justify-self: start; }
  .lightbox-next { grid-area: next; justify-self: end; }

  /* ---- About: mobile ----
     The "About Mobile Final" reference. This is the breakpoint where the
     site swaps to its mobile header (.gwm-page, capped at 520px and
     centred), so the About content is capped and centred to match - the
     reference draws the whole page in one 520px column and the page would
     otherwise read as a wide body under a narrow header. */
  .ab {
    width: 100%;
    max-width: 520px;
    margin: 0 auto;
  }

  .ab-bio { padding-top: 30px; }
  .ab-bio-head { padding-bottom: 12px; }

  /* The hanging numerals become a fixed 18px margin column instead of a
     twelfth of the page - at 390px a twelfth is 26px, which leaves the
     numeral and the prose almost touching. */
  .ab-row {
    grid-template-columns: 18px 1fr;
    gap: 0 14px;
    /* Not start - it would put the numerals back above the first line of
       prose at exactly the width where the margin column is tightest. See
       .ab-row's own rule. */
    align-items: baseline;
    padding-top: 30px;
  }

  .ab-row:first-of-type { padding-top: 28px; }

  .ab-lead,
  .ab-body,
  .ab-close { grid-column: 2; }

  .ab-body { gap: 18px; }
  .ab-close { padding-top: 18px; gap: 22px; }
  .ab-pull { font-size: 18px; }

  /* Runs out to the left edge of the page, past the side inset the prose
     keeps - the reference's one deliberate bleed, and the reason the
     section pads its children rather than itself. Mirrored to the left so
     the phone bleeds off the same side the grid breakpoints now hang the
     photograph from; the 82% width is the reference's and is unchanged. */
  .ab-figure {
    display: block;
    width: 82%;
    margin: 30px auto 0 0;
    padding: 0;
  }

  .ab-clients { padding-top: 56px; }
  .ab-clients-head { padding-bottom: 14px; }
  .ab-clients-track { padding: 40px 0; --ab-logo-scale: 0.55; }
  .ab-clients-row { gap: 9vw; }

  /* The mobile homepage's own Contact block (.gwm-contact-*, further down)
     value for value: the same clamp(38px, 11vw, 54px)/500/1.02/-0.025em
     heading, the same 22px note, the same 30px above the rows, and the same
     stacked row - label over value at 16px rather than the desktop's
     space-between line, which cramps an email address on a 350px column. */
  .ab-contact { padding: 56px var(--page-pad) 72px; }
  .ab-contact-head { padding-bottom: 34px; }

  .ab-contact-grid { grid-template-columns: 1fr; }

  .ab-contact-lead,
  .ab-contact-panel { grid-column: 1; }

  .ab-display {
    font-size: 30px;
    line-height: 1.04;
    letter-spacing: -0.015em;
  }

  .ab-contact-note {
    margin-top: 22px;
    max-width: none;
  }

  .ab-contact-panel { margin-top: 30px; }

  .ab-meta-row {
    flex-direction: column;
    gap: 6px;
    font-size: 16px;
  }

  .ab-meta-val { font-size: 16px; }

  /* .gwm-cta exactly - a 21px line of type with a 2px rule under it, no
     min-height. An earlier version here gave it a 44px touch target, which
     is the better accessibility answer but made this the one enquiry link
     on the site that behaved differently from the other three. If that
     target is wanted it belongs on .gwm-cta, so the homepage and this page
     change together. */
  /* .gwm-cta opens the rule up to 8px on mobile, where .gw-contact-cta
     keeps 6px on desktop. */
  .ab-enquiry { margin-top: 26px; padding-bottom: 8px; }
}

/* ══════════════════════════════════════════════════════════════════════
   Homepage — Mobile ("Homepage Mobile.dc.html", approved)

   index.html carries two complete, parallel structures - .gw-desktop
   (the approved desktop build, untouched by anything below) and
   .gwm-page (this one). Only one is ever visible at a time; the switch
   itself is the single media query at the very end of this section.
   Every rule above this point, and the .gw-* rules earlier in this file,
   are exactly as they were - nothing in the existing desktop cascade is
   read, overridden, or relied upon here.
   ══════════════════════════════════════════════════════════════════════ */

/* display:contents makes this wrapper invisible to layout - header/main/
   footer stay direct flex items of body (main's flex:1 still works
   exactly as before), and no selector targeting them changes behaviour.
   Only the mobile media query below ever actually hides this. */
.gw-desktop { display: contents; }

.gwm-page { display: none; }

.gwm-page a { color: var(--ink); text-decoration: none; }

/* ---------- Header + menu button ---------- */

.gwm-header {
  flex: 0 0 auto;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px 14px;
  border-bottom: 2px solid var(--rule);
  position: sticky;
  top: 0;
  background: var(--page);
  z-index: 40;
}

.gwm-wordmark {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

.gwm-menu-btn {
  appearance: none;
  background: none;
  border: 0;
  padding: 10px 0 10px 16px;
  margin: -10px 0;
  font-family: inherit;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
  gap: 10px;
}

.gwm-menu-icon {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 18px;
}

.gwm-menu-icon span {
  display: block;
  height: 2px;
  background: var(--ink);
}

/* ---------- Full-screen nav overlay ---------- */

.gwm-nav {
  display: none;
  position: fixed;
  top: 0;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: 520px;
  z-index: 39;
  background: var(--page);
  padding: 74px 20px 20px;
  flex-direction: column;
}

.gwm-nav.is-open { display: flex; }

.gwm-nav-link {
  padding: 18px 0;
  border-top: 1px solid var(--rule-soft);
  font-size: 26px;
  font-weight: 500;
  letter-spacing: -0.01em;
  text-transform: uppercase;
}

.gwm-nav-link:first-child { border-top: 2px solid var(--rule); }
.gwm-nav-link:last-of-type { border-bottom: 2px solid var(--rule); }

.gwm-nav-footer {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.gwm-nav-footer a { font-size: 13px; color: var(--body-copy); }

.gwm-nav-footer span {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--mute);
}

/* ---------- Hero + name (one first-viewport screen) ----------
   The whole composition - header, hero photo, name lockup - is pinned to
   one viewport-height as a single flex column, so it fills the initial
   Safari viewport with nothing cut off below the fold. Header and name
   keep their natural height (flex: 0 0 auto); the hero photo is the one
   flexible element and simply takes whatever's left (flex: 1, min-height:
   0), with aspect-ratio deriving its width from that height - this is
   deliberately not a fixed-height/vh formula, so it stays correct
   regardless of real device chrome or font-metric differences instead of
   relying on a guessed pixel offset.

   Uses --vh100 (set once in index.html's <head>, see that inline script)
   rather than 100dvh directly: dvh recalculates live as Safari's address
   bar shows/hides during scroll, which was making this whole block (and
   therefore the hero, sized off it via aspect-ratio) visibly resize and
   re-center mid-scroll. --vh100 is a plain, scroll-immune pixel value, so
   this stays exactly the same height regardless of scroll direction - the
   100dvh fallback only ever matters for the one frame before that inline
   script runs. */

.gwm-first-screen {
  height: var(--vh100, 100dvh);
  display: flex;
  flex-direction: column;
  border-bottom: 2px solid var(--rule);
}

.gwm-hero {
  flex: 1 1 0;
  min-height: 0;
  align-self: center;
  max-width: calc(100% - 40px);
  position: relative;
  margin: 16px 20px 0;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: var(--ink);

}

/* Same construction as .gw-hero img above: out of flow, every edge sized
   from .gwm-hero's own box (it is already position: relative), overhang
   above the top edge, resting at transform: none. Staying out of flow also
   means the percentage height resolves against this frame's used height
   rather than against a flex-resolved one, which is the sizing Safari is
   strict about. The gradient and caption keep their z-index above it. */
/* Identical treatment to .gw-hero img above, and identical for a reason -
   both heroes now run the same rule: full source delivered, this fixed
   frame crops it, whatever cover leaves spare becomes the parallax travel.
   On mobile that currently comes to nothing, and the arithmetic is worth
   recording. The photograph is 3:4 (0.7500); this frame renders at 0.6477
   on a 390px screen and 0.6302 on a 430px one - narrower than the photo.
   So cover fills the frame's HEIGHT exactly and the 55-74px it has left
   over spills sideways instead, where a vertical pan cannot use it.
   js/motion.js measures that and leaves this hero alone: no movement, but
   equally no zoom and no crop change - the natural centred crop, exactly as
   before the parallax existed. A mobile hero cut taller than the frame
   (0.60 or so) would give it real vertical travel with no code change. */
.gwm-hero img { width: 100%; height: 100%; object-fit: cover; }

.gwm-hero-gradient {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 84px;
  background: linear-gradient(to bottom, rgba(32,30,29,0) 0%, rgba(32,30,29,0.42) 100%);
  z-index: 2;
  pointer-events: none;
}

.gwm-hero-caption {
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 12px;
  z-index: 3;
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(243, 242, 242, 0.85);
}

.gwm-name { flex: 0 0 auto; padding: 22px 20px 20px; }

.gwm-name-title {
  margin: 0;
  font-weight: 800;
  font-size: clamp(44px, 15vw, 74px);
  line-height: 0.8;
  letter-spacing: -0.035em;
  text-transform: uppercase;
}

.gwm-name-sub {
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid var(--rule-soft);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.gwm-name-role {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  line-height: 1.4;
}

.gwm-name-desc {
  margin: 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--body-copy);
}

/* ---------- Section head (shared by Work/About/Shop/Contact) ---------- */

.gwm-section { padding: 56px 20px 0; }
#m-about.gwm-section,
#m-shop.gwm-section,
#m-contact.gwm-section { padding-top: 64px; }

/* Work deliberately has NO side-padding override of its own: it uses
   .gwm-section's standard 20px, which is the same content line the hero
   (.gwm-hero, margin 0 20px), the name lockup (.gwm-name) and every other
   mobile section sit on. It briefly used the tighter 12px --gw-pad here to
   match the Automotive/Lifestyle/Events gallery pages, but that pushed the
   category photos 8px wider per side than the hero directly above them, so
   they broke the shared vertical edge running down the page. Inline with
   the hero beats edge-to-edge. (The gallery/shoot pages keep their own
   12px --gw-pad - that's a different context and is untouched.) */

.gwm-section-head {
  border-top: 2px solid var(--rule);
  padding-top: 14px;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
}

.gwm-section-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--grey);
}

.gwm-section-note {
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mute);
}

.gwm-section-note--soon {
  font-weight: 600;
  letter-spacing: normal;
}

/* ---------- Work tiles (stacked) ---------- */

.gwm-tile {
  display: block;
  color: var(--ink);
  padding-top: 34px;
}

.gwm-tile + .gwm-tile { padding-top: 44px; }

.gwm-tile-frame {
  /* The one rule controlling this photo's size. Height comes from width x
     aspect-ratio and nothing else - deliberately NOT from viewport height.

     This was previously
       width: min(100%, calc((var(--vh100, 100dvh) - 250px) * 0.75));
     which tied the frame's WIDTH to viewport HEIGHT. On a clean 812px-tall
     test viewport the 100% branch always won (351px), so it measured
     correct every time it was checked - but a real iPhone gives Safari's
     URL bar and bottom toolbar their share, cutting innerHeight to ~710.
     There the calc branch won instead (346px), so the photo rendered
     narrower than its own container - a visible gap down the right-hand
     edge - while still standing ~460px tall. Sizing from width has neither
     failure mode: the photo always fills its column, and its height is
     predictable at every screen size regardless of browser chrome.

     3/4 is the design's portrait presentation for photography (same ratio
     the desktop Work tiles and the shoot pages use) and is deliberately
     fixed - the photographs are never recompressed to a shorter frame to
     buy vertical room. If the category title needs to sit closer to the
     photo, tighten the surrounding spacing (.gwm-tile / .gwm-tile-meta
     below), never this ratio. */
  width: 100%;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--surface);
}

.gwm-tile-frame img { width: 100%; height: 100%; object-fit: cover; }

.gwm-tile--soon .gwm-tile-frame {
  display: flex;
  align-items: center;
  justify-content: center;
}

.gwm-tile-soon-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--mute);
}

.gwm-tile-meta {
  padding-top: 14px;
  margin-top: 14px;
  border-top: 2px solid var(--rule);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
}

.gwm-tile-meta h3 {
  margin: 0;
  font-size: 30px;
  font-weight: 500;
  letter-spacing: -0.015em;
  text-transform: uppercase;
  line-height: 1;
}

.gwm-tile-view {
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--grey);
  white-space: nowrap;
}

.gwm-tile > p {
  margin: 14px 0 0;
  font-size: 14px;
  line-height: 1.75;
  color: var(--body-copy);
}

/* ---------- About ---------- */

.gwm-about-body { padding-top: 28px; }

.gwm-about-portrait {
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--surface);
}

.gwm-about-portrait img { width: 100%; height: 100%; object-fit: cover; }

.gwm-about-byline {
  padding-top: 12px;
  margin-top: 12px;
  border-top: 1px solid var(--rule-soft);
  display: flex;
  justify-content: space-between;
  gap: 16px;
  align-items: baseline;
  font-size: 10px;
}

.gwm-about-byline strong { font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; }
.gwm-about-byline span { letter-spacing: 0.06em; color: var(--grey); }

/* Left at .gwm-shop-copy h3's 30px - at this width the two headings already
   agree, so the desktop size cut has no mobile counterpart. Sentence case,
   like its desktop twin. */
.gwm-about-heading {
  margin: 28px 0 0;
  font-size: 30px;
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-wrap: pretty;
}

.gwm-about-copy {
  margin: 22px 0 0;
  font-size: 15px;
  line-height: 1.8;
  color: var(--body-copy);
  text-wrap: pretty;
}

.gwm-about-copy + .gwm-about-copy { margin-top: 18px; }

/* ---------- Shop ----------
   The prints themselves are .home-prints--pair, with the rest of the print
   surfaces near the bottom of this file; this is only the copy that closes
   the section under them. Its top rule is what separates the two - the pair
   above sits 24px apart and reads as one block, then the rule ends it. */

.gwm-shop-copy {
  padding-top: 22px;
  margin-top: 14px;
  border-top: 2px solid var(--rule);
}

.gwm-shop-copy h3 {
  margin: 0;
  font-size: 30px;
  font-weight: 500;
  letter-spacing: -0.015em;
  text-transform: uppercase;
  line-height: 1;
}

.gwm-shop-copy p {
  margin: 18px 0 0;
  font-size: 15px;
  line-height: 1.8;
  color: var(--body-copy);
  text-wrap: pretty;
}

/* ---------- Contact ---------- */

.gwm-contact-body { padding-top: 34px; }

/* .gwm-shop-copy h3's 30px, for the same reason as the desktop rule above:
   the two headings are the same kind of thing and should be the same size.
   Ink is explicit here too, so the mobile and desktop blocks cannot drift
   apart if the surrounding colours are ever changed. */
.gwm-contact-heading {
  margin: 0;
  font-size: 30px;
  font-weight: 500;
  line-height: 1.04;
  letter-spacing: -0.015em;
  text-transform: uppercase;
  color: var(--ink);
  text-wrap: pretty;
}

.gwm-contact-copy {
  margin: 22px 0 0;
  font-size: 15px;
  line-height: 1.8;
  color: var(--body-copy);
  text-wrap: pretty;
}

.gwm-contact-rows {
  margin-top: 30px;
  display: flex;
  flex-direction: column;
}

.gwm-contact-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px 0;
  border-top: 1px solid var(--rule-soft);
}

.gwm-contact-row:last-of-type { border-bottom: 1px solid var(--rule-soft); }

.gwm-contact-row-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--grey);
}

.gwm-contact-row-value { font-size: 16px; }

/* ---------- Shared CTA (More about me / Notify me / Make an enquiry) ---------- */

.gwm-cta {
  display: inline-block;
  margin-top: 26px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 2px solid var(--ink);
  padding-bottom: 8px;
}

.gwm-shop-copy .gwm-cta { margin-top: 24px; }

/* ---------- Footer ---------- */

.gwm-footer {
  margin-top: 72px;
  padding: 16px 20px 36px;
  border-top: 2px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.gwm-footer-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.gwm-footer-name { font-weight: 600; }
.gwm-footer-copyright { color: var(--mute); }

.gwm-footer-notice {
  margin: 0;
  font-size: 10px;
  letter-spacing: 0.01em;
  line-height: 1.6;
  color: var(--mute);
}

/* ---------- Desktop/mobile switch ---------- */

@media (max-width: 600px) {
  .gw-desktop { display: none; }
  .gwm-page {
    display: block;
    /* width: 100% is load-bearing, not decorative - .gwm-page is a flex
       item of body (display:flex, column), and a flex item with auto
       side margins (needed to center it once the viewport exceeds
       520px) stops stretching to the container's width by default: it
       shrinks to fit its own content instead, per the flexbox spec's
       "auto margins absorb free space instead of stretch" rule. On the
       homepage that was invisible because .gwm-page wraps the entire
       page (hero image etc.), which naturally needs close to the full
       viewport width anyway. On automotive.html/lifestyle.html/
       events.html, .gwm-page wraps only the header+nav - much narrower
       content - so it was actually shrink-wrapping to that instead of
       spanning the viewport, which is what made the header read as
       cramped (there was no free space for its contents to spread into,
       because the container itself had shrunk to match them). Explicit
       width: 100% overrides that and restores real stretch behaviour;
       max-width + margin: auto still center it past 520px as before. */
    width: 100%;
    max-width: 520px;
    margin: 0 auto;
    position: relative;
    overflow-x: hidden;
  }
}

/* ---------- Shop: print grid and print page ----------
   Added with the Shopify print shop. Everything here obeys the same design
   system as the rest of the site - Archivo only, no accent colour, 0px
   corners, no borders around pictures, no shadows - and reuses the shared
   --gallery-gap / --gw-pad tokens so the shop is spaced identically to the
   Work grid rather than inventing its own rhythm.

   One deliberate departure from .tile-meta: that block is hidden on
   hover-capable devices, because on the portfolio a caption can wait for a
   hover. A price cannot. .shop-tile-meta is therefore always visible at
   every width and pointer type. */

.shop-heading {
  margin: 0;
  font-size: clamp(20px, 1.9vw, 34px);
  font-weight: 500;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  line-height: 1;
}

.shop-subheading {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mute);
}

/* Only ever shown while js/shopify.js has no credentials. Ruled off rather
   than coloured, because the design system has no accent colour to warn
   with - the rule and the uppercase label do that job instead. */
.shop-preview-note {
  margin: 0 var(--gw-pad);
  padding: 12px 0;
  border-top: 2px solid var(--rule);
  border-bottom: 2px solid var(--rule);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--grey);
  max-width: 68ch;
}

/* TWO columns, not three. At three, a column on a 1440px screen was ~450px
   wide against a 380px-tall band - narrower than the 509px a landscape mockup
   needs to stand a full band tall - so every landscape print became
   width-limited and hung as a visibly shorter block than the portraits beside
   it. Two columns give each print roughly 680px to be large in, which is what
   makes the row work (see .shop-tile .frame below).

   --home-max, not --gallery-max (2200px). That token exists so a shoot page
   can fit an extra photograph per row on a wide monitor; here there are only
   ever two columns, so the same 2200px would simply make each print enormous
   on a large display. 1400px is the measure the homepage's own sections use,
   so the shop sits on the same column of the page as the rest of the site.

   --sp-4 across and --sp-5 down: a wider gutter than the --gallery-gap used
   between photographs in a portfolio wall, because these are separate
   products being compared rather than one continuous gallery, and the row gap
   has to clear the caption block under each tile as well. */
.shop-grid {
  padding: var(--gallery-top) var(--gw-pad) 64px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-5) var(--sp-4);
  max-width: var(--home-max);
  margin: 0 auto;
  width: 100%;
}

.shop-tile { min-width: 0; }

.shop-tile a { display: flex; flex-direction: column; }

/* Every print occupies the SAME image area, so the photographs in a row start
   and finish on the same two lines and the captions under them align - a
   landscape print is never a shorter block than the portrait beside it.

   The tile is a fixed 6:5 box and each orientation is fitted to fill its
   HEIGHT completely, which is the part that has to match:

   - PORTRAIT (mockup canvas 1792x2400) is `contain`. Taller than the box, so
     contain scales it to the box's height and it sits narrower than the
     column with page background either side. Nothing is cropped.
   - LANDSCAPE (2400x1792) is `cover`, set by .frame--wide from js/shop.js.
     Contain would have fitted it to the box's WIDTH instead and left it
     standing ~30% shorter than the portraits - the exact problem this
     replaces. Cover fills the box and trims the sides instead.

   Cropping a landscape mockup is safe in a way cropping the photograph would
   not be. These are framed prints photographed on a brick wall: measured, the
   frame sits inside ~10% of wall on each side, and a 6:5 box trims ~5.2% per
   side, so what is lost is wall, never the frame or the print inside it. That
   is why the ratio is 6:5 and not taller - at ~1.07 the crop would reach the
   frame's edge. Any mockup wider than 4:3 is also the case to re-check here.

   This is the one deliberate exception to the "uncropped image system" note
   below, and it is consistent with it: that rule protects the PHOTOGRAPH from
   being misrepresented, and the photograph is still shown whole. */
.shop-tile .frame {
  position: relative;
  aspect-ratio: 6 / 5;
}

.shop-tile-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-2);
  padding-top: 12px;
  margin-top: 12px;
  border-top: 2px solid var(--rule);
}

.shop-tile-title,
.shop-tile-price {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.shop-tile-title { color: var(--ink); }
.shop-tile-price { color: var(--grey); white-space: nowrap; }

.shop-tile a:hover .shop-tile-title { color: var(--grey); }

.shop-empty {
  padding: var(--gallery-top) var(--gw-pad) 96px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  max-width: 60ch;
}

.shop-empty p {
  margin: 0;
  font-size: 14px;
  line-height: 1.75;
  color: var(--body-copy);
}

/* ---------- One print ---------- */

.print-detail {
  padding: var(--gallery-top) var(--gw-pad) 96px;
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: var(--sp-6);
  align-items: start;
  max-width: var(--content-max);
  margin: 0 auto;
  width: 100%;
}

/* The product gallery. Every image a print carries is stacked here at its own
   natural proportions - no shared height, no shared shape - because these are
   the photographs being sold and each one's framing is the work itself. The
   only constraint is a maximum height, so a tall portrait cannot take over
   the whole screen. */
.print-gallery {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gallery-gap);
}

/* The track is a real horizontal scroll container with CSS scroll-snap, not a
   transform-based carousel. Swiping is therefore the platform's own - real
   momentum and rubber-band on a phone, two-finger swipe on a trackpad - and
   the images remain readable and scrollable even if the script never runs.

   Every slide is exactly one track-width wide and one fixed height tall. That
   fixed height is what lets a portrait and a landscape share a slider without
   the frame resizing as you swipe between them; the photograph is fitted
   inside with object-fit: contain, so neither is cropped and the leftover
   room is simply page background. */
.print-track {
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  height: var(--print-slide-h);
  scroll-snap-type: x mandatory;
  /* Switching print option swaps which slides are displayed. Without this the
     browser helpfully keeps the old one in view and the gallery re-opens on
     the last image instead of the first. */
  overflow-anchor: none;
  overscroll-behavior-x: contain;  /* a swipe past the last image must not
                                      trigger the browser's back gesture */
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;           /* Firefox */
  -ms-overflow-style: none;
}

/* The scrollbar is hidden because the arrows and the counter already say
   where you are; a scrollbar under a photograph is noise. */
.print-track::-webkit-scrollbar { display: none; }

.print-track:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

.print-slide {
  flex: 0 0 100%;
  height: 100%;
  scroll-snap-align: start;  /* slides are exactly one track wide, so start
                                lands on exact multiples of the track width -
                                center drifts a few px and leaves the frame
                                sitting slightly off between photographs */
  scroll-snap-stop: always;  /* one photograph per swipe, never skipping two */
}

.print-slide img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
}

.print-gallery-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding-top: 12px;
  margin-top: 12px;
  border-top: 2px solid var(--rule);
}

.print-nav-arrows { display: flex; gap: var(--sp-2); }

.print-nav-btn {
  border: 0;
  background: none;
  padding: 0;
  font-family: var(--font);
  font-size: 14px;
  line-height: 1;
  color: var(--ink);
  cursor: pointer;
}

.print-nav-btn:hover:not(:disabled) { color: var(--grey); }

/* Disabled rather than hidden, so the control never moves under the cursor
   as you reach either end of the gallery. */
.print-nav-btn:disabled { color: var(--mute); cursor: default; }

.print-counter {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--grey);
}

.print-info {
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  position: sticky;
  top: var(--sp-4);
}

.print-title {
  margin: 0;
  font-size: clamp(22px, 2.2vw, 40px);
  font-weight: 500;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  line-height: 1.05;
}

.print-description {
  margin: 20px 0 0;
  font-size: 14px;
  line-height: 1.75;
  color: var(--body-copy);
  max-width: 42ch;
  text-wrap: pretty;
}

/* The frame disclaimer, directly under the description and above the prices.
   Marked as an aside by a single left rule rather than a tinted box: the
   palette has no accent colour to warn in, and a filled panel would be the
   loudest thing in a column whose entire job is to stay quieter than the
   photograph beside it. The rule is the same 2px weight the sizes table and
   the gallery nav already use, so it reads as part of the system rather than
   as a warning bolted on.
   42ch matches .print-description, so the note sets to the same measure as
   the paragraph it qualifies. */
.print-frame-note {
  margin-top: 24px;
  padding-left: 16px;
  border-left: 2px solid var(--rule);
  max-width: 42ch;
}

/* Full ink and the site's small-label treatment - the same 11px/0.12em
   uppercase used by .print-size and .print-counter. It carries the whole
   message on its own for anyone who only scans the page. */
.print-frame-note-title {
  margin: 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  line-height: 1.45;
  color: var(--ink);
}

.print-frame-note-copy {
  margin: 8px 0 0;
  font-size: 13px;
  line-height: 1.7;
  color: var(--body-copy);
  text-wrap: pretty;
}

/* ---------- Print option: framed print vs print only ----------
   Rendered only for a print whose Shopify product carries the print-only
   images; the rest of the catalogue gets none of this markup and keeps the
   page it always had.

   Two cells rather than a <select>, for the same reason the sizes are three
   radios: there are exactly two choices and seeing both at once IS the
   decision. The 2px rule is the one .print-buy already uses and the label is
   the same 11px/0.12em uppercase as .print-size and .print-counter, so this
   reads as part of the existing system rather than as a new component. No
   fill, no tint, no rounded corner and no accent colour - selection is shown
   the way it is shown everywhere else on this page, by going to full ink. */
.print-option {
  margin-top: 32px;
  width: 100%;
  max-width: 420px;
}

.print-option-label {
  margin: 0 0 12px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--grey);
}

.print-option-cells {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-2);
}

/* min-height 48px is a deliberate floor, not the text's natural height: this
   is the first thing a visitor taps on a phone and a 10px label would
   otherwise give it a target barely half that. */
.print-option-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 12px var(--sp-1);
  border: 2px solid var(--rule-soft);
  border-radius: 0;
  background: transparent;
  cursor: pointer;
  text-align: center;
  font-family: var(--font);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.14em;
  line-height: 1.3;
  text-transform: uppercase;
  color: var(--grey);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.print-option-cell:hover { color: var(--ink); }

.print-option-cell.is-on {
  color: var(--ink);
  border-color: var(--ink);
  font-weight: 600;
}

.print-option-cell:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

.print-sizes {
  margin-top: 32px;
  width: 100%;
  max-width: 420px;
  border-top: 2px solid var(--rule);
}

/* The whole row is the label, so the entire strip is clickable rather than
   just the radio dot. */
.print-size {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: 14px 0;
  border-bottom: 2px solid var(--rule-soft);
  cursor: pointer;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--grey);
}

/* The native radio is hidden visually but stays in the accessibility tree and
   keeps full keyboard behaviour - arrow keys still move between sizes.
   Selection is shown by the row going to full ink weight instead of by a
   control, which suits a design system with no accent colour to tint one. */
.print-size input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.print-size:hover { color: var(--ink); }

.print-size:has(input:checked) {
  color: var(--ink);
  border-bottom-color: var(--ink);
}

.print-size:has(input:checked) .print-size-title { font-weight: 600; }

/* Keyboard focus must stay visible even though the input itself is hidden. */
.print-size:has(input:focus-visible) {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

.print-size.is-sold-out {
  color: var(--mute);
  cursor: not-allowed;
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.print-size-price { white-space: nowrap; }

.print-buy {
  margin-top: 32px;
  padding: 16px 32px;
  border: 2px solid var(--ink);
  border-radius: 0;
  background: var(--ink);
  color: var(--page);
  font-family: var(--font);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease;
}

.print-buy:hover:not(:disabled) {
  background: transparent;
  color: var(--ink);
}

.print-buy:disabled { opacity: 0.5; cursor: default; }

.print-status {
  margin: 16px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--grey);
  max-width: 42ch;
}

/* No red available in the palette, so an error is marked by weight and full
   ink rather than by colour. */
.print-status.is-error { color: var(--ink); font-weight: 500; }

.print-soldout-note {
  margin: 32px 0 0;
  font-size: 13px;
  line-height: 1.75;
  color: var(--body-copy);
  max-width: 42ch;
}

.print-soldout-note a { border-bottom: 1px solid var(--rule); }
.print-soldout-note a:hover { border-bottom-color: var(--ink); }

.print-back { margin: 40px 0 0; }

@media (max-width: 900px) {
  /* The photograph leads on a narrow screen and the buying detail follows it,
     so the picture is still the first thing seen. Sticky is dropped because
     in one column it would pin the panel over the photograph. */
  .print-detail {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-4);
  }

  .print-info { position: static; }
}

@media (max-width: 600px) {
  /* ONE print per row. Two columns on a 390px phone left each print about
     165px wide - a thumbnail of a thing being sold for R850, and a squeezed
     desktop grid rather than a shop. One per row gives each print the full
     content width and makes the photograph the reason to keep scrolling. */
  .shop-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-5);
  }

  /* Each print takes its height from its OWN proportions here rather than
     from the shared 6:5 box desktop uses - that box exists so two tiles side
     by side line up, and in a single column there is nothing to line up with.
     The rules that do it live with the uncropped image system at the bottom
     of this file, because they have to come after the ones they override. */

  .shop-tile-title,
  .shop-tile-price { font-size: 10px; letter-spacing: 0.08em; }

  /* Full width so the primary action is comfortably tappable. */
  .print-buy { width: 100%; }

  .print-sizes { max-width: none; }
}

/* ---------- The uncropped image system (prints only) ----------
   Added when the print shop went live.

   RULE: the layout decides how BIG a photograph is, never which parts of it
   you can see. Everywhere an actual print is displayed - the homepage Shop
   preview, the shop grid, the product gallery - the image is fitted inside
   its box rather than filling it, so a landscape photograph stays landscape
   and a portrait one stays portrait, both complete.

   This is deliberately the OPPOSITE of how the portfolio behaves. The Work
   tiles, hero and About portrait all use `object-fit: cover` against a fixed
   aspect-ratio, and they should keep doing so: there the crop is an editorial
   choice about a photograph on display. A print is a thing being sold, and
   cropping the preview would misrepresent what arrives in the tube.

   Tiles line up because each one is a fixed-ratio box that its photograph
   fills the HEIGHT of, so images start and finish on the same two lines and
   only their widths vary. That is how prints hang on a wall.

   ONE exception, and it is about the mockup rather than the photograph:
   .shop-tile .frame--wide uses cover, because a landscape mockup fitted by
   width stands far shorter than the portrait beside it. What cover trims is
   the brick wall the frame was photographed on, never the frame or the print.
   The reasoning and the measurements are on .shop-tile .frame above; don't
   extend cover to any other print surface. */
:root {
  /* One slider frame. Capped in both vh and px so it stays restrained on a
     phone (never a full-screen image viewer) and does not grow absurdly tall
     on a large monitor. Every print sits inside this, uncropped. */
  --print-slide-h: min(70vh, 720px);
  --print-slide-h-mobile: min(52vh, 420px);
}

.home-print-frame,
.shop-tile .frame { display: block; position: relative; }

/* The photograph is taken OUT OF FLOW so the frame's aspect-ratio is the only
   thing that decides how tall the tile is.

   In flow it is not: `aspect-ratio` sets a preferred height, but a block still
   grows to fit its content, and an in-flow image whose own `height: 100%`
   cannot resolve falls back to its intrinsic ratio and pushes the frame taller.
   Measured, that is exactly what happened - portrait tiles stood 868px tall
   inside a box asking for 540px, so the 6:5 box did nothing and portrait and
   landscape went back to different heights. Absolute positioning removes the
   image from that calculation entirely.

   It also keeps the original reason the image fills the box rather than sizing
   to its own content: the tile reserves its full height before the photograph
   has downloaded, where sizing to content collapsed every tile to 0x0 until
   its image arrived and the whole grid jumped as they loaded in. (The one
   place that does size to content is the mobile shop grid, where there is one
   tile per row and width/height attributes reserve the box instead.)

   object-fit: CONTAIN - never cover, with the single .frame--wide exception
   below. Contain is uncroppable by definition: the whole photograph is scaled
   to fit inside the box and any leftover room becomes empty space, where cover
   would fill the box by cutting the picture. That empty space is invisible
   because the frame has no background of its own, so a portrait simply sits
   narrower in its column.

   object-position pins the picture to the bottom of the box, so prints in a
   row sit on one hang line and their captions align. */
.home-print-frame img,
.shop-tile .frame img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center bottom;
}

/* The landscape mockup, and the one place cover is correct - see
   ".shop-tile .frame" for the measurements that make it safe. It has to come
   after the shared rule above: both selectors carry the same specificity, so
   source order is what decides which object-fit wins. */
.shop-tile .frame--wide img {
  object-fit: cover;
  object-position: center;
}

/* ---------- Homepage: Shop ---------- */

/* Sits opposite the section label, in the same small-caps idiom as
   .back-link, so it reads as navigation rather than as a call to action. The
   brand brief rules out booking banners and hard sells; this is a doorway,
   not a button. */
.gw-section-link,
.gwm-section-link {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--grey);
}

.gw-section-link:hover,
.gwm-section-link:hover { color: var(--ink); }

/* Two prints, shown as PHOTOGRAPHS - no caption, no price. This is a preview
   of a collection, not two products for sale; .gw-shop-copy beside it carries
   the words. See the note above #shop in index.html for why.

   The pair takes SEVEN of .gw-shop's twelve columns, split into two with the
   grid's own --sp-3 between them, which puts each print at ~390x520 on a
   1440px screen. That number is the whole balance of this section, and it was
   arrived at from both ends:

   - Two prints at the section's FULL width stood 885px tall, exactly as tall
     as the Work plates, and turned a teaser into the heaviest block on the
     homepage.
   - Pulled back to six columns they dropped to ~322x430 - lighter than the
     About portrait beside them, and too slight to carry a section of their
     own against all the space to their right.

   Seven columns sits between the two: substantial enough that the section
   reads as important, still clearly under Work, which is what should dominate
   a portfolio homepage.

   Sizing the prints by column span rather than by capping the block's width
   is deliberate: the section head, .gw-tiles and .gw-about all run the full
   --home-max, and a narrower centred block under a full-width rule leaves the
   label and the rule overhanging the photographs (the alignment bug the Work
   section was rebuilt to fix - see .gw-tiles). Here the grid still spans the
   full measure; only the prints occupy less of it.

   3:4 matches the 1792x2400 mockups closely enough that object-fit: contain
   never letterboxes visibly, and both featured prints are portrait by
   construction (js/home-prints.js). */
.home-prints {
  grid-column: 1 / span 7;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3);
  width: 100%;
}

/* No hover state on the photographs, deliberately. The prints carry no
   caption to underline and the section is meant to stay quiet; the heading,
   copy and CTA beside them are what invite the click. */
.home-print { display: block; color: var(--ink); }

.home-print-frame { aspect-ratio: 3 / 4; }

/* ---------- Mobile: the shop pair ----------
   The mobile homepage is a separate DOM (.gwm-page) that only ever renders
   below 600px, so none of this needs a media query of its own.

   Two prints SIDE BY SIDE, keeping the desktop composition rather than
   unfolding it into a column. Each one lands around 160x213 inside
   .gwm-section's 20px padding, which is small for a photograph - and is the
   right trade here, because these two are a glimpse of a collection rather
   than two things to examine. Stacking them full-width read as two products
   presented one after another, and made a teaser section taller than the
   phone screen twice over (measured 1260px against ~640px for this).

   That reasoning is specific to the HOMEPAGE preview. The /shop catalogue
   goes the other way on a phone - one print per row at full width - because
   there a visitor is looking at a thing to buy, at its price, and 160px is a
   thumbnail of it. Same photographs, opposite job, opposite layout; see
   .shop-grid's own mobile breakpoint.

   34px above matches .gwm-tile's rhythm; --sp-2 between the pair is close
   enough that they read as one block, which .gwm-shop-copy's top rule then
   closes off underneath. */
.home-prints--pair {
  grid-column: auto;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  padding-top: 34px;
  gap: var(--sp-2);
}

@media (max-width: 600px) {
  /* The mobile shop grid is ONE print per row, so each one takes its height
     from its OWN proportions instead of the shared 6:5 box: a portrait stands
     tall, a landscape sits wide, both run the full content width, and nothing
     is cropped at all on a phone - not even the landscape print.

     The image goes back INTO flow to do that, because here its own height is
     what the frame should take. js/shop.js emits width/height on every image,
     so the browser still reserves the right box before the photograph arrives
     and nothing reflows.

     This has to sit AFTER the uncropped image system above, not up in the
     breakpoint next to .shop-grid where it belongs by subject: every one of
     these selectors ties with the rule it overrides on specificity, so source
     order is the only thing that decides the winner. Placed earlier it lost
     silently and every mobile tile collapsed to 0px tall. */
  .shop-tile .frame { aspect-ratio: auto; }

  .shop-tile .frame img,
  .shop-tile .frame--wide img {
    position: static;
    height: auto;
    object-fit: contain;
  }

  /* The product gallery on a phone: restrained, so a single print never
     becomes a full-screen image viewer. */
  .print-track { height: var(--print-slide-h-mobile); }
}

/* ===========================================================================
   Cart: header button, drawer, and the print page's two purchase buttons
   (added 2026-08-29)
   ===========================================================================
   Built from the same tokens as everything else - --ink, --page, --rule,
   --sp-*, 10px/0.2em uppercase for controls, 0px corners, no shadows. There
   is no accent colour and no badge fill: the count sits beside the bag in the
   same ink as the nav, because the brief's rule that the photography supplies
   the colour applies to commerce too. */

.cart-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  line-height: 1;
}

.cart-bag {
  width: 13px;
  height: 14px;
  display: block;
}

/* The count is typographic, not a filled pill - a coloured circle would be
   the one loud element on an otherwise silent page. */
.cart-count {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  font-variant-numeric: tabular-nums;
}

.cart-btn:hover { color: var(--grey); }

/* On mobile the bag lives in the header beside the Menu button, NOT inside
   the slide-out nav. A count a shopper has to open a menu to see is not a
   count - the whole point is that adding a print is visibly acknowledged
   wherever they are. .gwm-header is a two-child space-between row, so the bag
   takes the auto margin and the menu button follows it. */
.gwm-cart-btn {
  margin-left: auto;
  margin-right: 18px;
  color: var(--ink);
}

.gwm-cart-btn .cart-bag { width: 15px; height: 17px; }

/* ---- drawer ---- */

.cart-drawer {
  position: fixed;
  inset: 0;
  z-index: 120;
}

.cart-drawer[hidden] { display: none; }

.cart-scrim {
  position: absolute;
  inset: 0;
  background: rgba(32, 30, 29, 0.28);
  opacity: 0;
  transition: opacity 0.26s ease;
}

.cart-drawer.is-open .cart-scrim { opacity: 1; }

.cart-panel {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: min(420px, 100%);
  background: var(--page);
  border-left: 1px solid var(--rule-soft);
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.26s ease;
}

.cart-drawer.is-open .cart-panel { transform: translateX(0); }

@media (prefers-reduced-motion: reduce) {
  .cart-scrim, .cart-panel { transition: none; }
}

/* Dimmed and click-through while a Shopify round trip is in flight, so two
   quick taps on "+" cannot race each other. */
.cart-drawer.is-busy .cart-body { opacity: 0.55; pointer-events: none; }

.cart-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: var(--sp-3) var(--sp-3) var(--sp-2);
  border-bottom: 1px solid var(--rule-soft);
}

.cart-head-title,
.cart-close {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.cart-close {
  padding: 0;
  border: 0;
  background: none;
  color: var(--grey);
  font-family: var(--font);
  cursor: pointer;
}

.cart-close:hover { color: var(--ink); }

.cart-body {
  flex: 1 1 auto;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  transition: opacity 0.15s ease;
}

.cart-lines {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1 1 auto;
}

.cart-line {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr) auto;
  gap: var(--sp-2);
  padding: var(--sp-3);
  border-bottom: 1px solid var(--rule-soft);
}

.cart-line-thumb { display: block; }

.cart-line-thumb img {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  display: block;
}

.cart-line-detail {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.cart-line-title {
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
  color: var(--ink);
}

.cart-line-title:hover { color: var(--grey); }

.cart-line-variant,
.cart-line-unit {
  font-size: 11px;
  line-height: 1.5;
  color: var(--grey);
}

.cart-line-foot {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: 6px;
}

.cart-qty {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--rule-soft);
}

.cart-qty-btn {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font-family: var(--font);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}

.cart-qty-btn:hover { color: var(--grey); }

.cart-qty-value {
  min-width: 24px;
  text-align: center;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

.cart-remove {
  padding: 0;
  border: 0;
  background: none;
  color: var(--grey);
  font-family: var(--font);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
}

.cart-remove:hover { color: var(--ink); }

.cart-line-total {
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.cart-foot {
  padding: var(--sp-3);
  border-top: 1px solid var(--rule);
}

.cart-subtotal {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 12px;
  font-weight: 500;
}

.cart-subtotal span:last-child { font-variant-numeric: tabular-nums; }

.cart-note {
  margin: var(--sp-1) 0 0;
  font-size: 11px;
  line-height: 1.6;
  color: var(--grey);
}

.cart-checkout { width: 100%; margin-top: var(--sp-2); }

.cart-status { margin-top: var(--sp-1); }

.cart-empty {
  padding: var(--sp-5) var(--sp-3);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-3);
}

.cart-empty p {
  margin: 0;
  font-size: 13px;
  color: var(--body-copy);
}

/* Stops the page behind the drawer scrolling under it on touch. */
html.cart-open, html.cart-open body { overflow: hidden; }

/* ---- print page: two purchase buttons ---- */

.print-actions {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  margin-top: 32px;
  /* .print-info is a flex column with align-items: flex-start, so without
     this the actions row collapses to its content width and the buttons come
     out around 45% of the column - a poor tap target on a phone. */
  align-self: stretch;
}

/* .print-buy already carries the filled-ink treatment; these only add the
   states the single old button never needed. margin-top moves to the flex
   container so the two buttons sit tight together. */
.print-actions .print-buy { margin-top: 0; }

/* Buy now is the quieter of the two: adding to the cart is the primary,
   lower-commitment action, so it keeps the filled button. */
.print-buy.is-secondary {
  background: transparent;
  color: var(--ink);
}

.print-buy.is-secondary:hover:not(:disabled) {
  background: var(--ink);
  color: var(--page);
}

.print-buy.is-busy { opacity: 0.55; }

/* Success reads as a state change on the button itself rather than a message
   elsewhere, so confirmation appears exactly where the click happened. */
.print-buy.is-done {
  opacity: 1;
  background: transparent;
  color: var(--ink);
}

.print-buy:disabled { cursor: default; }

@media (min-width: 601px) {
  /* Side by side once there is room, still equal weight in the layout. */
  .print-actions { flex-direction: row; }
  .print-actions .print-buy { flex: 1 1 0; }
}

/* ===========================================================================
   Shipping bar (added 2026-08-29)
   ===========================================================================
   A single element at the top of <body>, outside both .gw-desktop and
   .gwm-page, so one bar serves the desktop and mobile structures rather than
   the duplicated pair every other header component needs.

   Deliberately quiet: ink on --surface, the same 10px/0.2em uppercase the nav
   links and buttons use, and a hairline rule underneath. It reads as a line of
   type, not as a promotional banner - the brand brief rules out anything that
   behaves like a booking banner, and this sits one line above the wordmark. */

.ship-bar {
  background: var(--surface);
  border-bottom: 1px solid var(--rule-soft);
  padding: 7px var(--gw-pad);
  text-align: center;
}

.ship-bar span {
  display: inline-block;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  line-height: 1.3;
  color: var(--grey);
}

@media (max-width: 600px) {
  /* The line is 52 characters and it DOES fit one row on a 375px phone, but
     only if it is allowed the full width - an earlier max-width here broke it
     onto three rows and made the bar taller than the wordmark beneath it.
     Tracking is eased from 0.2em to 0.1em to buy the room; below ~340px it
     wraps to two centred rows rather than overflowing, which is why the
     padding is small and the height is left to the content. */
  .ship-bar {
    padding: 6px 10px;
  }

  .ship-bar span {
    font-size: 8.5px;
    letter-spacing: 0.1em;
  }
}
