
/* ── ROOT ───────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --cream:   #f5f2ed;
  --paper:   #ede9e2;
  --rule:    rgba(40,36,30,0.14);
  --rule-lt: rgba(245,242,237,0.16);
  --mid:     #8a8279;
  --ink:     #17140f;
  /* The hero's height, declared here rather than on the stage: the gallery is pulled up
     by exactly this much so it is already pinned behind the hero, and both need it. */
  /* Two thirds of the screen. Everything in the hero is sized against this band rather
     than against the window, so the drawing, the rings and the dissolve all come down
     with it. */
  --hero-height: 100svh;   /* the hero takes the whole screen again */
  --accent:  #304599;   /* sampled from the supplied swatch */
  --hot:     #ff5a1f;   /* bright orange, used as a second accent where the page wants
                           a mark rather than a colour: numbers, rules, small labels */
  /* Instrument Serif is a display face — high contrast, meant for the italic
     accents only. Body copy is set in the grotesque. */
  --serif:   'Instrument Serif', Georgia, serif;
  /* system-ui ahead of the old grotesques: it resolves to SF Pro, Segoe UI or Roboto
     depending on the platform, all of which sit in the same neo-grotesque lineage as
     Inter Tight — so if the webfont is slow or blocked the substitution barely shows.
     Arial stays last for browsers that don't understand system-ui. */
  --sans:    'Inter Tight', system-ui, -apple-system, 'Segoe UI', Roboto,
             'Helvetica Neue', Arial, sans-serif;
}

html { font-size: 16px; }
/* Lenis owns the smoothing now; a native `scroll-behavior: smooth` would be a second
   hand on the same scroll position, and the two fight. Its own classes below are what
   the library asks for. */
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }
.lenis.lenis-smooth iframe { pointer-events: none; }

body {
  background: #fff;
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  /* `clip`, not `hidden`. `overflow-x: hidden` on the body makes it a scroll container,
     and Safari then treats the body — not the viewport — as the scrolling ancestor for
     `position: sticky`. The body has no scrollable overflow of its own, so nothing ever
     sticks: the pinned gallery and the pinned hero both just scroll past, which takes
     the horizontal run with them. Chrome propagates the body's overflow to the viewport
     and hides the fault. `clip` stops sideways overflow without creating a scroll
     container, so sticky keeps working. */
  overflow-x: clip;
}

/* ── Film grain over the whole page ──────────────────────────────────────────────
   Fixed rather than scrolled: grain belongs to the print, not to what is printed on it,
   so it should sit still while the page moves under it.
   Two departures from the usual recipe, both for cause:
   • `feColorMatrix saturate 0`. feTurbulence generates each channel independently, so
     without this every speck is a random colour and what you get is RGB television
     static, not film grain — the same fault the footer lockup's grain had.
   • `inset: 0` rather than 100vw/100vh. On a desktop with a visible scrollbar, 100vw is
     wider than the viewport, which pushes the layer sideways. */
body::after {
  content: "";
  position: fixed; inset: 0;
  pointer-events: none;
  z-index: 9999;
  opacity: 0.045;
  /* This layer covers the whole viewport and never changes, but it sits above every
     other thing on the page — without a layer of its own the compositor has to redraw
     the screen through it on each scrolled frame. Promoting it is the difference
     between a texture and a tax, and it is felt on a phone long before a desktop. */
  will-change: transform;
  background: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
}

a { text-decoration: none; color: inherit; }
img, video { display: block; max-width: 100%; }

::-webkit-scrollbar { width: 1px; background: #fff; }
::-webkit-scrollbar-thumb { background: var(--mid); }

/* ── POINTER LABEL ──────────────────────────────── */
/* The system cursor is left alone. This is a single word that appears beside it over
   things worth naming, and nothing anywhere else — information rather than ornament.
   The word comes from `data-cursor` on the target, so it is per-element, not global. */
/* A small floating pill that trails the pointer. `left`/`top` stay at 0 and the
   position is written as a transform, so the follow runs on the compositor; the
   margin is what offsets it from the hotspot so it sits beside the arrow. */
/* The words that appear over a picture on hover. No pill, no ground of its own and no
   shadow — blue type sitting just above the picture's top edge. The transform puts it
   at the picture's own corner (written from app.js) and the margin lifts it clear, so
   the two are set independently and the one the script writes is never the one that
   positions it relative to the picture. */
#cur {
  position: fixed; left: 0; top: 0; z-index: 9999; pointer-events: none;
  /* Centred on the picture itself, so no offset of its own. */
  margin-top: 0;
  opacity: 0; transition: opacity .22s ease;
  font-family: var(--sans); font-size: .64rem; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; white-space: nowrap;
  color: var(--accent);
  /* The same pill the menu at the foot of the page wears: a pale, blurred ground, a
     hairline border and a fully rounded end. The blur is what makes it legible over a
     photograph without a hard box around it. */
  padding: .62rem 1.15rem;
  background: rgba(255, 255, 255, .7);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border: 1px solid rgba(0, 0, 0, .08);
  border-radius: 999px;
  box-shadow: none;
}
#cur.show { opacity: 1; }
/* A dot before the words, in the same blue — a mark to hang the label off rather than
   a bullet, so it is sized to the type and set on its middle. */
#cur::before {
  content: ''; display: inline-block; vertical-align: middle;
  width: .42em; height: .42em; margin-right: .62em; margin-top: -0.12em;
  /* The dot stays blue while the words are orange. */
  border-radius: 50%; background: var(--accent);
}

/* ── The spray the cursor leaves when it is thrown about ─────────────────────────
   A canvas over the whole window, holding a few hundred small orange dots at most. It
   is drawn to only while there is something on it, and it never catches the pointer.
   One canvas rather than hundreds of elements: at that count the browser would spend
   its whole frame laying out boxes that live for half a second. */
.cursor-spray {
  position: fixed; inset: 0; z-index: 9997;
  pointer-events: none;
}
@media (hover: none), (pointer: coarse) { .cursor-spray { display: none; } }

/* One colour in both places: the words are blue whatever they are sitting over. */
#cur.on-dark { color: var(--accent); }
/* No pointer to sit beside on a touch screen, and it is motion nobody asked for
   under reduced-motion. */
@media (prefers-reduced-motion: reduce), (hover: none), (pointer: coarse) {
  #cur { display: none; }
}

/* ── NAV ────────────────────────────────────────── */
nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  /* Shallower at the top than at the foot: her name and the three rules are centred in
     this box, so taking the top padding in is what lifts the lockup toward the edge of
     the page without moving the pills in the corner. */
  /* A bar of its own height rather than a box as tall as whatever hangs off it: the
     name at one end, the three controls at the other, and a ground that only arrives
     once the page has been scrolled under it. */
  /* A bar of its own height rather than a box as tall as whatever hangs off it: the
     name at one end, the three controls at the other, and a ground that only arrives
     once the page has been scrolled under it. */
  /* A bar of its own height rather than a box as tall as whatever hangs off it: the
     name at one end, the three controls at the other, and a ground that only arrives
     once the page has been scrolled under it. */
  height: 68px; padding: 0 clamp(1.25rem, 3vw, 2.75rem);
  display: flex; align-items: center; gap: 1rem;
  background: rgba(255, 255, 255, 0);
  border-bottom: 1px solid transparent;
  /* Transparent so the hero images run up behind it. The mobile menu panel keeps
     its own solid background — see below. */
  background: transparent;
  /* No difference blend: against the now-undimmed pale hero it inverted the white
     logo to blue and the links to dark. Plain white instead. */
  color: #fff;
  /* Opacity is written every frame by the scroll now — see updateHeaderFade — so only
     the colour is eased. */
  transition: color .35s ease, background-color .35s ease,
              border-color .35s ease, backdrop-filter .35s ease;
  opacity: var(--header-fade, 1);
}
/* Frosted only once there is something behind it worth separating from. */
nav.is-stuck {
  background: rgba(255, 255, 255, .74);
  -webkit-backdrop-filter: blur(16px) saturate(1.5);
  backdrop-filter: blur(16px) saturate(1.5);
  border-bottom-color: rgba(20, 20, 20, .07);
}
/* The header's own ground, and it does not fade in evenly — it fills in as pixels. The
   layer is a grid of cells, each carrying its own threshold, and each turns on the
   moment the scroll's number passes that threshold. One variable changes; the engine
   does the rest, so a hundred-odd cells cost one write per frame rather than a hundred.
   The multiplier is what makes each cell snap rather than fade. */
.nav-ground {
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  display: grid;
  grid-template-columns: repeat(var(--cols, 40), 1fr);
  grid-template-rows: repeat(var(--rows, 4), 1fr);
  /* Dense, so the big squares can sit among the small ones without leaving holes. */
  grid-auto-flow: row dense;
  /* Any row the spans push past the declared count has to be sized too, or the squares
     that land in it come out with no height at all. */
  grid-auto-rows: 1fr;
}
.nav-ground i {
  background: #fff;
  opacity: clamp(0, calc((var(--nav-white, 0) - var(--t, 0)) * 16), 1);
}
/* Not every square is one cell: some take two across and two down, so the fill reads as
   pixels of a few sizes rather than one even mesh. */
.nav-ground i.big { grid-column: span 2; grid-row: span 2; }
.nav-ground i.wide { grid-column: span 2; }
/* And a smaller tier still: a cell split four ways, each quarter arriving on its own
   count. Nothing else changes — the quarters are the same kind of square, a size down. */
.nav-ground i.quad {
  background: none;
  display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr;
}
.nav-ground i.quad b {
  background: #fff;
  opacity: clamp(0, calc((var(--nav-white, 0) - var(--t, 0)) * 16), 1);
}
.nav-ground i.quad b.warm { background: var(--hot); position: relative; }
.nav-ground i.quad b.warm::after {
  content: ''; position: absolute; inset: 0; background: #fff;
  opacity: max(clamp(0, calc((var(--nav-white, 0) - var(--t2, 1)) * 9), 1), var(--run, 0));
}
/* Some arrive orange and turn white a moment later. Two layers rather than a colour
   mix: the square is orange, and a white one over it comes up on a second, later
   threshold — which is a plain opacity, and so resolves everywhere. */
.nav-ground i.warm { background: var(--hot); position: relative; }
.nav-ground i.warm::after {
  content: ''; position: absolute; inset: 0; background: #fff;
  /* Whichever comes first: the square's own turn, or the moment the strip starts
     running sideways — by then the orange has done its job and the header should be
     plain white behind the pictures. */
  opacity: max(clamp(0, calc((var(--nav-white, 0) - var(--t2, 1)) * 9), 1), var(--run, 0));
}
/* Nothing behind the lockup: the ground the header used to fill in with is off, so the
   mark and the name sit straight on whatever the page is showing underneath. The
   machinery that dealt it is left in place — the number it fills on still drives the
   colour of the type — but none of it is painted. */
.nav-ground { display: none; }

/* The type darkens on the same number the ground lightens on. Without this the header
   spends the middle of the fade as white type on half-white, which is a moment of
   nothing legible; here the two cross together. The plain colours above stay as the
   fallback for anything without color-mix. */
@supports (color: color-mix(in srgb, #000 50%, #fff)) {
  nav .nav-links a, nav .nav-brand-name {
    color: color-mix(in srgb, #000 calc(var(--nav-white, 0) * 100%), #fff);
  }
  nav .nav-name-left, nav .nav-toggle span {
    background: color-mix(in srgb, #000 calc(var(--nav-white, 0) * 100%), #fff);
  }
}
/* The header's box is as tall as the column of blocks that hangs off it, so left as it
   was it lay over the whole wordmark and took every pointer the mark should have had.
   The box itself is let through; the things in it that are meant to be used take their
   pointers back. */
nav { pointer-events: none; }
nav a, nav button, .nav-corner,
.nav-portfolio-panel, .nav-about-panel { pointer-events: auto; }

nav.hidden { opacity: 0; pointer-events: none; }
body.nav-hidden .nav-links { opacity: 0; pointer-events: none; }
.nav-name-text {
  font-family: var(--serif);
  font-size: 1.1rem;
  color: var(--cream);
}
.nav-name-text em { font-style: italic; }
/* Brand mark. The logo is white on transparency, so it rides the nav's existing
   mix-blend-mode: difference and inverts by itself over the pale sections —
   no separate light/dark asset needed. */
/* Top left, with the wordmark beside it. `order` rather than moving the markup, since
   the name comes first in the DOM; the auto margin on the name absorbs the free space
   so the pair stays grouped left and the links stay hard right. */
.nav-brand {
  order: 1; margin-right: 0.85rem;
  display: flex; align-items: center; gap: 0.85rem; text-decoration: none;
}
.nav-mark {
  display: block; height: 4.1rem; aspect-ratio: 600 / 429;
  /* The PNG becomes a mask, so the colour is a gradient we control rather than a
     fixed asset — that's what makes it tintable and interactive. */
  -webkit-mask: url("brand.svg") center / contain no-repeat;
          mask: url("brand.svg") center / contain no-repeat;
  /* Flat ink. The polished-metal gradient that used to fill the mask, the highlight
     that tracked the pointer across it, and the lift under the cursor are all gone —
     it is one solid colour that the page state alone decides. */
  background: #000;
  animation: brand-in .9s cubic-bezier(.22,.61,.36,1) both;
  transition: background-color .35s ease;
}
@keyframes brand-in {
  from { opacity: 0; transform: translateY(-8px) scale(.86); }
  to   { opacity: 1; transform: none; }
}
/* Vestibular safety: no entrance. */
@media (prefers-reduced-motion: reduce) {
  .nav-mark { animation: none; transition: none; }
}
/* The name now lives here rather than in the hero. The img is alt="" because this
   text is the link's accessible name — otherwise it would be announced twice. */
/* Matches .nav-links a exactly — same family, size, weight, tracking and case. */
.nav-brand-name, .nav-name-left {
  font-family: var(--sans); font-size: 1.25rem; font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: #fff; white-space: nowrap;
  transition: color .35s ease;
}
.nav-name-left {
  display: block; height: 3.2rem; aspect-ratio: 1196 / 161;
  overflow: hidden; text-indent: -200%;
  background: #fff;
  -webkit-mask: url("wordmark.png") left center / contain no-repeat;
          mask: url("wordmark.png") left center / contain no-repeat;
  transition: background-color .35s ease;
}
/* The pill behind the black lockup. It is a box of its own rather than a background on
   either the name or the rules, because those two are placed independently — the name at
   the header's centre, the rules measured out from its right edge — and no single element
   contains them both. Its position, size and strength are written every frame by the same
   pass that places the rules. */
.nav-lockup-pill {
  position: absolute; left: 0; top: 0; width: 0; height: 0;
  z-index: 0; pointer-events: none;
  opacity: 0;
  /* Carried lightly, like the pills in the footer: the blur is what keeps her name
     legible at this strength rather than the white itself, and the hairline goes pale,
     since a dark edge reads as an outline once the fill is this thin. */
  background: rgba(255, 255, 255, .34);
  -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
  border: 1px solid rgba(255, 255, 255, .5);
  border-radius: 999px;
}
/* Both stand on it rather than under it. */
/* The film goes soft on the same approach. Nought at rest, so there is no filter on the
   player to composite while the hero is being watched. */
.hero-film { filter: blur(var(--film-blur, 0px)); }

/* The mark in the corner, on the blue. Set from the header's own top rather than left in
   the row, so it sits in the middle of the blue block whatever the row beside it does,
   and on the page's own left margin. Above the block, below nothing else. */
.nav-brand {
  position: absolute; z-index: 1;
  left: 3rem; top: 1.6rem; transform: translateY(-50%);
  margin: 0;
}
.nav-brand .nav-mark { height: 2rem; }
@media (max-width: 780px) {
  .nav-brand { left: 1.6rem; top: 0.9rem; }
  .nav-brand .nav-mark { height: 1.2rem; }
}

/* Small type at the far left of the header. Set to the top of the header rather than
   its middle, since the header is tall enough to carry the pills further down, and kept
   above the hero's own wordmark so it stays legible where the two meet. */
/* The far corner from the name, in the same face but set smaller — it is a note about
   where the studio is, not a second name. Last in the row, so the name's auto margin
   pushes it to the edge. */
.nav-place {
  order: 8; margin: 0;
  font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-weight: 700;
  font-size: .68rem; letter-spacing: -.01em;
  color: var(--ink); line-height: 1;
}
@media (max-width: 620px) { .nav-place { display: none; } }

/* On the name's own line and in its face, but placed over the two G of the mark — the
   middle of that pair is written by app.js, the same way the two lists are placed. */
.nav-studio {
  position: absolute; left: var(--gg-mid, 50%); top: var(--menu-y, 1rem);
  transform: translateX(-50%); margin: 0; z-index: 2;
  font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-weight: 700;
  font-size: .86rem; letter-spacing: -.01em; line-height: 1;
  color: var(--ink); white-space: nowrap;
}
.nav-studio { animation: listsUp .75s cubic-bezier(.16, 1, .3, 1) 1.95s both; }
@media (max-width: 900px) { .nav-studio { display: none; } }

.nav-tiny {
  /* First in the bar, standing in the row rather than laid over it. */
  order: 0; margin-right: auto;
  font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-weight: 700;
  font-size: .86rem; letter-spacing: -.01em;
  color: var(--ink); text-decoration: none; line-height: 1;
}

.nav-name-left { z-index: 1; }
.nav-burger-wrap { z-index: 4; }

.nav-name-left {
  /* In the hero now, not in the header: it stands at the top of the two fifths right of
     the vertical rule, above her trade, and travels with the section rather than staying
     with the page. Ranged left from the rule rather than centred on anything. */
  /* At the top of the two fifths right of the rule, just under the header's band — the
     pills that used to stand there have moved down below it. */
  position: absolute; left: calc(60% + 1.4rem); top: 6.4rem;
  z-index: 3;
  transform: translateY(-50%) scale(var(--name-scale, 1));
  transform-origin: 0 50%;
  transition: background-color .35s ease, transform .18s linear;
  margin: 0;
}

/* Once the gallery's horizontal run is over, the header carries the menu alone. The
   lockup has no panel behind it, so over the grid's full-bleed photographs it would
   sit unprotected — it clears instead. Set from updateNavBrand in app.js. */
.nav-brand, .nav-name-left {
  /* No opacity transition: the value is written every scroll frame, so easing here
     would only lag behind the page. */
  opacity: calc(var(--brand-fade, 1) * var(--lockup-fade, 1));
  transition: color .35s ease;
}
/* The monogram at the left goes as the posts are read: --blog-fade is one until the blog
   reaches the header and nothing once it has been scrolled well into, so the corner
   clears for the work rather than keeping a mark over it. */
.nav-brand {
  opacity: calc(var(--brand-fade, 1) * var(--lockup-fade, 1) * var(--blog-fade, 1));
}
nav.brand-gone .nav-brand,
nav.brand-gone .nav-name-left { pointer-events: none; }
/* Over the hero the name is not in the header, because the hero is busy turning its
   drawings into that same name — two of it at once is one too many. It arrives as the
   gallery does. A third factor rather than a replacement, so it multiplies with the two
   fades already governing the lockup instead of overruling them. */
.nav-name-left { opacity: calc(var(--brand-fade, 1) * var(--lockup-fade, 1) * var(--hero-brand, 1)); }

.s2-item.is-filtered-out { display: none; }
/* A post that is not in the chosen category steps aside rather than vanishing outright,
   so the list settles instead of jumping. */
.post-row.is-filtered-out { display: none; }
.post-list.is-empty::after {
  content: 'Nothing filed here yet';
  display: block; padding: 2rem 0;
  font-family: var(--sans); font-size: .68rem; letter-spacing: .12em;
  text-transform: uppercase; color: rgba(23, 20, 15, .45);
}
/* Nothing to show for a category that has no pictures in it yet. */
.s2-grid.is-empty::after {
  content: 'Nothing in this category yet.';
  grid-column: 1 / -1; padding: 3rem 0;
  font-family: var(--sans); font-size: .85rem; color: rgba(23,20,15,.5);
}
/* ── The menu sits in the header ──
   The far right of the nav's row, opposite the name. It keeps the frosted pill it wore
   as a dock — the surface is what makes it legible over both the film and the white
   page — but it is part of the header now and travels with it. */
.nav-links {
  /* In the header's own row, at the far right — the name holds the left, the mark the
     middle, this the end. It was a dock at the foot of the window and then a pill in the
     band; it is neither now, so it needs no place of its own. */
  position: relative; z-index: 101;
  display: flex; gap: 0; align-items: stretch;
  margin: 0; padding: 0;
  gap: 2rem; padding: .75rem 2rem;
  background: rgba(255, 255, 255, .7);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border: 1px solid rgba(0, 0, 0, .08);
  border-radius: 999px;
  /* Held at the head of the header's row, whatever else is in it. */
  order: -1; margin-right: auto; margin-left: 0;
  /* It hears the cursor whatever the header is doing around it — the header itself is
     made deaf over the hero, and the menu lives inside it. */
  pointer-events: auto;
  /* No longer tied to --nav-fade: that number was the dock's, and it arrives with the
     gallery — in the header the pill is wanted from the moment the introduction lands. */
  transition: opacity .4s ease, visibility .4s ease;
}
/* The menu is there from the first frame. It used to wait out the introduction, on the
   grounds that the run is meant to be watched rather than clicked through — but a way
   out of a twenty-second introduction is exactly what somebody who does not want to
   watch it needs. The pill in the band still waits; this one does not. */
.nav-links { transition: opacity .9s ease; }

/* The second pill, at the tail of the header's row while the menu holds its head. The
   same surface as the menu — same frosting, same border, same type — so the two read as
   a pair holding the header's two ends. A link, not a control: it goes where the
   footer's Contact goes. */
/* The two of them stand one above the other at the end of the row, ranged right so
   their ends line up on the margin. */
.nav-corner {
  /* Set down from the top of the header: her name and the rules are placed from the top
     themselves now, so the column can be moved without dragging them with it. */
  /* Set well down the header: her name now stands in the hero at the top of the same two
     fifths, and the pills belong under it rather than beside it. */
  order: 9; margin-left: auto; margin-top: 0;
  /* Stretched rather than ranged right, so both pills take the width of the wider of the
     two and the pair reads as one block instead of a staircase. The column itself is only
     as wide as its widest child, so nothing is padded out to the page. */
  /* Stood over the A of the mark below. Where that is depends on the hero's padding as
     well as the mark's width, so it is measured from the drawn image rather than worked
     out here — app.js writes --a-mid, and this falls back to the middle of the page if
     the script has not run. */
  position: absolute; left: var(--a-mid, 50%); top: var(--menu-y, 1rem);
  /* Hung from the top of the page instead of from the mark: the first item lines up
     with the name in the bar, and the list grows downward from there. */
  transform: translate(-50%, 0);
  margin: 0;
  display: flex; flex-direction: column; align-items: center; gap: .18rem;
}
/* Below this the mark is narrow enough that the A sits under the name, so the row goes
   back to the end of the bar. */
@media (max-width: 900px) {
  .nav-corner { position: static; transform: none; margin-left: auto; gap: .9rem; }
}
/* The call to action reads last, as it does on every service page. */
.nav-portfolio-wrap { order: 1; }
.nav-about-wrap { order: 2; }
.nav-contact { order: 3; margin-left: 0; }
.nav-contact, .nav-portfolio {
  display: flex; align-items: center; justify-content: center;
  padding: .6rem .95rem;
  font-family: var(--sans); font-size: .64rem; font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase; text-decoration: none;
  color: #000; opacity: .82;
  background: rgba(255, 255, 255, .7);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border: 1px solid rgba(0, 0, 0, .08);
  /* Square corners: the three read as blocks rather than pills. */
  border-radius: 0;
  pointer-events: auto;
  transition: opacity .3s ease, background-color .25s ease;
}
.nav-contact:hover, .nav-about:hover, .nav-portfolio:hover {
  opacity: 1; background: rgba(255, 255, 255, .82);
}
/* The first of the three is the larger, and carries a photograph at its left: the
   padding is drawn in on that side so the picture sits inside the pill's own curve
   rather than floating in a gap of its own. */
.nav-contact {
  justify-content: flex-start;
  gap: .7rem;
  padding: .5rem 1.6rem .5rem .5rem;
  font-size: .72rem;
}
.nav-contact-face {
  flex: none;
  width: 2.1rem; height: 2.1rem;
  border-radius: 999px;
  object-fit: cover; object-position: 50% 22%;
  display: block;
}
/* The second of the pair wears the same surface as the first, and carries a photograph
   of her under it. The wrapper is what the panel hangs from. */
/* Written by the scroll — see updateNavContrast. Full over the hero, nothing once the
   header has turned, and no transition: the value changes on every frame. Get in touch
   is not in this list, so it holds. */

/* ── The bar's controls ──────────────────────────────────────────────────────
   Two quiet text buttons and one that is plainly the thing to press. */
/* The second list, over the I. Placed by the same measurement as the first — app.js
   writes --i-mid — and set in the same face, so the two read as one system. */
.nav-social {
  position: absolute; left: var(--i-mid, 75%); top: var(--menu-y, 1rem);
  transform: translate(-50%, 0);
  display: flex; flex-direction: column; align-items: center; gap: .18rem;
  pointer-events: auto;
}
.nav-soc {
  font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-weight: 700;
  font-size: .72rem; letter-spacing: -.01em;
  color: var(--ink); text-decoration: none; line-height: 1.5;
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0 1.5px; background-position: 0 100%; background-repeat: no-repeat;
  transition: background-size .3s cubic-bezier(.2, .8, .2, 1);
}
.nav-soc:hover { background-size: 100% 1.5px; }
/* No address on record, so it draws no rule and offers no pointer. */
.nav-soc.is-unlinked { cursor: default; background-image: none; }
/* Same for a menu item with nowhere to go. */
.nav-contact.is-unlinked { cursor: default; background-image: none; }
.nav-about.is-unlinked { cursor: default; background-image: none; }
.nav-portfolio.is-unlinked { cursor: default; background-image: none; }
@media (max-width: 900px) { .nav-social { display: none; } }

/* All three in the name's own face and weight, so the row reads as one menu rather
   than as two labels and a button. */
.nav-portfolio, .nav-about, .nav-contact {
  background: none; border: 0; border-radius: 0;
  padding: .3rem 0;
  font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-weight: 700;
  font-size: .72rem; letter-spacing: -.01em; text-transform: none;
  color: var(--ink); opacity: 1;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  /* The rule is drawn from the left rather than switched on. */
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0 1.5px; background-position: 0 100%; background-repeat: no-repeat;
  transition: background-size .3s cubic-bezier(.2, .8, .2, 1);
}
.nav-portfolio:hover, .nav-about:hover, .nav-contact:hover {
  background-color: transparent; background-size: 100% 1.5px;
  color: var(--ink); opacity: 1; transform: none; box-shadow: none;
}
@media (max-width: 700px) {
  .nav-portfolio-wrap, .nav-about-wrap { display: none; }
}

.nav-portfolio-wrap, .nav-about-wrap {
  opacity: calc(1 - var(--corner-fade, 0));
  pointer-events: var(--corner-hit, auto);
}
.nav-about-wrap { position: relative; display: flex; }
.nav-about-wrap .nav-about { flex: 1; }
.nav-about-panel {
  /* Stretched between the wrapper's two edges rather than sized by the picture inside it,
     so it comes out exactly as wide as the pill it hangs from, whatever that is. */
  position: absolute; top: calc(100% + .5rem); right: 0; left: 0;
  padding: .5rem;
  background: rgba(255, 255, 255, .7);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border: 1px solid rgba(0, 0, 0, .08);
  border-radius: 1.1rem;
  animation: menu-drop .22s cubic-bezier(.2, .7, .3, 1) both;
}
/* The attribute is what closes it; without this the panel's own display would win. */
.nav-about-panel[hidden] { display: none; }
.nav-about-panel img {
  display: block; width: 100%; height: auto; border-radius: .8rem;
}
/* Portfolio hangs a pair of pictures under its pill: one large, and one smaller set
   lower and lapping over its right edge, as in the reference. The second is placed
   rather than in flow, so the panel is given the room it needs underneath. */
.nav-portfolio-wrap { position: relative; display: flex; }
.nav-portfolio-wrap .nav-portfolio { flex: 1; }
.nav-portfolio-panel {
  position: absolute; top: calc(100% + .5rem); right: 0;
  /* Cut to the pill's own width, so the panel reads as the pill continuing downward
     rather than as a card of its own. */
  width: 186px;
  /* The room under the pair is what holds the lower picture: it is placed rather than in
     flow, so the panel cannot measure it. */
  padding: .5rem .5rem 1.5rem;
  background: rgba(255, 255, 255, .7);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border: 1px solid rgba(0, 0, 0, .08);
  border-radius: 1.1rem;
  animation: menu-drop .22s cubic-bezier(.2, .7, .3, 1) both;
}
.nav-portfolio-panel[hidden] { display: none; }
.nav-portfolio-panel .pf-a {
  display: block; width: 110px; height: auto; border-radius: .45rem;
}
.nav-portfolio-panel .pf-b {
  position: absolute; right: .5rem; top: 42%;
  width: 76px; height: auto; border-radius: .45rem;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .22);
}

.nav-about {
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; text-decoration: none;
  pointer-events: auto;
}

/* ── The three rules beside her name ─────────────────────────────────────────────
   A second way into the same menu, standing next to the name in the header. Its place is
   written by app.js, which measures where the name ends; everything else about it is
   here. White, like the name it sits beside. */
.nav-burger-wrap {
  /* On the same line as the name, measured the same way. */
  position: absolute; top: 1.6rem; transform: translateY(-50%);
  z-index: 4; pointer-events: auto;
}
/* Sized against the name it stands beside: three rules, together about as tall as the
   wordmark's letters and as wide as they are tall, so the two read as one weight. */
.nav-burger {
  --bar: 2px;                 /* a rule's thickness */
  --gap: 9px;                 /* the air between two of them */
  display: block; padding: .35rem 0; margin: 0;
  background: none; border: 0; cursor: pointer;
  width: 34px;
  transform: scale(var(--burger-scale, 1));
  transform-origin: 50% 50%;
  transition: transform .22s cubic-bezier(.2, .7, .3, 1);
}
.nav-burger span {
  display: block; height: var(--bar); width: 100%;
  background: #fff; opacity: .85;
  transition: opacity .25s ease, transform .25s ease;
}
.nav-burger span + span { margin-top: var(--gap); }
.nav-burger:hover span { opacity: 1; }
/* Open, the three become two crossed, and the whole control grows by half again — the
   same button saying it is now a way out, and saying it louder. The distance each rule
   travels is the gap plus its own thickness, so they meet in the middle whatever those
   are set to. */
.nav-burger[aria-expanded="true"] { transform: scale(calc(var(--burger-scale, 1) * 1.5)); }
.nav-burger[aria-expanded="true"] span:nth-child(1) {
  transform: translateY(calc(var(--gap) + var(--bar))) rotate(45deg);
}
.nav-burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-burger[aria-expanded="true"] span:nth-child(3) {
  transform: translateY(calc((var(--gap) + var(--bar)) * -1)) rotate(-45deg);
}
@media (max-width: 780px) { .nav-burger { --bar: 1.5px; --gap: 6px; width: 24px; } }
/* Where it opens the menu, the panel hangs from it rather than from the pill. */
.nav-burger-wrap .nav-menu-panel { top: calc(100% + .55rem); right: auto; left: 0; }

/* ── The pill says one word ──────────────────────────────────────────────────────
   The destinations are behind it and come out when it is asked. The panel hangs from
   the pill's own box, so it follows wherever the pill is — the band's top right on the
   front page, the foot of the window everywhere else. */
.nav-menu-btn {
  font-family: var(--sans); font-size: .64rem; font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase;
  background: none; border: 0; padding: 0; margin: 0;
  color: #000; opacity: .82; cursor: pointer;
  transition: opacity .3s ease;
}
.nav-menu-btn:hover, .nav-links.is-open .nav-menu-btn { opacity: 1; }
/* The menu drops from the lockup and is cut to its width: the panel is placed against
   the page rather than inside the header — a fixed thing inside a transformed ancestor is
   held by that ancestor, and the three rules are transformed twice over — so app.js
   writes its left, its width and its top from the pill's own box on every frame. The
   destinations arrive one after another rather than together. */
.nav-menu-panel {
  position: fixed; z-index: 90;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: .1rem;
  padding: .7rem .9rem;
  background: rgba(255, 255, 255, .78);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid rgba(0, 0, 0, .08);
  border-radius: 1.1rem;
  white-space: nowrap;
  animation: menu-drop .26s cubic-bezier(.2, .7, .3, 1) both;
  transform-origin: 50% 0;
}
/* The attribute is what closes it; without this the panel's own display would win. */
.nav-menu-panel[hidden] { display: none; }
@keyframes menu-drop {
  from { opacity: 0; transform: translateY(-8px) scaleY(.92); }
  to   { opacity: 1; transform: none; }
}

/* Each destination on its own line, filling the width it has been given, and each a beat
   behind the one above it. */
.nav-menu-panel a {
  display: block; width: 100%;
  padding: .5rem .7rem; border-radius: 999px;
  font-family: var(--sans); font-size: .64rem; font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase; text-align: left;
  color: #000; opacity: .82; text-decoration: none;
  animation: menu-arrive .38s cubic-bezier(.2, .7, .3, 1) both;
  animation-delay: calc(.08s + var(--n, 0) * .07s);
  transition: opacity .25s ease, background-color .25s ease;
}
.nav-menu-panel a:hover { opacity: 1; background: rgba(0, 0, 0, .05); }
@keyframes menu-arrive {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: .82; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .nav-menu-panel, .nav-menu-panel a { animation: none; }
}
/* The two things to do, at the foot of the panel and side by side. Ruled off from the
   destinations above, set smaller than them, and each on its own ground so they read as
   buttons rather than as two more places to go. */
.nav-menu-actions {
  width: 100%; margin-top: .5rem; padding-top: .6rem;
  border-top: 1px solid rgba(0, 0, 0, .12);
  display: grid; grid-template-columns: 1fr 1fr; gap: .35rem;
}
.nav-menu-panel .nav-menu-actions a {
  width: auto; margin: 0;
  padding: .5rem .4rem;
  font-size: .55rem; letter-spacing: .1em; text-align: center; line-height: 1.35;
  /* The two things to do are in the house blue with white type, so they read as actions
     against the pale destinations above them rather than as two more of them. */
  background: var(--accent);
  color: #fff;
  border-radius: .7rem;
  animation-delay: calc(.08s + var(--n, 0) * .07s);
}
.nav-menu-panel .nav-menu-actions a:hover { background: #263a80; opacity: 1; }
/* The rules stand over their own menu, so they are white on it whatever the page behind
   has done to them. */
.nav-burger[aria-expanded="true"] span { background: #fff; }

.nav-toggle { display: none; }
.nav-links a {
  /* Smaller and more widely tracked than a website nav — at 13.76px caps read as a
     menu bar, at 11px with air between the letters they read as a masthead. */
  font-family: var(--sans); font-size: .64rem; font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase;
  padding: .85rem .6rem;
  display: flex; align-items: center;
  padding: 0; border-radius: 999px;
  color: #000; opacity: .82;
  transition: opacity .3s, color .25s ease, background-color .25s ease;
}
/* The pill is its own surface now — light wherever it happens to sit — so its links are
   simply black, and the rules that used to paint them to match the page behind no longer
   name them at all. */
.nav-links a.active, .nav-links a:hover { opacity: 1; }


/* ── SCROLL ZOOM (continuous, driven by JS on every scroll frame) ── */
.sr { opacity: 0; transform: translateY(26px) scale(.86); transform-origin: center bottom; }
/* Hero copy is left-aligned, so scale from the left edge — scaling from the centre
   moves each line's left edge in by a different amount (wider element = bigger shift),
   which knocks the lines out of alignment with each other. */
.hero .sr { transform-origin: left bottom; }

/* ── SECTION RHYTHM ─────────────────────────────── */
section, header.hero, footer.site-foot { position: relative; }
.kicker {
  font-family: var(--sans); font-size: 0.86rem; font-weight: 600;
  letter-spacing: -0.005em; text-transform: none; color: var(--accent);
}

/* ═════════════════════════════════════════════════
   HERO
═════════════════════════════════════════════════ */
/* `header.hero` and not `.hero`: the shared `section, header.hero, footer` rule above
   sets position: relative at higher specificity and would win otherwise. */
header.hero {
  /* Clipped left and right only. The rings wander and grow as the page is scrolled, and
     on a narrow screen the right-hand one reached past the edge — which gave the whole
     page a sideways scroll. `clip` rather than `hidden` so the pixel band at the foot
     can still hang below the hero. */
  overflow-x: clip;
  /* The 3:2 ratio existed only to fit the photograph; with no image it just made
     the hero needlessly tall. Back to a screen height. */
  /* Two 2:3 frames side by side: each column is 50vw, so 75vw of height shows both
     in full with nothing cropped. Taller than a screen at typical desktop widths. */
  /* Height and pin offset both derive from --hero-h, so a breakpoint can change the
     hero's height in one place without the two falling out of step. */
  /* Not pinned. The section goes up and off with everything else, which is the only
     way the page reads as scrolling at all — held still, the wheel turned and nothing
     on screen moved. What stays behind is the letter and the list, and they stay by
     being fixed to the screen rather than by the section refusing to leave. */
  position: relative;
  height: var(--hero-h);
  display: flex; flex-direction: column; justify-content: flex-start; align-items: center;
  /* The word sits a little lower on the page. It is held to the screen at whatever
     this padding puts it at — see holdMark in app.js — so this is still the one place
     its height is set. */
  padding: 7.4rem clamp(1.25rem, 3vw, 2.75rem) 4rem;
  /* What is left of the screen under the lettering. The mark spans the page, so its
     height follows the page's WIDTH (433 tall per 1142.875 wide), and the foot of the
     letters sits 328.26 of those units down — 28.72vw. Subtract that and the section's
     own padding from the screen's height and what remains is the room the titles have.
     Sizing them as a share of the viewport instead is what let them ride up over the
     mark on a short window. */
  --works-h: clamp(96px, calc(100svh - 4.9rem - 28.72vw - 3.2rem), 430px);
  /* No overflow clip: there's no image to contain any more, and clipping would cut
     the headline as it lags behind the scroll. */
}
/* One screen taller than the hero: that surplus is the runway the panels part over.
   The negative `top` above means the hero only pins once its bottom has arrived at
   the bottom of the screen, so the full frames are seen before anything moves. */
/* Two screens of surplus, not one: the first is the runway the copy rides up on to
   reach the header, the second is the one the panels part over. */
/* The stage is the hero and nothing more. It used to carry two extra screens of
   runway, during which the hero stayed pinned while its halves drew apart to uncover
   the gallery lying underneath. The hero now simply scrolls away and the gallery
   follows it down the page, so the runway — and the negative margin that pulled the
   gallery up under it — are both gone. */
.hero-stage {
  /* Two hundred pixels taller than it needs to be to hold its picture. Everything the
     hero does on the way past — the drawings turning into the name, the pause, the climb
     to the header — has to finish before the gallery reaches the header, and that ceiling
     is the hero's own height. This is the runway that buys the turn its slower pace. */
  /* Half of what it was: the runway the hero pins against, and with it the distance
     the drawings have to turn into the name, is now half a screen rather than a screen
     and a bit. */
  --hero-h: var(--hero-height);
  /* A screen taller than the hero. The hero pins at the top for the whole of that
     surplus, and that surplus is the runway the whole sequence is read off: the word
     fading, the O turning out to the right, the titles coming up down the left. Without
     it the page has nothing to scroll and none of it can run. */
  position: relative; z-index: 2; height: calc(var(--hero-h) + 300svh);
  /* Written by the scroll on its way out — no transition, since the value changes on
     every frame of the run. */
  opacity: var(--hero-exit-fade, 1);
}
.hero video, .hero .hero-img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  filter: none; z-index: 0;
}
/* Subject sits right of centre; the hero copy sits bottom-left. */
.hero .hero-img { object-position: 62% center; }
/* The three fifths left of the vertical rule, in plain white. It is laid over the hero's
   ground — which carries the grain and its bloom — rather than replacing it, so the two
   fifths on the other side of the line keep theirs. Above the ground at nought, below the
   carousel at two and the type at three. */
.hero-left-ground {
  position: absolute; z-index: 1; pointer-events: none;
  left: 0; top: 0; bottom: 0; width: 60%;
  background: #fff;
}

/* The mark across the whole width of the page, edge to edge, sitting just under the
   header's band. No margin: at this size the margin is what it would be fighting. Above
   the ground it stands on, below the hero's own type and the pills. */
.hero-mark {
  /* Full bleed: pulled out of the section's own side padding to the width of the page,
     and the artwork's box is cropped to the lettering — so the B and the O sit on the
     edges rather than inside a margin. */
  position: relative; z-index: 3;
  width: 100vw; margin: 0;
  display: block; line-height: 0;
}
.hero-mark img { display: block; width: 100%; height: auto; }
.hero-mark .mark-svg { display: block; width: 100%; height: auto; color: var(--ink); }

/* ── Hovering a title sends the O out ────────────────────────────────────────
   The word goes quickly and a copy of its last letter, laid exactly over the original,
   is left to travel: turned on its side, carried to the middle of the screen and taken
   up to eight times its size. A copy rather than the letter itself, because at that size
   the real one would be cut off by the artwork's own box. */
.hero-o {
  /* Fixed, and placed from where the letter sits at the top of the page — so the word
     can slide up out from under it while the letter itself stays where it was. Left,
     top and width are written from app.js. */
  position: fixed; height: auto;
  z-index: 4; pointer-events: none; opacity: 0;
  /* No transition: transform, weight and opacity are all written from the scroll on
     every frame, and a transition on top of that is a second animation lagging the
     first. */
  transition: none;
}
/* The picture sits in the opening of the O, cut to the same stadium shape as the
   counter and placed from the letter's own measurements — see app.js. It arrives once
   the letter has finished travelling, so nothing is chasing anything. */
.hero-plate {
  position: fixed; z-index: 5; pointer-events: none; cursor: pointer;
  opacity: 0; overflow: hidden;
  transition: opacity .7s ease;
}
.hero.is-plate .hero-plate {
  opacity: 1; pointer-events: auto;
  transition: opacity .55s ease .78s;
}
.hero-plate img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* A piece marked as free is not held to the opening: the ring stops being a frame and
   becomes something the artwork sits across. Nothing is cropped and nothing is filled —
   the picture keeps its own shape, so it only reads properly with a transparent ground. */
.hero-plate.is-free { overflow: visible; border-radius: 0; }
.hero-plate.is-free img {
  width: 118%; height: auto;
  max-width: none; max-height: none;
  object-fit: contain;
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
}

/* The word comes back slowly, and waits until the letter is most of the way home. */
/* The letter thins as it goes and fills back in as it comes home, arriving at the
   weight the rest of the word is drawn at — so it lands as its own letter again rather
   than as a hoop that happens to be in the right place. */
/* The weight of the letter and the presence of the word are both read off the scroll
   position — see app.js — so neither is given a transition here. What used to be a
   flight fired by a hover is now simply where you are on the page. */
.hero-o path { transition: none; }
.mark-svg { transition: none; }
/* Nothing is allowed out of the section while the letter is oversized. */
.hero { overflow: clip; }
.hero-works li { cursor: pointer; }
@media (prefers-reduced-motion: reduce) {
  .hero-o, .mark-svg, .hero-works li { transition: none; }
  /* Nothing runs, so the titles are simply there. */
  .hero-works li { opacity: 1; }
}

/* The work set on its side along the foot of the section: each title turned a quarter
   and read from the bottom up, all of them standing on one line so their ends make an
   edge. Given a fixed height so the tagline above can be placed from the same number
   rather than guessing at the longest title. */
.hero-works {
  /* A list down the left-hand side, level with the middle of the screen, with the letter
     out to the right of it. It used to be a row along the foot with every name turned on
     its side; set as a column it reads as a list of work rather than as a rule. */
  position: fixed; left: clamp(1.25rem, 3vw, 2.75rem); right: auto;
  top: 50%; bottom: auto; transform: translateY(-50%);
  z-index: 6;
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; align-items: flex-start; justify-content: center;
  gap: clamp(.2rem, .48vw, .42rem);
}
.hero-works li {
  font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-weight: 700;
  font-size: clamp(.66rem, 1vw, .95rem); line-height: 1;
  letter-spacing: -.02em; color: var(--ink);
  white-space: nowrap;
  /* The page opens on the word alone. Each title is faded up from here by the scroll,
     in its own quarter of the runway — see app.js. */
  opacity: 0;
}
@media (max-width: 700px) {
  .hero-works li { font-size: .78rem; }
}



/* ── The mark arrives ────────────────────────────────────────────────────────
   A hairline draws itself along the baseline, and the letters climb out of it one after
   another from the left — each is held below a mask cut at the foot of the lettering,
   so what is seen is the word growing up off the rule rather than sliding into place.
   The rule goes once the first letters are up. The two lists follow a beat later, so
   the word lands first and the navigation admits itself second. */
.mark-rule {
  transform-box: fill-box; transform-origin: 0 50%;
  animation: ruleDraw .62s cubic-bezier(.65, 0, .35, 1) .15s both,
             ruleGo .45s ease 1.15s forwards;
}
@keyframes ruleDraw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes ruleGo   { to { opacity: 0; } }

/* ── The letters never quite settle ──────────────────────────────────────────
   A drift of under two units, on the box inside the one the arrival animates, so it
   composes with the rise rather than being overwritten when that finishes. Each letter
   is given its own period — six numbers that share no common multiple worth speaking of
   — so the word never returns to a pose it has held before and the motion cannot be
   read as a loop. It begins once the arrival is done. */
.gr1 > g, .gr2 > g, .gr3 > g, .gr4 > g, .gr5 > g, .gr6 > g, .gr7 > g {
  animation: letterDrift 7s ease-in-out 2.4s infinite alternate;
}
.gr1 > g { animation-duration: 6.1s; }
.gr2 > g { animation-duration: 7.4s; animation-delay: 2.7s; }
.gr3 > g { animation-duration: 5.6s; animation-delay: 2.5s; }
.gr4 > g { animation-duration: 8.3s; animation-delay: 2.9s; }
.gr5 > g { animation-duration: 6.8s; animation-delay: 2.6s; }
.gr6 > g { animation-duration: 7.9s; animation-delay: 3.1s; }
.gr7 > g { animation-duration: 7.1s; animation-delay: 2.8s; }
@keyframes letterDrift {
  from { transform: translate(-0.7px, -1.8px); }
  to   { transform: translate(0.7px, 1.8px); }
}

.grise { animation: glyphRise 1s cubic-bezier(.16, 1, .3, 1) both; }
/* Far enough down to clear the mask entirely before it starts. */
@keyframes glyphRise { from { transform: translateY(460px); } to { transform: translateY(0); } }
.gr1 { animation-delay: .80s; }
.gr2 { animation-delay: .875s; }
.gr3 { animation-delay: .95s; }
.gr4 { animation-delay: 1.025s; }
.gr5 { animation-delay: 1.10s; }
.gr6 { animation-delay: 1.175s; }
.gr7 { animation-delay: 1.25s; }

.nav-tiny, .nav-place { animation: nameUp .75s cubic-bezier(.16, 1, .3, 1) 1.95s both; }
.nav-corner, .nav-social { animation: listsUp .75s cubic-bezier(.16, 1, .3, 1) 1.95s both; }
@keyframes nameUp {
  from { opacity: 0; transform: translateY(9px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes listsUp {
  from { opacity: 0; transform: translate(-50%, 9px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* For anyone who has asked not to be moved, everything is simply there. */
@media (prefers-reduced-motion: reduce) {
  .mark-rule { animation: none; opacity: 0; }
  .grise, .grise > g, .nav-tiny, .nav-place, .nav-studio, .nav-corner, .nav-social,
  .hero-works { animation: none; }
}

/* ── The hero's stack ────────────────────────────────────────────────────────
   Status, mark, what the studio does, and the two things a visitor can do about it —
   read in that order down the middle of the page. */
.hero-stack {
  position: relative; z-index: 3;
  width: 100%;
  /* Centred on the page again, and the two lists follow it — they are placed from where
     the mark actually lands rather than from a figure written down here. */
  display: flex; flex-direction: column; align-items: center;
  gap: clamp(1rem, 2.2vw, 1.9rem);
  text-align: center;
}
/* A live status line: the one thing a visitor most wants to know, and the convention
   every studio page now opens with. */
.hero-badge {
  display: inline-flex; align-items: center; gap: .55rem;
  margin: 0; padding: .42rem .9rem .42rem .72rem;
  border: 1px solid rgba(20, 20, 20, .09); border-radius: 999px;
  background: rgba(255, 255, 255, .66);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  font-family: var(--sans); font-size: .72rem; font-weight: 500;
  letter-spacing: .09em; text-transform: uppercase;
  color: rgba(23, 20, 15, .66);
}
.hero-dot {
  width: 7px; height: 7px; border-radius: 50%; background: #2FBF71;
  animation: heroPulse 2.4s ease-out infinite;
}
@keyframes heroPulse {
  0%   { box-shadow: 0 0 0 0 rgba(47, 191, 113, .5); }
  70%  { box-shadow: 0 0 0 7px rgba(47, 191, 113, 0); }
  100% { box-shadow: 0 0 0 0 rgba(47, 191, 113, 0); }
}
.hero-sub {
  margin: 0; max-width: 46ch;
  font-family: var(--sans); font-weight: 400;
  font-size: clamp(1.02rem, 1.5vw, 1.32rem); line-height: 1.5;
  letter-spacing: -.005em; color: rgba(23, 20, 15, .62);
}
.hero-cta { display: flex; flex-wrap: wrap; justify-content: center; gap: .7rem; }
.btn {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .86rem 1.5rem; border-radius: 12px;
  font-family: var(--sans); font-size: .88rem; font-weight: 500;
  letter-spacing: .005em; text-decoration: none; cursor: pointer;
  transition: transform .2s ease, background-color .2s ease,
              border-color .2s ease, box-shadow .2s ease;
}
.btn-solid {
  background: #16150F; color: #fff; border: 1px solid #16150F;
  box-shadow: 0 1px 2px rgba(20, 20, 20, .12);
}
.btn-solid:hover {
  background: #000; transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(20, 20, 20, .2);
}
/* The arrow moves rather than the whole button changing: the smaller signal reads as
   the more considered one. */
.btn-arrow { transition: transform .2s ease; }
.btn-solid:hover .btn-arrow { transform: translateX(3px); }
.btn-ghost {
  background: rgba(255, 255, 255, .6); color: var(--ink);
  border: 1px solid rgba(20, 20, 20, .13);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
}
.btn-ghost:hover {
  background: #fff; border-color: rgba(20, 20, 20, .24); transform: translateY(-2px);
}
@media (prefers-reduced-motion: reduce) {
  .hero-dot { animation: none; }
  .btn:hover, .nav-contact:hover { transform: none; }
}

@media (max-width: 780px) {
  .hero-mark { width: 100vw; }
}

/* White: the section's own ground, seen wherever the film has not painted over it and
   for the moment before it loads. */
/* One ground for the header and the hero together: a light grey that opens near
   white at the top of the page and settles a shade or two down by the hero's foot.
   The header has no ground of its own and stands over this, so the two read as a
   single field rather than as a band and a section. */
.hero {
  background:
    /* A soft bloom of white high on the left, wide enough that it has no edge of its
       own — it only lifts that part of the field rather than sitting on it. */
    radial-gradient(128% 74% at 32% 12%, rgba(255, 255, 255, 1) 0%,
                    rgba(255, 255, 255, .72) 40%, rgba(255, 255, 255, .3) 66%,
                    rgba(255, 255, 255, 0) 86%),
    /* And the field itself, opening on true white at the top of the page. The stops are
       close together so the fall stays smooth rather than banding across this width. */
    /* Grey is a band through the middle now rather than the end of the fall: the field
       opens white, deepens behind the mark, and comes back to white under the titles so
       they sit on clean paper. */
    linear-gradient(180deg, #ffffff 0%, #ffffff 14%, #f7f7f7 30%,
                    #ebebeb 44%, #f4f4f4 58%, #fcfcfc 66%, #ffffff 72%, #ffffff 100%);
}
/* Two frames, equal halves, filling the hero behind the copy. */
.hero-bg {
  /* Not clipped: the film hangs below the hero's foot and this box is its parent — a
     hidden overflow here cut it off at exactly the edge it is meant to cross. The film
     clips its own player, and the black over it is inset to this box, so nothing else
     can spill. */
  position: absolute; inset: 0; z-index: 0; overflow: hidden;
  display: block;
}
/* Nothing over the picture while the hero is being read — it is shown as it is. The
   layer stays for the approach alone: --film-sink runs to one as the statement comes up
   and takes the wash from nothing to four fifths black, so the picture sinks away rather
   than being scrolled off. Above the picture, below everything else in the hero. */
.hero-bg::before {
  content: ''; position: absolute; inset: 0; z-index: 1;
  background: rgb(0 0 0 / calc(var(--film-sink, 0) * 80%));
  pointer-events: none;
}
/* An iframe has no object-fit, so the cover is done by hand: the player is held at
   sixteen by nine and told to be at least as wide and at least as tall as the hero,
   which leaves one axis overflowing and the other flush. Centred on both, so whatever
   overflows is trimmed evenly from the two ends rather than all off one. */
.hero-film {
  position: absolute; top: 50%; left: 50%;
  /* The centring and the drift ride the same transform: --film-drift is written by the
     scroll and holds the picture back as the hero leaves, so the film travels at a
     fraction of the rate the copy does and the section has some depth on the way out. */
  transform: translate(-50%, calc(-50% + var(--film-drift, 0px)));
  /* An iframe again: it has no object-fit, so the covering is done by the sizing below —
     sixteen by nine, at least as wide and as tall as the hero, centred on both. */
  width: auto; height: auto;
  /* A fifth taller than the box it fills, which is the room the drift travels in: the
     film moves down as the hero moves up, and without the slack a strip of ground would
     open along the hero's top edge on the way out. */
  min-width: 100%; min-height: 120%;
  aspect-ratio: 16 / 9;
  border: 0; pointer-events: none;
}

/* Stacked at phone widths rather than side by side: two half-width columns on a
   375px screen would leave each frame too narrow to read. The pair still parts —
   app.js swaps the slide to the vertical axis below this breakpoint, so the top
   frame leaves upward and the bottom one downward. */
@media (max-width: 780px) {
  /* Handed to the compositor. Both of these are painted once and then only moved — the
     ground is a gradient under a tile of grain, the drawings are masks being rotated —
     and without a layer of their own the phone redraws them on every scrolled frame. */
  .hero-bg, .hero-camera { will-change: transform; }
  /* No layout containment on the cards. It saved a little measuring, but layout
     containment forces `transform-style` to compute to flat, and these cards are two
     faces turning about their own middle — flattened, the face that should be showing
     can be culled as a backface, which is how the word disappeared here entirely. */
  /* Stacked, not side by side: 30% of a 375px screen is too narrow to read as a
     gradient, so the frame takes the top of the hero and the ground takes the rest. */
  /* The same ground as the wide screen gets: black at the left, running to the accent
     at the right. It is declared once, at the top of the file, and this override — which
     used to flatten it to black here — is gone rather than repeated with the same value,
     so the two can never drift apart. */
}
/* Sits near the base of the hero now. */
/* The four views of the sheet, taken apart and restacked: one large, the rest stepping
   down in size and lapping over each other's corners rather than sitting in the tidy
   2x2 the drawing came in. Each view is its own crop, carried as a mask so the colour
   stays in CSS, and each is sized as a percentage of the frame so the whole arrangement
   scales as one. Sits well back in the blue end of the hero gradient, clear of the
   photograph on the left half and above the title. */
/* The mark in the header is not gone, it is waiting — see below. */
/* The mark is not gone from the header, it is waiting — and it waits for the white
   itself, not for the pixels arriving. The ground now begins filling in a good way
   before the gallery reaches the header; the mark holds back until it is actually on
   white, then comes up on its own short fade. Out of the tab order until it is there. */
.nav-brand.is-hidden {
  opacity: 0; pointer-events: none;
  transition: opacity .3s ease;
}
nav.on-light .nav-brand.is-hidden { opacity: var(--blog-fade, 1); pointer-events: auto; }


.hero-camera {
  /* Above the copy, which sits at 2. It fills the hero exactly as the background wrapper
     does, so moving it out of that wrapper changed where it paints and nothing else. */
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  /* The word lands at the size the header wears its own: same height, and the width that
     follows from the logotype's proportion. Declared on the frame rather than on the
     cards so that app.js — which reads it from here to work out where the word's middle
     is — sees the same value the cards do. On the cards it was invisible from here, and
     the fallback quietly put the phone's word fifteen pixels below its slot. */
  --word-h: 3.2rem;
  --word-w: calc(var(--word-h) * 7.4286);
  --card-w: calc(var(--word-w) / 4);
  /* Anchored to the photograph's edge rather than the window's, so the overlap is a
     fixed amount rather than whatever is left over at a given width. One number sets it
     for both layouts: here the picture ends at the halfway line, and on a phone it ends
     at 60% of the hero's height, so the same figure reads as the same lap either way.
     This element comes after the <img> at the same level, so the line work crosses in
     front of it. */
  /* Sized to the band it has to live in. The hero is a quarter of what it was, and the
     drawing at its old size stood nearly a thousand pixels tall — three quarters of it
     hanging over the white gallery below, in white, which is to say invisible. */
  /* Half the frame's own width, so the drawing stays centred as it grows. */
  /* Height first: the frame has to fit the band, and the band is a fraction of the
     screen that changes. Its width follows from the drawing's own proportion, and the
     lap — half that width — is what keeps it centred. */
  height: 92%; width: auto; aspect-ratio: 1 / 0.78;
  --bp-lap: calc(92% * 1.282 / 2);
  left: calc(50% - min(33vw, 420px)); top: 3%;
  /* Fades on the same variable as the gradient and its grain, so the drawing leaves
     with the ground it is drawn on as the hero parts, rather than on a count of its
     own. Set on the frame, so it multiplies the depth already in the pieces. */
  opacity: var(--hero-ground, 1);
}
/* Each drawing is a card with two faces: the elevation on the front, its quarter of the
   logotype on the back. As the page scrolls the four travel out of their scattered
   arrangement into one row, turning on their own axes as they go, and what lands is the
   name — the drawings do not fade out and a word fade in, they turn into it.
   Everything between the two arrangements is interpolated from one number, so there is
   a single measure to change: `--bp-p`, written by app.js.
   Heights are given outright rather than by aspect ratio, because the shape itself has
   to change: each card is the proportion of its drawing at the start and the proportion
   of a quarter of the word at the end, and a ratio cannot be interpolated. They read as
   a share of the frame's height, which is 0.78 of its width — hence the divisions. */
.bp {
  position: absolute;
  transform-style: preserve-3d;
  /* Each card starts a little after the one before, so the four arrive as a run. */
  --local: clamp(0, calc((var(--bp-p, 0) - var(--start)) / 0.55), 1);
  left:   calc(var(--x0) + (var(--x1) - var(--x0)) * var(--local));
  top:    calc(var(--y0) + (var(--y1) - var(--y0)) * var(--local));
  width:  calc(var(--w0) + (var(--w1) - var(--w0)) * var(--local));
  height: calc(var(--h0) + (var(--h1) - var(--h0)) * var(--local));
  /* Perspective per card, so each turns about its own centre rather than being skewed
     by its distance from a vanishing point the whole row would share.
     `--local * (1 - --local) * 4` is a curve that is nothing at both ends and one in the
     middle: it lets the card lift toward the viewer and roll off true on the way across
     without disturbing either the arrangement it leaves or the row it lands in. */
  --arc: calc(var(--local) * (1 - var(--local)) * 4);
  transform: perspective(900px)
             translateZ(calc(var(--arc) * 140px))
             rotateZ(calc(var(--arc) * var(--tilt)))
             rotateY(calc(var(--local) * 180deg));
  /* The line work is bright white in its own right now, not a faint under-drawing: it
     starts nearly at full strength and reaches full as the name forms. */
  opacity: calc(var(--o0) + (1 - var(--o0)) * var(--local));
}
.bp::before, .bp::after {
  content: ''; position: absolute; inset: 0;
  background: #fff;
  -webkit-backface-visibility: hidden; backface-visibility: hidden;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
}
/* The drawing, stretched to whatever shape the card currently holds. */
.bp::before { -webkit-mask-image: var(--face); mask-image: var(--face); }
/* The word, one quarter per card, so the four together spell it out. Pre-turned, so it
   reads the right way round once the card has come about. */
.bp::after {
  -webkit-mask-image: url("wordmark.png"); mask-image: url("wordmark.png");
  -webkit-mask-size: 400% 100%; mask-size: 400% 100%;
  -webkit-mask-position: var(--slice) center; mask-position: var(--slice) center;
  transform: rotateY(180deg);
}
/*            scattered start                              row that spells the name      */
.bp-chamber { --start: 0;   --x0:  0%; --y0: 28%; --w0: 62%; --h0: 53.1%;
              --x1: calc(50% - var(--word-w) / 2 + var(--word-dx, 0px)); --y1: calc(50% - var(--word-h) / 2 + var(--word-dy, 0px)); --w1: var(--card-w); --h1: var(--word-h); --slice: 0%;
              --face: url("bp-chamber.svg"); --o0: .95; --tilt: -9deg; }
.bp-back    { --start: .15; --x0: 46%; --y0:  0%; --w0: 46%; --h0: 43.4%;
              --x1: calc(50% - var(--word-w) / 2 + 1 * var(--card-w) + var(--word-dx, 0px)); --y1: calc(50% - var(--word-h) / 2 + var(--word-dy, 0px)); --w1: var(--card-w); --h1: var(--word-h); --slice: 33.333%;
              --face: url("bp-back.svg"); --o0: .95; --tilt: 7deg; }
.bp-plan    { --start: .30; --x0: 34%; --y0: 70%; --w0: 38%; --h0: 28.6%;
              --x1: calc(50% - var(--word-w) / 2 + 2 * var(--card-w) + var(--word-dx, 0px)); --y1: calc(50% - var(--word-h) / 2 + var(--word-dy, 0px)); --w1: var(--card-w); --h1: var(--word-h); --slice: 66.667%;
              --face: url("bp-plan.svg"); --o0: .95; --tilt: -5deg; }
.bp-front   { --start: .45; --x0: 70%; --y0: 59%; --w0: 28%; --h0: 18.7%;
              --x1: calc(50% - var(--word-w) / 2 + 3 * var(--card-w) + var(--word-dx, 0px)); --y1: calc(50% - var(--word-h) / 2 + var(--word-dy, 0px)); --w1: var(--card-w); --h1: var(--word-h); --slice: 100%;
              --face: url("bp-front.svg"); --o0: .95; --tilt: 11deg; }
/* On a phone it lies across the gradient below the photograph, where it finally has
   width to be read as a drawing. The top edge is pinned to where the picture ends so
   the line work never crosses the photograph — this element paints after the image and
   would otherwise sit on top of it — and the copy, a layer above, reads over it. */
@media (max-width: 780px) {
  .hero-camera {
    /* Width-driven and centred here, for the same reason as the marks above: at 74% of
       a tall hero the frame was over five hundred pixels across on a 375px screen. */
    /* Centred by left: 50% and the translate app.js restates on every frame — the frame
       must keep both, or the drawing walks half a screen to the left. */
    /* Width-driven, but bounded by the height as well: on a short, wide window — a
       phone on its side — a width of 112vw stood half the drawing below the hero's
       foot. Whichever of the two is smaller wins. */
    height: auto; width: min(112vw, 110vh);
    --bp-lap: calc(min(112vw, 110vh) / 2);
    left: 50%; top: 4%; transform: translateX(-50%);
  }
  /* The header wears its mark at 1.4rem here, so the word lands at 1.4rem here. */
  .hero-camera {
    --word-h: 1.4rem;
  }
  /* All four are here now. The word is cut into four, so four cards have to carry it —
     hiding the smallest two left the phone with half a name. They start in the same
     scattered arrangement as the desktop, which is written in percentages of the frame
     and so needs nothing restated here. */
}

.hero-inner {
  /* Set against the fold rather than against the hero's foot. The hero sits its copy at
     the bottom, so a taller hero used to push the copy further out of sight — the
     headline already ran 72px past the fold before this. Measured from the fold, the
     runway above can grow without the copy moving at all. */
  position: relative; z-index: 2; padding: 0 3rem calc(var(--hero-h) - 100svh + 1.2rem);
  color: #fff; will-change: transform;
}
/* Entrance lives on the title, not .hero-inner — the scroll rise writes an inline
   transform there every frame and would overwrite a keyframe animation. */
/* No entrance animation. It filled at opacity 1 for the life of the page, and a filling
   animation outranks an inline style — the scroll-driven brightness below could never
   have taken effect while it was there. The scroll now does the entrance's job. */


.hero-title {
  font-family: var(--sans);
  text-transform: uppercase;
  font-weight: 800;
  font-size: clamp(2rem, 7vw, 8rem);
  line-height: 0.86;
  letter-spacing: -0.035em;
  /* It grows as the page scrolls; from the left edge, since it is set left-aligned and
     scaling from the middle would walk it sideways. */
  transform-origin: left center;
  max-width: 14ch;
}
.hero-title em { font-family: var(--serif); font-style: italic; font-weight: 400; text-transform: none; }
/* ── MARQUEE ─────────────────────────────────────── */
.marquee {
  background: var(--ink); color: var(--cream);
  padding: 1.1rem 0; overflow: hidden; white-space: nowrap;
  border-bottom: 1px solid var(--rule-lt);
}
.marquee-track { display: inline-flex; animation: scroll-left 34s linear infinite; }
.marquee-track span {
  font-family: var(--sans); font-size: 0.78rem; font-weight: 500;
  letter-spacing: .1em; text-transform: uppercase; padding: 0 2.4rem;
  display: inline-flex; align-items: center; gap: 2.4rem;
}
.marquee-track span::after { content: '—'; opacity: .4; }
@keyframes scroll-left { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* ── INTRO STRIP ─────────────────────────────────── */
.intro-strip {
  display: grid; grid-template-columns: repeat(3, 1fr);
  border-bottom: 1px solid var(--rule);
}
.intro-cell { padding: 3.2rem 3rem; border-right: 1px solid var(--rule); }
.intro-cell:last-child { border-right: none; }
.intro-label { font-family: var(--sans); font-size: .55rem; letter-spacing: .24em; text-transform: uppercase; color: var(--accent); margin-bottom: 1rem; }
.intro-body { font-size: 1.05rem; }
.intro-body a { border-bottom: 1px solid currentColor; }

/* ═════════════════════════════════════════════════
   WORKS — pinned horizontal-scroll gallery
═════════════════════════════════════════════════ */
/* Pulled up by two screens so its first frame is already pinned and full-bleed behind
   the hero by the time the panels start to part — otherwise the gap would open onto
   empty page. The scroll length that costs is handed back as a hold at the top of the
   gallery in app.js, so the horizontal run doesn't begin until the curtain is open. */
/* Pulled up under the hero by the hero's own height, so its sticky window is already
   pinned to the top of the screen on arrival. Without this the window began below the
   hero and the pictures — which stand on its floor — sat a screen further down, leaving
   a tall empty white band under the hero on the first screen. The hero paints over it
   at z-index 2, and the pixel bleed at the hero's foot is what joins the two. */
#works {
  background: #fff; color: var(--ink); position: relative; z-index: 1;
  margin-top: calc(-1 * var(--hero-height));
}
/* The horizontal gallery is out of the page for now. Its markup and its script are
   untouched — this is the only line standing between it and coming back. */
#works { display: none; }
.metallic {
  background: linear-gradient(115deg, #6b655c 0%, #d8d2c6 18%, #f8f6f1 32%, #9b9488 45%, #4d4740 55%, #d8d2c6 70%, #f8f6f1 85%, #6b655c 100%);
  background-size: 250% auto;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  animation: metallic-shimmer 28s linear infinite;
}
@keyframes metallic-shimmer { to { background-position: -250% center; } }
/* currentColor rather than a fixed value, so the rule is black on the white Works
   section and near-white on the dark Contact section without needing an override. */
.accent-rule { width: 56px; height: 3px; margin-top: 1.8rem; background: currentColor; }
.works-pin-spacer { position: relative; }
.works-viewport {
  position: sticky; top: 0; height: 100vh; height: 100svh; overflow: hidden;
  /* The pictures stand on the floor of the window rather than floating in the middle
     of it, so the row reads as a shelf. The padding is what they stand on. */
  display: flex; align-items: flex-end;
  padding-top: 8.8rem; padding-bottom: 4.5rem;
}
/* Sits inside the pinned window so it stays put while the frames run past beneath it,
   and clears the fixed header. Same face and size as the category row on the grid. */
@media (max-width: 780px) {
  /* Less air around the pictures here: a phone has none to spare, and the header is
     shorter than it is on a wide screen. */
  /* Room under the second row for the menu pill, which floats above the bottom of the
     screen — at 1.2rem the lower row's captions ran under it. */
  .works-viewport { padding-top: 4.4rem; padding-bottom: 4.6rem; }
}
.works-track { display: flex; align-items: flex-end; will-change: transform; padding-left: 3rem; }

/* The card is the frame plus its caption beneath, so it no longer clips: the picture
   is clipped by .wc-frame and the words sit outside it, in the margin. */
.work-card {
  /* The arrival: up from thirty pixels and out of nothing, on a number the scroll
     writes. No transition — the value changes every frame, and easing it would leave
     the pictures trailing the page. */
  opacity: var(--cin, 1);
  transform: translateY(calc((1 - var(--cin, 1)) * 30px));
  position: relative; flex: 0 0 auto;
  width: auto; margin-right: 1.75rem;
  display: flex; flex-direction: column;
  background: transparent; color: var(--ink);
}
@property --hov { syntax: '<number>'; initial-value: 1; inherits: false; }
/* Two scales on one picture: --cw, which the carousel writes from where the card is
   across the screen, and --hov, which the pointer sets. Multiplied rather than fought
   over. Only --hov is eased; --cw is driven by scroll and must land on the frame it was
   measured for. */
.wc-frame {
  transform: scale(calc(var(--cw, 1) * var(--hov, 1)));
  /* Scaled from the foot, so a small picture keeps its bottom edge on the same line as
     a large one instead of shrinking away from it. */
  transform-origin: 50% 100%;
  transition: --hov .5s cubic-bezier(.16, 1, .3, 1);
}

/* 2:3, matching the source files, so object-fit crops nothing. Driven by height —
   width follows from the ratio — and capped by width so it can't overflow a short
   or narrow window. */
.wc-frame {
  position: relative; flex: 0 0 auto;
  height: min(60vh, 92vw); width: auto; aspect-ratio: 2 / 3;
  border-radius: 2px; overflow: hidden; background: #fff;
}
/* The caption used to sit on the photograph, in cream over a scrim. On the white page
   it reads in ink like the grid's captions do. */
.work-card.has-media { color: var(--ink); }
.work-card video, .work-card img, .work-card .wc-fallback {
  /* Wider than the frame and pulled left, which is the room the parallax travels in:
     the picture slides the other way to the strip, so the pictures read as sitting
     behind their frames rather than painted on them. */
  position: absolute; inset: 0; width: 114%; height: 100%; left: -7%; object-fit: cover;
  transform: translateX(var(--wcp, 0px));
}
.work-card .wc-fallback {
  background:
    repeating-linear-gradient(135deg, rgba(245,242,237,.05) 0 2px, transparent 2px 14px),
    linear-gradient(160deg, #2a251c, #17140f 60%);
}
.wc-body { position: relative; z-index: 1; margin-top: .95rem; }
.wc-num { font-family: var(--sans); font-size: .62rem; letter-spacing: .2em; color: var(--accent); }
.wc-title { font-size: clamp(1.5rem, 2.6vw, 2.1rem); margin: .5rem 0 .4rem; }
.wc-title em { font-style: italic; }
.wc-meta { font-family: var(--sans); font-size: .78rem; letter-spacing: 0; text-transform: none; color: rgba(23, 20, 15, .5); }
.wc-desc { font-family: var(--serif); font-style: italic; font-size: .95rem; max-width: 40ch; margin-top: .9rem; opacity: .9; }

/* ═════════════════════════════════════════════════
   STATEMENT
═════════════════════════════════════════════════ */
/* ── NEW SECTION (empty) ─────────────────────────── */
/* White to continue from the Works section above. min-height keeps it visible while
   empty; drop that once there's content and it can size to what's inside. */
/* Full-bleed: the pan window runs edge to edge. */
/* ── Where she works ─────────────────────────────────────────────────────────────
   Under the rule, at the left-hand end — her trade holds the right, above it. Small and
   tracked, in the same manner as the cue in the corner, so it reads as a caption rather
   than as a line of copy. */
.hero-place {
  position: absolute; left: 3rem; bottom: calc(8% - 1.35rem); z-index: 3;
  margin: 0; pointer-events: none;
  font-family: var(--sans); font-size: .62rem; font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase;
  color: rgba(255, 255, 255, .88);
}
@media (max-width: 780px) {
  .hero-place { left: 1.6rem; bottom: calc(7% - 1.15rem); font-size: .56rem; }
}

/* Where to find her, set under the rule at the right-hand end — the city holds the left.
   The same small tracked caps, so the two ends of the line match. */
/* The word set on its side at the hero's right edge, reading top to bottom. It stands
   on the same margin as the rule above the foot, and being inside the hero it fades out
   with it rather than keeping a count of its own. */
.hero-scroll {
  position: absolute; right: 3rem; top: 50%;
  transform: translateY(-50%);
  z-index: 3; margin: 0;
  writing-mode: vertical-rl; text-orientation: mixed;
  /* Bold on a wide screen; the phone keeps the lighter weight, where the word is smaller
     and heavy type at that size closes up. */
  font-family: var(--sans); font-size: .62rem; font-weight: 700;
  letter-spacing: .3em; text-transform: uppercase;
  /* The blue the ring and the drawings are in, lifted enough to hold against the film. */
  color: #4f74d8;
  pointer-events: none;
}
@media (max-width: 780px) {
  .hero-scroll { right: 1.6rem; font-size: .56rem; letter-spacing: .24em; font-weight: 500; }
}

.hero-social {
  position: absolute; right: 3rem; bottom: calc(8% - 1.35rem); z-index: 3;
  margin: 0; display: flex; gap: 1.4rem;
  font-family: var(--sans); font-size: .62rem; font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase;
  color: rgba(255, 255, 255, .88);
}
/* The line along the hero's foot leaves to the left as the statement below comes up, and
   is gone by the moment "What I Do" is cued — the two never share the screen. Both ends
   of the line travel together, so it reads as one row being drawn off rather than two
   pieces of type going their own ways. Set from updateApproach in app.js. */
.hero-place, .hero-social {
  transform: translateX(calc(var(--furniture-out, 0) * -26vw));
  opacity: calc(1 - var(--furniture-out, 0));
}
/* Nothing invisible should catch a click. */
.hero-social { pointer-events: var(--furniture-hit, auto); }
@media (prefers-reduced-motion: reduce) {
  .hero-place, .hero-social { transform: none; }
}

/* A short upright rule opening the row, between the place at the far left of the line
   and the words that follow here. One pixel wide and about as tall as the type it stands
   beside. */
.hero-tick {
  flex: none; width: 1px; height: 1.1em;
  background: rgba(255, 255, 255, .55);
  margin-right: .2rem;
}
/* Standing at the head of the row, heavier than what follows it, and set a little
   apart so it reads as the label for the two links rather than as one of them. */
.hero-social a { color: inherit; text-decoration: none; opacity: 1; transition: opacity .25s ease; }
.hero-social a:hover { opacity: .65; }
@media (max-width: 780px) {
  .hero-social { right: 1.6rem; bottom: calc(7% - 1.15rem); gap: 1rem; font-size: .56rem; }
}

/* ── Her trade, standing in the hero ──────────────────────────────────────
   Set heavy and upper case in the page's own grotesque, two lines, ranged right and
   standing on the rule near the hero's foot. It belongs to the hero now, so it travels
   and fades with it rather than staying pinned to the window. It takes its colour from the same
   number the header does — white while the hero is black behind it, dark once the page
   has turned white — so it is legible over both without a rule of its own. Inert: it is
   a statement, not a control, and the gallery behind it stays reachable. */
.corner-role {
  /* In the hero rather than pinned to the window, and set to sit directly above the
     rule — the same margin as the rule at one end, and clear of it by a little at the
     foot, so the two read as one block. */
  /* Set to the right of the vertical rule rather than on the page's left margin: it
     begins where the rule stands and runs into the two fifths beside it. */
  position: absolute; left: calc(60% + 1.4rem); right: auto; bottom: calc(8% + .7rem); z-index: 3;
  margin: 0; pointer-events: none;
  /* Ranged left, so the two lines start flush on the margin — the same one the rule and
     the city below it are set to. */
  display: flex; flex-direction: column; align-items: flex-start; text-align: left;
  font-family: var(--sans);
  /* Smaller than it was on the wide side of the page: it now has two fifths to stand in
     rather than the whole width, and it still grows by half again as the page goes. At
     the old size the last of that growth ran off the edge. */
  font-weight: 800; font-size: clamp(1.1rem, 2.5vw, 2.35rem);
  line-height: .92; letter-spacing: -.015em; text-transform: uppercase;
  color: var(--ink); opacity: .9;
  /* Largest at the top of the page and smaller the further down you go, so scrolling
     back up grows it again. Sized from its own corner, so it opens out into the hero
     rather than off the left margin. */
  transform: scale(var(--role-scale, 1));
  transform-origin: 0 100%;
  will-change: transform;
}
/* Black throughout, rather than white over the hero and turning as the page goes: it
   stands on the section's own ground now, not on a photograph. */
@media (max-width: 780px) {
  /* Back on the left margin on a phone: the ground right of the rule is 40% of a
     small screen, which is not a column anything can be set in. */
  .corner-role { left: 1.6rem; right: auto; bottom: calc(7% + .5rem); font-size: clamp(1.1rem, 7vw, 1.9rem); }
}

#section-two {
  /* A band of white above the filter row, so the section starts lower and the pixels
     finishing above it have somewhere to land. */
  background: #fff; color: var(--ink); padding: 9rem 0 0; overflow: hidden;
  opacity: 0; transition: opacity .85s ease;
}
#section-two.is-in { opacity: 1; }
/* No JS: never hide it. */
@media (prefers-reduced-motion: reduce) { #section-two { opacity: 1; transition: none; } }
.section-two-inner { max-width: none; margin: 0; }
/* Promoted only while the slow stretch is actually carrying it. A permanent layer this
   tall is more than some devices will hold, but repainting five thousand pixels on each
   frame of the stretch is worse — so it is a layer for the stretch and nothing after. */
.section-two-inner.is-shifting { will-change: transform; }
/* Flows down the page rather than sideways: no pinned window, no pointer pan, no
   horizontal travel — the grid is simply as tall as it needs to be and you scroll it. */
.s2-scroll {
  position: relative;
  /* Inset so tiles never sit flush against the window edge. */
  padding: max(1.5rem, 1.6vw);
}
/* The grid is wider than the screen and slides sideways as the page scrolls down, so
   the two motions run together — see updateGridDrift. The window clips the overhang;
   with its height left to the content, `hidden` costs nothing vertically. */
.s2-pan { overflow: hidden; }
/* Full width, and no promoted layer: nothing moves it any more. */
.s2-pan .s2-grid { width: 100%; }
/* The list is one full-width column, so it has no overhang to travel across. */
.s2-pan .s2-grid.is-list { width: 100%; }
/* Twelve columns the layout is authored against, not a fixed tile size. Each frame
   declares where it starts, how wide it is and its ratio, so the arrangement is
   deliberately uneven — that asymmetry, and the large row gap, is the whole point. */
.s2-grid {
  position: relative;
  display: grid; grid-template-columns: repeat(24, 1fr);
  /* Finer track and a tighter gutter, so a frame can be half the width it was. */
  column-gap: max(0.9rem, 1.1vw);
  row-gap: max(3.6rem, 4.8vw);
  /* One step of vertical stagger. Frames drop by a multiple of it, so nothing in a
     row shares a top edge — the grid still places them, it just doesn't line them up. */
  --s2-step: max(1.4rem, 1.8vw);
  align-items: start;
}
.s2-item {
  margin: 0; grid-column: var(--col, auto) / span var(--span, 4);
  margin-top: calc(var(--offset, 0) * var(--s2-step));
}
/* One frame given the screen. */
.s2-item.is-full { grid-column: 1 / -1; margin-top: 0; }
.s2-item.is-full .s2-mask { aspect-ratio: auto; height: 68svh; }
/* Ratio is per frame now — portrait, landscape or square, as the picture wants. */
.s2-mask {
  position: relative; aspect-ratio: var(--ratio, 2 / 3); overflow: hidden;
  background: #fff;
}
.s2-shift { position: absolute; inset: 0; }
/* Wraps the mask so the hover zoom on the image inside can be eased on its own. */
.s2-move { position: relative; }
.s2-mask img {
  /* Taller than its window and lifted, so there is somewhere for it to travel: the
     picture slides against the page as its frame crosses the screen and the frame
     clips what runs off. --s2p is written by the scroll, --s2s by the hover. */
  width: 100%; height: 116%; top: -8%; position: absolute; object-fit: cover; display: block;
  transform: translateY(var(--s2p, 0px)) scale(var(--s2s, 1));
  transition: transform .5s cubic-bezier(.25, 1, .5, 1);
}
/* The scroll writes the drift every frame; easing it would leave the picture trailing
   seconds behind the page. Only the hover's zoom is eased, and only while hovering. */
.s2-mask img { transition: none; }
@media (hover: hover) and (pointer: fine) {
  .s2-item:hover .s2-mask img { transition: transform .5s cubic-bezier(.25, 1, .5, 1); }
}
/* Zoom the frame under the cursor. The mask clips, so the image grows inside a fixed
   window rather than pushing its neighbours around. 1.03 rather than the 1.16 this
   used to be: at editorial scale the move should be felt, not watched. */
@media (hover: hover) and (pointer: fine) {
  .s2-item:hover .s2-mask img { --s2s: 1.03; }
}
/* Caption sits in the margin beneath its frame and stays there — no scrim, no hover.
   A permanent marginal note reads as editorial; an overlay reads as a thumbnail. */
.s2-info { margin-top: .95rem; }
.s2-info span {
  display: inline-block;
  font-family: var(--sans); font-size: .78rem; font-weight: 500;
  letter-spacing: -0.005em; text-transform: none;
  color: rgba(23, 20, 15, .5);
  transition: color .3s ease, opacity .5s cubic-bezier(.25, 1, .5, 1),
              transform .5s cubic-bezier(.25, 1, .5, 1);
}
/* The caption rises the last few pixels into place on hover. Only where there is a
   pointer to hover with — on a touch screen it has to be legible at rest, so it stays
   where it is, fully opaque. */
@media (hover: hover) and (pointer: fine) {
  .s2-info span { opacity: .7; transform: translateY(8px); }
  .s2-item:hover .s2-info span { color: var(--ink); opacity: 1; transform: translateY(0); }
  /* Same treatment on the shop's cards. */
  .print .print-frame { overflow: hidden; }
  .print .print-frame img { transition: transform .5s cubic-bezier(.25, 1, .5, 1); }
  .print:hover .print-frame img { transform: scale(1.03); }
  .print .print-title, .print .print-price {
    transition: opacity .5s cubic-bezier(.25, 1, .5, 1), transform .5s cubic-bezier(.25, 1, .5, 1);
    opacity: .7; transform: translateY(8px);
  }
  .print:hover .print-title, .print:hover .print-price { opacity: 1; transform: translateY(0); }
}

/* ── Grid / list switch ─────────────────────────── */
/* Categories above the view switch — same centred column, a step larger, with a
   little more air beneath them than between them and the grid. */
/* One row on a highlighted band. inline-flex so the blue hugs the words rather than
   stretching across the whole section; the slashes are their own elements so they sit
   outside the buttons and can't be clicked into a filter. */
.s2-filters {
  /* Ranged left rather than centred: the band starts on the section's own margin, the
     same one the grid below it is set to. */
  /* A block of its own width rather than an inline one: auto margins only centre a
     block-level box, and the inline-flex it used to be simply sat where the line put it. */
  display: flex; flex-direction: row; align-items: baseline;
  flex-wrap: wrap; justify-content: flex-start;
  /* A dock at the foot of the window rather than a line in the page: it comes up as the
     gallery arrives and stays under the thumb for as long as the gallery is what you are
     looking at. Its own width, centred on the window. */
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(2rem + env(safe-area-inset-bottom, 0px));
  z-index: 90;
  /* The bar is carried at seven tenths, so the rows behind it used to read as colliding
     with the words rather than passing under them. Blurring its ground keeps it as light
     as it was while making what is behind it plainly behind it. */
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  gap: .1em .45em; margin: 0; width: max-content;
  /* Written by the scroll — see app.js. Full strength by default, so a page whose script
     never runs still shows its categories. */
  opacity: var(--filters-in, 1);
  pointer-events: var(--filters-hit, auto);
  /* A pill rather than a band: the same rounded box the header wears, in the drawing's
     blue, with enough padding either end that the round reads as a shape rather than as
     clipped corners. The blue is carried at seven tenths with a blur behind it, so the
     posts show through it the way the page shows through the header's own pills. */
  background: rgba(48, 69, 153, .7);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  padding: .3em 1.1em .38em;
  border-radius: 999px;
}
/* inline-flex sits in a line box, so where the line starts is where the band starts. */
.s2-scroll { text-align: left; }
.s2-grid, .s2-item, .s2-info { text-align: left; }
.s2-filter-sep {
  font-family: var(--sans); font-weight: 500;
  font-size: .95rem; line-height: 1; color: rgba(255, 255, 255, .45);
  user-select: none;
}
/* Set exactly as the grid/list control below it: same family, same size, same weight,
   same tracking — the two menus read as one system rather than a heading and a control. */
.s2-filter {
  background: none; border: 0; padding: .3rem .55rem; border-radius: 999px; cursor: pointer;
  font-family: var(--sans); font-size: .95rem; font-weight: 500;
  letter-spacing: -0.005em; text-transform: none; text-align: left; line-height: 1.14;
  color: #fff;
  transition: color .3s ease;
}
/* Plain white type on the blue bar. The chosen one turns the bar inside out — white
   ground, blue word — which reads at a glance from across the page in a way a two pixel
   rule under one word in a row of them never did. */
.s2-filter.is-on {
  background: #fff; color: var(--accent);
  transition: background-color .3s ease, color .3s ease;
}
@media (max-width: 780px) {
  .s2-filters { margin: 0; gap: .1em .3em; padding: .28em .9em .34em;
                bottom: calc(1.2rem + env(safe-area-inset-bottom, 0px)); }
  .s2-filter { font-size: .95rem; padding: .12rem .5rem; text-align: left; }
  .s2-filter-sep { font-size: .9rem; }
}

/* ── List view ──────────────────────────────────── */
/* One row per photograph: thumbnail, then the label beside it. The wide tiles stop
   spanning here, but each thumbnail keeps its own ratio — a portrait stays portrait. */
.s2-grid.is-list { grid-template-columns: 1fr; gap: 0; }
.s2-grid.is-list .s2-item { grid-column: 1 / -1; margin-top: 0; }
.s2-grid.is-list .s2-move {
  display: grid; grid-template-columns: 120px 1fr;
  align-items: center; gap: 1.6rem;
  height: auto; padding: .85rem 0;
  border-bottom: 1px solid var(--rule);
}
/* One ratio for every thumbnail, so the rows keep an even rhythm. */
.s2-grid.is-list .s2-mask { height: auto; aspect-ratio: var(--ratio, 2 / 3); }
/* Out of the overlay and into the flow as the second column. */
.s2-grid.is-list .s2-info {
  position: static; opacity: 1; background: none; padding: 0;
  align-items: center;
}
.s2-grid.is-list .s2-info span { color: var(--ink); }
@media (max-width: 780px) {
  .s2-grid.is-list .s2-move { grid-template-columns: 84px 1fr; gap: 1.1rem; }
}

/* ── The picture that follows the cursor down the list ───────────────────────────
   Two elements, not one: the outer is moved by script every frame, the inner carries
   the scale and fade. Putting both on one element means the script's transform
   overwrites the CSS one each frame and the entrance never plays. */
.hover-preview {
  position: fixed; top: 0; left: 0;
  width: 280px; height: 360px;
  pointer-events: none; z-index: 999;
  will-change: transform;
}
.hover-preview-inner {
  width: 100%; height: 100%; overflow: hidden;
  opacity: 0; transform: scale(.85);
  transition: opacity .25s ease, transform .25s cubic-bezier(.16, 1, .3, 1);
}
.hover-preview.is-on .hover-preview-inner { opacity: 1; transform: scale(1); }
.hover-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The row under the cursor is the one in focus; the rest step back while it is. */
.s2-grid.is-list.is-browsing .s2-item { opacity: .35; transition: opacity .3s ease; }
.s2-grid.is-list.is-browsing .s2-item:hover { opacity: 1; }
.s2-item figcaption {
  font-family: var(--sans); font-size: .58rem; letter-spacing: .16em;
  text-transform: uppercase; color: var(--accent); margin-top: .7rem;
}
/* Fewer columns as the viewport narrows, so tiles never get too small to read. */
/* Narrower viewports get fewer columns to author against, so the same starts and
   spans stay meaningful; phones drop to a single full-width column. */
@media (max-width: 1100px) { .s2-grid { row-gap: max(4rem, 5.4vw); } }
/* Two across on phones rather than one full-width frame per row — the authored
   twelfth-spans are meaningless at this width, so every frame takes half and the
   full-bleed one keeps the whole row. Ratios still vary, so rows stay uneven. */
@media (max-width: 780px) {
  #section-two { padding: 5rem 0 0; }
  .s2-scroll { padding: 1.1rem; }
  /* No overwidth on a phone. At 340px the window holds three columns, so spending a
     third of the grid off-canvas meant a whole column of photographs was outside the
     window at any moment — see updateGridDrift, which leaves the transform alone here. */
  .s2-pan .s2-grid { width: 100%; }
  .s2-grid {
    /* Two across. At three the frames came out barely a hundred pixels wide — a
       contact sheet rather than a portfolio. */
    grid-template-columns: repeat(2, 1fr);
    column-gap: .8rem; row-gap: 2rem;
    /* Smaller step here — the same multiples would swamp a 100px frame. */
    --s2-step: max(0.55rem, 1.9vw);
    align-items: start;
  }
  /* The authored twelfth-spans and their stagger mean nothing at this width: every
     frame takes half a row, and none of them is nudged down. */
  .s2-item { grid-column: auto / span 1; margin-top: 0; }
  .s2-item.is-full { grid-column: 1 / -1; }
  .s2-item.is-full .s2-mask { height: 52svh; }
  .s2-info { margin-top: .6rem; }
  .s2-info span { font-size: .68rem; }
}

/* ═════════════════════════════════════════════════
   SHOP
═════════════════════════════════════════════════ */
.shop-intro {
  max-width: 44rem; margin-top: 1.4rem;
  font-size: 1.02rem; line-height: 1.7; color: rgba(23,20,15,.72);
}
#shop { padding: 2rem 3rem 7rem; border-top: 1px solid var(--rule); margin-top: 2rem; }
.shop-grid {
  display: grid; grid-template-columns: repeat(3, 1fr);
  column-gap: max(1.5rem, 1.6vw); row-gap: 3.5rem;
  align-items: start;
}
.print { margin: 0; }
/* Same masked frame as the portfolio: the picture fills a declared ratio and is
   clipped rather than letterboxed. */
.print-frame {
  position: relative; aspect-ratio: var(--ratio, 2 / 3);
  overflow: hidden; background: #fff;
}
.print-frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
.print-meta { display: grid; gap: .55rem; margin-top: 1rem; }
.print-title {
  font-family: var(--sans); font-size: 1rem; font-weight: 500;
  letter-spacing: -0.01em; color: var(--ink);
}
.print-price { font-family: var(--sans); font-size: .85rem; color: rgba(23,20,15,.6); }
.print-size {
  display: grid; gap: .3rem; margin-top: .4rem;
  font-family: var(--sans); font-size: .72rem; color: rgba(23,20,15,.58);
}
.print-size select {
  font-family: var(--sans); font-size: .9rem; color: var(--ink);
  background: transparent; border: 0; border-bottom: 1px solid var(--rule);
  border-radius: 0; padding: .4rem 0; cursor: pointer; appearance: none;
}
.print-size select:focus { outline: none; border-bottom-color: var(--ink); }
.print-order {
  justify-self: start; margin-top: .6rem;
  font-family: var(--sans); font-size: .78rem; font-weight: 500;
  letter-spacing: -0.005em;
  color: #fff; background: var(--ink);
  border: 1px solid var(--ink); border-radius: 0;
  padding: .7rem 1.4rem; cursor: pointer;
  transition: background .3s ease, color .3s ease;
}
.print-order:hover { background: transparent; color: var(--ink); }
@media (max-width: 1100px) { .shop-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 780px) {
  #shop { padding: 2rem 1.6rem 5rem; }
  .shop-grid { grid-template-columns: repeat(2, 1fr); column-gap: .9rem; row-gap: 2.4rem; }
  .print-title { font-size: .92rem; }
  .print-order { font-size: .74rem; padding: .6rem 1.1rem; }
}

/* ═════════════════════════════════════════════════
   ABOUT
═════════════════════════════════════════════════ */
/* Inset to match the page heading above it. The right-hand column is empty while
   the body copy is out, so the list is capped rather than stretched full width. */
#about {
  padding: 2rem 3rem 7rem; display: grid;
  grid-template-columns: minmax(0, 46rem); gap: 4rem;
  border-top: 1px solid var(--rule);
}
/* Two columns again as soon as there is copy to sit beside the list. */
#about:has(.about-text) { grid-template-columns: 1fr 1.4fr; }
.about-index { list-style: none; }
.about-index li { padding: 1.1rem 0; border-bottom: 1px solid var(--rule); font-size: .92rem; }
.about-index li strong {
  display: block; font-family: var(--sans); font-size: .78rem; font-weight: 600;
  letter-spacing: -0.005em; text-transform: none; color: var(--accent); margin-bottom: .4rem;
}
.about-index li a { border-bottom: 1px solid currentColor; }
.about-text p { font-size: 1.02rem; line-height: 1.75; margin-bottom: 1.3rem; max-width: 56ch; }
.about-text p:first-child { font-size: 1.2rem; font-style: italic; }

/* ═════════════════════════════════════════════════
   CONTACT
═════════════════════════════════════════════════ */
#contact { background: #fff; color: var(--ink); padding: 7rem 3rem 5rem; }
/* Scaled for the sentence it now carries: at the previous ramp this line ran to
   roughly four times the column width. */
.contact-title { font-family: var(--sans); font-weight: 800; text-transform: uppercase; font-size: clamp(1.7rem, 3.7vw, 3rem); letter-spacing: -.03em; line-height: .95; }
.contact-title em { font-family: var(--serif); font-style: italic; font-weight: 400; text-transform: none; }
/* Sits under the rule in the intro column, at reading size rather than label size —
   it is the point of the page, not a caption. */
.contact-email { font-size: 1.05rem; }
.contact-email a {
  color: var(--ink); border-bottom: 1px solid rgba(23,20,15,.25);
  padding-bottom: .12em; transition: border-color .3s ease;
}
.contact-email a:hover { border-bottom-color: var(--ink); }
.contact-sub { max-width: 46ch; margin-top: 1.4rem; font-size: 1rem; opacity: .8; }

/* ── CONTACT: white page, black type, form beside the heading ── */
/* One column: heading, then the form, then the address under it. The address used to
   sit in a second column beside the form; a sticky aside next to a form the same height
   as it had nothing to stick to. */
.contact-grid {
  display: grid; grid-template-columns: 1fr; gap: 2rem; align-items: start;
  max-width: 44rem;
}
.contact-head { margin-bottom: 1.5rem; }
.contact-aside { margin-top: 1.6rem; padding-top: 1.6rem; border-top: 1px solid var(--rule); }

.contact-form { display: grid; gap: 1.9rem; transform-origin: left center; }
.cf-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.9rem; }
.cf-field { display: grid; gap: .55rem; }
.cf-field label {
  font-family: var(--sans); font-size: .78rem; font-weight: 500;
  letter-spacing: -0.005em; text-transform: none; color: rgba(23,20,15,.58);
}
/* Underline only — a boxed field would fight the rules used everywhere else here. */
.cf-field input, .cf-field select, .cf-field textarea {
  font-family: var(--sans); font-size: 1.02rem; color: var(--ink);
  background: transparent; border: 0; border-bottom: 1px solid var(--rule);
  border-radius: 0; padding: .55rem 0; width: 100%;
  transition: border-color .3s ease;
}
.cf-field textarea { resize: vertical; min-height: 8rem; line-height: 1.6; }
.cf-field select { cursor: pointer; appearance: none; }
/* The native menu renders in the OS palette, so its options need explicit colours. */
.cf-field select option { background: #fff; color: var(--ink); }
.cf-field input:focus, .cf-field select:focus, .cf-field textarea:focus {
  outline: none; border-bottom-color: var(--ink);
}
.cf-field input:user-invalid, .cf-field textarea:user-invalid { border-bottom-color: #b23c30; }

.cf-submit {
  justify-self: start; margin-top: .8rem;
  font-family: var(--sans); font-size: .84rem; font-weight: 500;
  letter-spacing: -0.005em; text-transform: none;
  color: #fff; background: var(--ink);
  border: 1px solid var(--ink); border-radius: 0;
  padding: 1rem 2.4rem; cursor: pointer;
  transition: background .3s ease, color .3s ease;
}
.cf-submit:hover { background: transparent; color: var(--ink); }
.cf-note {
  font-family: var(--sans); font-size: .66rem; letter-spacing: .06em;
  line-height: 1.6; color: rgba(23,20,15,.7); min-height: 1.2em;
}
.cf-note.is-error { color: #b23c30; }

@media (max-width: 900px) {
  .contact-grid { gap: 2rem; }
  /* Stacked, the address belongs above the form rather than buried under it. */
  .contact-head { margin-bottom: 0; }
}
@media (max-width: 560px) { .cf-row { grid-template-columns: 1fr; } }

.contact-links { margin-top: 3.5rem; border-top: 1px solid var(--rule); }
.contact-link-row {
  display: flex; justify-content: space-between; align-items: center;
  padding: 1.6rem 0; border-bottom: 1px solid var(--rule);
  font-size: 1.4rem; transition: opacity .3s;
}
.contact-link-row:hover { opacity: .6; }
.contact-link-row em { font-style: italic; }
.cl-tag { font-family: var(--sans); font-size: .62rem; letter-spacing: .2em; text-transform: uppercase; color: var(--hot); }

/* ── The band under the hero ─────────────────────────────────────────────────────
   The blog, one post to a line: a rule, the title at the left, and the pictures that
   post carries running across beside it. Nothing else — no author, no date, no link to
   read it. Set as a list so a second post simply takes the next line. */
.blank-band {
  position: relative; z-index: 2;
  min-height: 100vh;
  background: #fff;
  display: flex; flex-direction: column; justify-content: center;
  padding: 6rem 3rem;
}
/* The one above the work: a single statement set on the same left margin everything else
   in the page begins on. No screen of height and no padding top or bottom — the band is
   exactly as tall as the words in it, so nothing stands above or below them. */
.blank-band-two { min-height: 0; padding: 4rem 3rem 0; }
/* Names the band, cut exactly as the work's own heading below is — same family, size,
   weight, tracking and ink as .blog-head, so the two read as one pair of headings. */
.band-label {
  margin: 0 0 1.5rem;
  font-family: var(--sans); font-size: 35px; font-weight: 800;
  letter-spacing: -.015em; text-transform: uppercase;
  color: var(--ink);
  opacity: 0; transform: translateY(.45em);
  transition: opacity .62s cubic-bezier(.22, .61, .36, 1),
              transform .62s cubic-bezier(.22, .61, .36, 1);
}
#blankTwo.is-in .band-label { opacity: 1; transform: none; }

.band-say {
  margin: 0; max-width: 46rem;
  /* Inter proper rather than the Inter Tight the rest of the page is set in — this is a
     paragraph to be read rather than a piece of furniture, and the wider fit reads
     easier at length. */
  font-family: 'Inter', var(--sans);
  font-size: clamp(1.15rem, 2.1vw, 1.9rem); font-weight: 400;
  line-height: 1.42; letter-spacing: -0.012em;
  color: var(--accent);
  text-wrap: pretty;
}
@media (max-width: 780px) {
  .blank-band-two { padding: 2.6rem 1.6rem 0; }
}

/* The statement arrives a line at a time rather than all at once: every word carries the
   index of the line it fell on, so a whole line lifts together and the next follows a
   beat later. Which words are on which line is measured in app.js, since only the layout
   knows where it broke. */
.say-w {
  display: inline-block;
  opacity: 0; transform: translateY(.45em);
  transition: opacity .62s cubic-bezier(.22, .61, .36, 1),
              transform .62s cubic-bezier(.22, .61, .36, 1);
  transition-delay: calc(var(--line-i, 0) * .09s + .1s);
}
#blankTwo.is-in .say-w { opacity: 1; transform: none; }

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

/* And a blue rule drawn across the join as the section lands — left to right, the full
   width of the page, so the edge between the hero and the white is deliberate. */
#blankTwo::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: var(--accent); pointer-events: none;
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform .95s cubic-bezier(.2, .7, .3, 1) .05s;
}
#blankTwo.is-in::before { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
  .say-w, #blankTwo::before { transition: none; }
}

/* Room at the foot of the list for the dock that floats over it: without this the last
   row ends underneath the bar and can never be scrolled clear of it. */
#blankBand { padding-bottom: 9rem; }
@media (max-width: 780px) {
  #blankBand { padding-bottom: 7rem; }
}

/* The heading over the list. Ranged left on the same line the titles begin on, and
   ruled off from them the way the footer's column headings are. */
.blog-head {
  margin: 0 0 1.6rem;
  /* Cut the same way as the trade line in the hero: the same family, its heavy weight,
     and the tracking drawn in rather than opened out. */
  font-family: var(--sans); font-size: 35px; font-weight: 800;
  letter-spacing: -.015em; text-transform: uppercase;
  color: var(--ink);
}
.post-list { width: 100%; display: grid; }
/* A line reading left to right: the pictures the post carries, then its name, then what
   it is filed under, ranged to the far right. The pictures are put first by order rather
   than by moving them in the markup — the name stays the row's heading where it is read
   out. The categories are said rather than offered — they are plain words, not a
   menu, so there is nothing here to click through to. No author, no date, no link to read
   it either. The entry is a line on a list rather than a page of its own. */
.post-row {
  position: relative;
  display: flex; align-items: center; gap: 1.2rem;
  border-top: 1px solid var(--rule);
  padding: 1.2rem 0;
  transition: border-color .3s ease;
}
/* The row answers under the cursor, not just its picture: its own rule takes the accent,
   which says the whole line is the target rather than the thumbnail alone. Only where
   there is a cursor to hover with — on a touch screen it would simply be the last row
   touched, left marked. */
@media (hover: hover) and (pointer: fine) {
  .post-row:hover { border-top-color: var(--accent); }
}
/* As wide as the title itself and no wider, so the pictures sit right beside the word
   rather than across a column of air. A long title pushes its own pictures along; that
   is the trade for keeping them close to a short one. */
.post-title { flex: 0 1 auto; min-width: 0; }
.post-shots { order: -1; }
/* What the post is filed under, ranged to the right-hand edge of the section — the auto
   margin takes up whatever the line has left, so every post's filing lines up down the
   same right edge — and dropped a line below the title, so the two read as a heading and
   a note under it rather than as one long line. */
.post-cats {
  /* Dropped by a line with a transform rather than a margin: a centred flex item splits
     any margin between its two sides, so a margin moved these only half as far as it
     said. This shifts them bodily, and by their own line height, so it holds whatever
     size the row is. */
  flex: none; margin: 0 0 0 auto; align-self: center;
  transform: translateY(1.7em);
  display: flex; flex-wrap: wrap; gap: .2rem .9rem;
  font-family: var(--sans); font-size: .62rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--accent);
}

.post-row:last-child { border-bottom: 1px solid var(--rule); }
/* The titles are set in the same face as the categories beside them — the page's own
   grotesque — so the line reads as one piece of type rather than two. */
.post-title {
  margin: 0;
  font-family: var(--sans);
  /* Half of what it was, and set in the roman: no bold anywhere on it, including on the
     link it carries. */
  font-size: clamp(.55rem, 1.13vw, 1rem); font-weight: 400;
  letter-spacing: .02em; line-height: 1;
  color: var(--ink);
}
.post-title a { color: inherit; font-weight: 400; text-decoration: none; transition: opacity .25s ease; }
.post-title a:hover { opacity: .6; }
/* Every picture the post holds, at one height and its own width, so a portrait stays
   portrait beside a landscape. They run off the right edge rather than shrinking if
   there are more of them than the line will take. */
.post-shots {
  display: flex; align-items: center; gap: .6rem;
  /* Not clipped: a picture under the cursor grows past this box, and a hidden or
     scrolling overflow here would cut it off at exactly the edge it is meant to pass. */
  overflow: visible;
}
.post-shots img {
  flex: none; height: 7.5rem; width: auto; display: block;
  object-fit: cover;
  position: relative; z-index: 0;
  transform-origin: center;
  transition: transform .35s cubic-bezier(.2, .7, .3, 1);
}
/* Under the cursor it comes up half again as large, and comes forward — the raised
   layer is what lets it pass over its neighbour rather than under it. */
.post-shots img:hover {
  transform: scale(1.6);
  z-index: 5;
}
@media (prefers-reduced-motion: reduce) {
  .post-shots img { transition: none; }
}
@media (max-width: 780px) {
  /* One column on a phone: the title above, the pictures beneath it. */
  .blank-band { min-height: 70vh; padding: 4rem 1.6rem; }
  /* Said again after the band's own phone rule, which comes later in the file and was
     putting the screen of height back. */
  .blank-band-two { min-height: 0; padding: 2.6rem 1.6rem 0; }
  .post-row { gap: 1.2rem; padding: 1rem 0; }
  .post-shots img { height: 4.8rem; }
  .post-title { flex: 0 0 auto; }
  .post-cats { font-size: .55rem; gap: .2rem .6rem; }
}

/* ── Things arrive rather than appear ─────────────────────────────────────────────
   Two movements, both one-way and both driven by app.js putting `is-in` on an element
   as it crosses into view. A row rises a few pixels and fades up; a photograph uncovers
   itself from its left edge instead of dissolving, which reads as film rather than as a
   web page. The starting state is only applied once the script has marked the document
   as ready, so nothing is left invisible if the script never runs. */
.reveal-ready [data-reveal] {
  opacity: 0; transform: translateY(14px);
  transition: opacity .7s cubic-bezier(.2, .7, .3, 1), transform .7s cubic-bezier(.2, .7, .3, 1);
  /* Each one a beat behind the one before it, so a list arrives as a run rather than
     all at once. The number is written per element by the script. */
  transition-delay: calc(var(--reveal-i, 0) * 90ms);
  will-change: opacity, transform;
}
.reveal-ready [data-reveal].is-in { opacity: 1; transform: none; will-change: auto; }

.reveal-ready [data-wipe] {
  clip-path: inset(0 100% 0 0);
  transition: clip-path .9s cubic-bezier(.22, .85, .24, 1);
  /* A shade behind its row, so the frame moves first and the picture opens into it. */
  transition-delay: calc(var(--reveal-i, 0) * 90ms + 70ms);
  will-change: clip-path;
}
.reveal-ready [data-wipe].is-in { clip-path: inset(0 0 0 0); will-change: auto; }

@media (prefers-reduced-motion: reduce) {
  .reveal-ready [data-reveal],
  .reveal-ready [data-wipe] {
    opacity: 1; transform: none; clip-path: none; transition: none;
  }
}

/* ── A picture opened where it stands ────────────────────────────────────────────
   Clicking a post's photograph carries it up into a reader over the page: the same
   picture, large, with the post's name and a way through to it on the blog. The flight
   is the browser's — the picture is named on both sides of the change and morphed from
   one to the other — so there is nothing here to animate but the ground it lands on. */
.post-shots img { cursor: zoom-in; }
.post-view {
  position: fixed; inset: 0; z-index: 9995;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1.4rem; padding: 4vh 4vw;
  background: rgba(255, 255, 255, .96);
  -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
}
.post-view[hidden] { display: none; }
.post-view img {
  max-width: min(92vw, 1100px); max-height: 74vh;
  width: auto; height: auto; object-fit: contain; display: block;
}
.post-view-say {
  display: flex; align-items: baseline; gap: 1.4rem; flex-wrap: wrap;
  justify-content: center;
}
.post-view-name {
  margin: 0; font-family: var(--sans); font-size: 1rem; font-weight: 400;
  letter-spacing: .02em; color: var(--ink);
}
.post-view-through {
  font-family: var(--sans); font-size: .62rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase; text-decoration: none;
  color: var(--accent);
  padding-bottom: .25rem;
  transition: opacity .25s ease;
}
/* The way out sits in the corner and says so in the same small type as the rest. */
.post-view-shut {
  position: absolute; top: 2.2rem; right: 2.4rem;
  background: none; border: 0; padding: .4rem; cursor: pointer;
  font-family: var(--sans); font-size: .62rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase; color: var(--ink);
  opacity: .7; transition: opacity .25s ease;
}
.post-view-shut:hover { opacity: 1; }
/* Nothing but the picture is named, so the ground it lands on simply fades. */
::view-transition-old(post-shot),
::view-transition-new(post-shot) { animation-duration: .5s; }
@media (max-width: 780px) {
  .post-view { padding: 3vh 4vw; gap: 1rem; }
  .post-view img { max-height: 66vh; }
  .post-view-shut { top: 1.2rem; right: 1.2rem; }
}

/* ── One page giving way to the next ─────────────────────────────────────────────
   A click on Contact, Shop or About does not cut: the page breaks into blue and white
   squares, the squares fill the screen, and the next page comes back out of them the
   same way. About a second the whole way through. The canvas that draws it is made by
   app.js; all that is needed here is a surface over everything else. */
.pixel-curtain {
  position: fixed; inset: 0; z-index: 10000;
  pointer-events: none;
  display: block;
}

/* ── Underlines that are drawn rather than switched ──────────────────────────────
   A rule under a link is a hairline that grows out from the left as the pointer arrives
   and retreats to the right as it goes — the origin is swapped between the two states,
   which is what makes it withdraw the way it came in rather than collapsing back into
   its own middle.

   The rule itself is metal: the same travelling gradient the footer's monogram wears,
   which is what carries that finish onto the hover states rather than leaving it to one
   mark at the foot of the page. Two alloys, since a white metal is invisible on white —
   the light one for links standing on the film, the graphite for those on paper. */
.foot-links a,
.foot-social a,
.hero-social a,
.post-title a,
.post-view-through,
.contact-email a {
  position: relative;
  --sheen: linear-gradient(100deg,
    #2b2f36 0%, #7d8794 14%, #e8ecf2 26%, #6d7784 38%,
    #3a3f47 52%, #dfe4ea 66%, #59626e 80%, #2b2f36 100%);
}
.hero-social a {
  --sheen: linear-gradient(100deg,
    #ffffff 0%, #c9d4f5 12%, #ffffff 26%, #93a5dd 38%,
    #ffffff 52%, #dbe3ff 66%, #ffffff 80%, #8fa2dc 100%);
}
.foot-links a::after,
.foot-social a::after,
.hero-social a::after,
.post-title a::after,
.post-view-through::after,
.contact-email a::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -.18em;
  height: 1px; pointer-events: none;
  background-image: var(--sheen);
  background-size: 260% auto; background-position: 0% center;
  transform: scaleX(0); transform-origin: 100% 50%;
  transition: transform .42s cubic-bezier(.2, .7, .3, 1);
}
.foot-links a:hover::after,
.foot-social a:hover::after,
.hero-social a:hover::after,
.post-title a:hover::after,
.post-view-through:hover::after,
.contact-email a:hover::after {
  transform: scaleX(1); transform-origin: 0 50%;
  /* The sheen only travels while the rule is there to carry it. */
  animation: metallic-shimmer 5s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .foot-links a::after, .foot-social a::after, .hero-social a::after,
  .post-title a::after, .post-view-through::after, .contact-email a::after {
    transition: none;
  }
  .foot-links a:hover::after, .foot-social a:hover::after, .hero-social a:hover::after,
  .post-title a:hover::after, .post-view-through:hover::after, .contact-email a:hover::after {
    animation: none;
  }
}

/* ── The filing follows the post in ──────────────────────────────────────────────
   The categories are not part of the row's own arrival: they wait until it has landed
   and then fade up in their place, which is what makes them read as a note on the post
   rather than another piece of it. */
.reveal-ready .post-row .post-cats {
  opacity: 0;
  transition: opacity .55s ease;
  transition-delay: calc(var(--reveal-i, 0) * 90ms + 430ms);
}
.reveal-ready .post-row.is-in .post-cats { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .reveal-ready .post-row .post-cats { opacity: 1; transition: none; }
}

/* ── FOOTER ─────────────────────────────────────── */
/* A wash behind the footer: one radial in the four colours — white at its heart, then
   silver, the drawing's blue, and black at the outside. Where its centre falls and where
   each colour gives way to the next are rolled afresh on every load by app.js and handed
   here as --foot-radial; what is written below is only what shows if that never runs.
   It is laid over the footer's white rather than replacing it, and held back far enough
   that the type on top of it keeps its contrast. */
.site-foot::before {
  /* One vertical run down the whole footer: white at the top, easing into the lockup's own
     blue at the foot, so the band at the bottom of the page is where the gradient arrives
     rather than something laid over it. The white is held to a bit over a third — that is
     where the columns and the two pills sit. */
  content: ''; position: absolute; inset: 0;
  z-index: 0; pointer-events: none;
  background-image: linear-gradient(to bottom,
    #ffffff 0%, #ffffff 38%, var(--accent) 100%);
}
/* Everything in the footer stands on the wash rather than under it. */
.site-foot > *, .foot-grid, .foot-ack, .foot-meta { position: relative; z-index: 1; }
.site-foot {
  /* Text flipped to dark alongside the background — the previous cream at 60% would
     have been near-invisible on white. .62 alpha on white measures 5.2:1. */
  background: #fff; color: rgba(23,20,15,.62);
  /* Held in a number rather than written twice: the blue band below reaches back out
     through exactly this much to meet the edges of the page. */
  --foot-pad: 3rem;
  /* Held in a number rather than written twice: the lockup at the foot reaches back down
     through exactly this much, so it closes the page rather than floating above it. */
  --foot-pad-bottom: 6.5rem;
  padding: 4rem var(--foot-pad) var(--foot-pad-bottom);
  font-family: var(--sans); font-size: .8rem; letter-spacing: -0.005em; text-transform: none;
}
/* The brand row spans the footer; the two link columns share the row beneath. */
.foot-grid {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 2rem 4rem; align-items: start;
}
/* No longer one of the grid's own: it stands at the foot of the footer, after the
   acknowledgement, and reaches out through the footer's padding to both edges of the
   page the way the band inside it does. */
.foot-brand-col { margin: 3.6rem 0 calc(var(--foot-pad-bottom) * -1); }
/* Order, not markup: the brand stays first in the DOM but sits below the columns. */
.foot-col { order: 2; }
.foot-col-social { order: 2; }
/* Reads as a column heading rather than another link: heavier, a step larger, and
   ruled off from the items beneath it. Same on both breakpoints. */
.foot-head {
  font-size: .92rem; font-weight: 700; letter-spacing: -0.012em; text-transform: none;
  color: var(--ink);
  margin: 0 0 1.1rem; padding-bottom: .7rem;
  border-bottom: 1px solid var(--rule);
}
/* A little more air than the other columns get: these two are pills rather than lines of
   type, and set as close as text they read as one control split in half. */
.foot-links { list-style: none; margin: 0; padding: 0; display: grid; gap: 1.35rem; }
/* The address wears the same pill the header does: a pale, blurred ground with a hairline
   round it, so it reads as something to press rather than as another line of the column.
   It keeps its own hover and drops the drawn underline, which would sit under the pill
   rather than under the words. */
.foot-links a[href^="mailto:"],
.foot-links a[href*="wa.me"] {
  display: inline-flex; align-items: center;
  /* Shallower than the header's pills: these hold an address rather than a word, and at
     the deeper padding they read as buttons twice the weight of the type in them. */
  padding: .28rem 1.1rem .32rem;
  /* Still see-through, but drawn in the blue rather than in white: this part of the
     footer's gradient is white, and a white pill with a white edge on white ground is no
     pill at all — which is exactly what happened when the wash changed. */
  background: rgba(48, 69, 153, .08);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid rgba(48, 69, 153, .32);
  border-radius: 999px;
  color: var(--ink);
  transition: background-color .25s ease, color .25s ease;
}
.foot-links a[href^="mailto:"]:hover,
.foot-links a[href*="wa.me"]:hover { background: rgba(48, 69, 153, .16); }
.foot-links a[href^="mailto:"]::after,
.foot-links a[href*="wa.me"]::after { content: none; }
/* The two pills are the same size whatever is written in them, so the column reads as a
   pair rather than as two different shapes. */
.foot-links a[href^="mailto:"],
.foot-links a[href*="wa.me"] { justify-content: center; min-width: 178px; }

/* The socials run across rather than down — they are the only column left, so a
   stack of three would leave the footer's whole width empty beside them. */
.foot-social {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: .75rem; justify-items: start;
}
.foot-social a, .foot-social .foot-social-item, .foot-links a {
  display: inline-flex; align-items: center; gap: .65rem;
  color: inherit; text-decoration: none; transition: color .25s ease;
}
.foot-social a:hover, .foot-links a:hover { color: var(--ink); }
/* An address, not a label: lowercase and untracked so it reads as something you can
   copy, and darker than the links around it because it is the primary route in. */
.foot-wordmark { white-space: nowrap; }
/* Fixed px, not em: the footer's type is 9px, and an em-sized glyph at that scale
   loses the detail that makes each mark recognisable. */
/* Acknowledgement is prose, not chrome: sentence case at a readable size, and darker
   (9:1 on white) because it is meant to be read rather than skimmed past. */
.foot-ack {
  margin: 3.5rem 0 0; max-width: 60rem;
  border-top: 1px solid rgba(23, 20, 15, .12); padding-top: 1.6rem;
  font-size: .72rem; line-height: 1.7; letter-spacing: normal; text-transform: none;
  color: rgba(23, 20, 15, .78);
}
/* Second row across the same two columns. Each half starts on its column's own edge,
   so "Based in" lines up under the left heading and the year under Socials. */
.foot-meta {
  display: grid; grid-template-columns: 1fr 1fr; gap: .4rem 4rem;
  margin-top: 2.6rem; padding-top: 1.5rem; border-top: 1px solid var(--rule);
}
.foot-meta p {
  margin: 0; font-family: var(--sans); font-size: .72rem;
  letter-spacing: -0.005em; color: rgba(23, 20, 15, .5);
}

@media (max-width: 780px) {
  /* 3rem of side padding costs a quarter of a 375px screen. Wraps so the toggle can
     take a line of its own below the lockup. */
  nav { padding: .2rem 1.6rem 1.15rem; }
  /* Nearer the top again on a phone, where both are smaller. */
  .nav-burger-wrap { top: 0.9rem; }
  /* On a phone there is no column right of the rule: her name goes back to the margin,
     above her trade as it is there. */
  .nav-name-left { left: 1.6rem; top: 5.4rem; }
  /* Mark and name travel together to the right, in the footer's order (mark first),
     using flex `order` because the DOM has the name before the mark. */
  /* No trailing margin: the wordmark ends flush on the nav's padding edge, in the
     corner, with the toggle's bars lined up directly beneath it. */
  .nav-name-left { height: 1.4rem; }
  .nav-mark { height: 2.5rem; }
  .nav-links { gap: 1.3rem; }
  .nav-links a, .nav-brand-name { font-size: .64rem; }
  .intro-strip { grid-template-columns: 1fr; }
  .intro-cell { border-right: none; border-bottom: 1px solid var(--rule); }
  #about { grid-template-columns: 1fr; padding: 2rem 1.6rem 5rem; }
  #contact { padding: 5rem 1.6rem 3.5rem; }
  /* Three across the screen. The width is what is being chosen here — a 2:3 picture
     three to a screen, with a small gutter and two small gaps — and the height follows
     from it, so the card is left to wrap whatever that comes to. */
  .work-card { height: auto; width: auto; margin-right: 0.3rem; }
  .wc-frame { height: 45vw; }
  /* Two lines. A grid flowing by column fills the first card into the top row and the
     second into the bottom, then moves along — so the order is unchanged and the strip
     is half as long, which is what makes two rows fit a phone at all. */
  .works-track {
    padding-left: 0.8rem;
    display: grid; grid-auto-flow: column;
    grid-template-rows: auto auto; row-gap: .7rem;
    align-items: end;
  }
  /* A screen and a bit tall, so the frames settle before the panels start to part —
     at exactly one screen the reveal begins on the very first pixel of scroll. */
  :root { --hero-height: 100svh; }

  /* ── Menu row ──
     No hamburger on a phone: the four links live in the header itself, on a line of
     their own beneath the lockup and hard right. The nav wraps to make room. */
  .nav-toggle { display: none; }
  nav { flex-wrap: wrap; }

  /* Left edge, not centred: back into the flex row (the desktop rule takes it out
     with absolute + left: 50%) with no auto margin, so the lockup starts at the nav's
     padding edge. */
  .nav-brand {
    order: 1; position: relative; left: auto; transform: none;
    margin-left: 0; margin-right: .5rem; z-index: 120;
  }
  /* A full-width basis is what forces the wrap, putting the links on their own line;
     flex-end then pushes the row to the right edge, under the wordmark. */
}
@media (max-width: 360px) {
  .nav-links a { font-size: .58rem; letter-spacing: .09em; }
  .nav-links { gap: 1.1rem; padding: .7rem 1.2rem; }


  /* A fixed rem value barely lifts the copy on a tall phone screen, so scale the
     offset to the viewport instead — puts the headline around mid-frame. */
  /* The hero runs a fifth of a screen past the fold here so the frames can settle
     before they part. Measuring the copy up from the bottom of the *hero* would put
     the headline off-screen on arrival, so it is offset by that overhang and lands
     near the bottom of the first screen instead. */
  /* Sits in the band below the frame rather than near the hero's floor, which on a
     156svh hero was a screen and a half down and off the fold on arrival. */
  .hero-inner { padding: 0 1.6rem calc(var(--hero-h) * .37); }
}

/* ═════════════════════════════════════════════════
   SUB-PAGES (about.html)
═════════════════════════════════════════════════ */
/* The nav is white text with a white logo, which works over the home page's hero
   but disappears on a light page — so light pages invert it. */
body.page-light { background: #fff; }
/* .nav-name-left included — the wordmark moved out of .nav-brand and would
   otherwise have stayed white. */
body.page-light nav,
body.page-light .nav-brand-name,
body.page-light .nav-name-left { color: #000; }
body.page-light .nav-toggle span { background: #000; }

/* Home page: header sits over photographs, so it goes white — About and Contact
   keep the black treatment above, since their grounds are light. */
/* ── The page's ground ───────────────────────────────────────────────────────
   One field for the whole page rather than one the hero carries and takes with it when
   it leaves. It is fixed to the screen, so the grey does not travel: the page moves over
   it, and the tone under the letter is the same at the foot of the scroll as it was at
   the top. Near white at the head, a light grey through the body of it, and lifted again
   at the very bottom so the field has somewhere to go. The bloom high on the left is a
   soft one, wide enough to have no edge of its own — it lifts that part of the field
   rather than sitting on it. */
body.page-hero {
  /* Held in one place so the loading screen can stand on the same field — it is the same
     page, before it is ready. */
  /* ── An open spread ────────────────────────────────────────────────────────
     Two pages meeting at a fold, rather than a field of grey. Read from the outside in:
     the fold down the middle — a soft seam with a darker line at the crease itself, so
     it reads as paper bending rather than as a rule drawn on it; the sheets lifting at
     the outer edges, which is what stops the paper looking like a flat wash; and the
     paper's own tone, a warm near-white that falls very slightly toward the foot. The
     grain that already sits over the whole page does the rest of the work. */
  --page-field:
    linear-gradient(to right,
      rgba(0, 0, 0, 0) 44%,
      rgba(0, 0, 0, .035) 48.4%,
      rgba(0, 0, 0, .085) 49.6%,
      rgba(0, 0, 0, .13) 50%,
      rgba(0, 0, 0, .085) 50.4%,
      rgba(0, 0, 0, .035) 51.6%,
      rgba(0, 0, 0, 0) 56%),
    linear-gradient(to right,
      rgba(0, 0, 0, .06) 0%, rgba(0, 0, 0, .015) 3.5%, rgba(0, 0, 0, 0) 8%,
      rgba(0, 0, 0, 0) 92%, rgba(0, 0, 0, .015) 96.5%, rgba(0, 0, 0, .06) 100%),
    linear-gradient(180deg, #fbfaf7 0%, #f8f7f3 46%, #f4f2ed 100%);
  background: var(--page-field);
  background-attachment: fixed;
  background-repeat: no-repeat;
}

/* ── The loading screen ──────────────────────────────────────────────────────
   Not a spinner over a white page but the page's own field with nothing on it yet: the
   name where the header's name will be, a count where the place name will be, and a rule
   at the height the foot of the wordmark will sit at. It fills as the page loads, and
   when it lifts the letters climb out of that same line. */
.ld {
  position: fixed; inset: 0; z-index: 9998;
  background: var(--page-field, #efefef);
  background-attachment: fixed;
  background-repeat: no-repeat;
  transition: opacity .72s cubic-bezier(.4, 0, .2, 1);
}
.ld.is-out { opacity: 0; pointer-events: none; }
/* The header's bar, to the pixel: same height, same margins, same middle — so the name
   does not move when the real one takes over. */
.ld-bar {
  position: absolute; top: 0; left: 0; right: 0; height: 68px;
  padding: 0 clamp(1.25rem, 3vw, 2.75rem);
  display: flex; align-items: center; justify-content: space-between;
}
.ld-name {
  font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-weight: 700;
  letter-spacing: -.01em; color: var(--ink); line-height: 1;
  font-size: .86rem;
}

/* The word, laid over the box the real one occupies — see index.html. */
.ld-mark { position: absolute; pointer-events: none; display: block; }
/* The mark has arrived by the time the screen lifts, so the arrival does not run: the
   rule it climbs out of is not drawn, and the letters are simply where they belong. The
   drift is untouched — that one never stops. */
html.ld-done .mark-rule { display: none; }
html.ld-done .grise { animation: none !important; }
/* Nothing on the page below runs while it is covered. The arrival is the first thing a
   visitor sees, and it was playing to an empty room behind the loading screen. */
html.is-loading .hero *, html.is-loading nav * { animation-play-state: paused !important; }
@media (prefers-reduced-motion: reduce) {
  .ld { transition: none; }
}
/* The hero had a field of its own, drawn to sit under the header and the mark together.
   With one ground for the page there is nothing left for it to do, and two fields laid
   over each other is how a seam appears at the hero's foot. */
body.page-hero .hero { background: none; }
body.page-hero nav,
body.page-hero .nav-brand-name,
body.page-hero .nav-name-left { color: #fff; }
body.page-hero .nav-toggle span { background: #fff; }
body.page-hero .nav-mark {
  /* Flat white, matching the plain white the nav links and wordmark already use —
     no gradient and no shimmer. */
  background: #fff;
  animation: brand-in .9s cubic-bezier(.22,.61,.36,1) both;
  /* Lifts the mark off the photographs behind it; without the gradient's darker
     bands there is nothing else separating white-on-pale. */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .35));
}

/* Past the hero reveal the page is white all the way down, so the header inverts.
   Set from app.js — see updateNavContrast. */
body.page-hero nav.on-light,
body.page-hero nav.on-light .nav-brand-name,
body.page-hero nav.on-light .nav-name-left { color: #000; }
body.page-hero nav.on-light .nav-toggle span { background: #000; }
/* Flat black, and the drop shadow goes — it exists to lift the mark off photographs
   and would read as a smudge against white. */
body.page-hero nav.on-light .nav-mark { background: #000; filter: none; }
/* The wordmark is masked, so its colour is a background — these mirror the `color`
   rules above, which a mask ignores. */
/* The wordmark is a masked block, so its background is its ink. Black throughout now: it
   stands in the hero on the section's own ground rather than over a photograph, so there
   is nothing for it to turn for. */
body.page-light .nav-name-left { background: #000; }
body.page-hero .nav-name-left { background: #000; }
/* The three rules turn with the name, on the same measurement: white while the header
   stands over the hero, black from the moment the white section reaches it. They are
   painted rather than typed, so it is their background that changes rather than their
   colour. */
body.page-light .nav-burger span { background: #000; }
body.page-hero .nav-burger span { background: #fff; }
body.page-hero nav.on-light .nav-burger span { background: #000; }
/* The size is no longer a state but a count: --name-scale and --burger-scale are written
   by the scroll from one at the top of the page down to a half at the moment the type
   turns black, so the two draw down through the whole approach rather than stepping at
   the end. app.js writes them; the same measurement decides the colour. The other pages
   keep their own sizes — they have no hero to turn away from. */
/* Over the hero the header carries the lockup alone. The links — and the Menu button
   that stands in for them on a phone — arrive with the gallery, already black, at the
   same moment the header inverts. Home page only: About and Contact have no hero to
   clear, so theirs are visible throughout. */
/* The button keeps the hard rule; the dock does not — it comes up on --nav-fade as the
   grid is scrolled, and a rule that pinned it to zero until the header inverted meant
   the ramp was never seen. */
body.page-hero nav:not(.on-light) .nav-toggle {
  opacity: 0; visibility: hidden; pointer-events: none;
}
/* ...but not while the menu is open. The panel lays its own dark ground over the
   whole screen, so inverting for the white page behind it put black links on near
   black — 1.14:1. Everything in the header reverts to white for as long as it's up. */
body.page-hero nav.on-light.open,
body.page-hero.nav-on-light.nav-open .nav-links a,
body.page-hero nav.on-light.open .nav-brand-name,
body.page-hero nav.on-light.open .nav-name-left { color: #fff; }
body.page-hero nav.on-light.open .nav-toggle span { background: #fff; }
body.page-hero nav.on-light.open .nav-burger span { background: #fff; }
body.page-hero nav.on-light.open .nav-mark {
  background: #fff; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .35));
}
/* The panel is black, so its links are white — including once the header has inverted
   for the white page behind it, which is why this sits after those rules rather than
   fighting them on specificity. */
/* The dock is pale on every page now, so its words are black on every page. This block
   said the opposite for as long as the bar was dark, and it outranks everything above by
   design — which is why the bar's own `color` was having no effect until it was changed
   here. */
body.page-light .nav-links a,
body.page-hero .nav-links a,
body.page-hero.nav-on-light .nav-links a,
body.page-hero.nav-on-light.nav-glass .nav-links a,
body.nav-glass .nav-links a { color: #000; }

/* With no box there is nothing to fill, so the current page is marked with a rule
   under the word and the pointer just dims it. */
/* A filled sub-pill marks the page you are on — the segmented-control idiom. */
/* Marked by weight rather than by a pill: the bar is pale now, and the filled pill that
   used to carry the current page was white on black — invisible against this. */
body.nav-glass .nav-links a.active,
.nav-links a.active { background: none; font-weight: 700; }
body.nav-glass .nav-links a:hover,
.nav-links a:hover { background: rgba(0, 0, 0, .06); }

/* The items arrive one after another rather than the panel appearing all at once. */
@keyframes nav-item-in {
  from { opacity: 0; transform: translateY(-7px); }
  to   { opacity: 1; transform: none; }
}
body.nav-glass .nav-links a {
  animation: nav-item-in .45s cubic-bezier(.22, .61, .36, 1) both;
}
body.nav-glass .nav-links a:nth-child(1) { animation-delay: .06s; }
body.nav-glass .nav-links a:nth-child(2) { animation-delay: .10s; }
body.nav-glass .nav-links a:nth-child(3) { animation-delay: .14s; }
body.nav-glass .nav-links a:nth-child(4) { animation-delay: .18s; }
@media (prefers-reduced-motion: reduce) {
  body.nav-glass .nav-links a { animation: none; }
}

.page { padding-top: 8rem; }
.page-head { padding: 3rem 3rem 1rem; }
.page-title {
  font-family: var(--sans); font-weight: 800; text-transform: uppercase;
  font-size: clamp(2.4rem, 7vw, 5.6rem); letter-spacing: -.03em; line-height: .9; margin: 0;
}
.page #about { padding-top: 2rem; }
@media (max-width: 780px) {
  .page { padding-top: 6rem; }
  .page-head { padding: 2rem 1.6rem 1rem; }
}

/* Contact page keeps the section's own dark ground, so the default white nav still
   reads and no light-page inversion is needed. Body matches to avoid a cream flash
   above/below the section. */
.page-contact { padding-top: 3rem; }
/* The phone header is two rows tall (lockup above, toggle below), so the heading
   needs more room under it than the one-row desktop bar does. */
@media (max-width: 780px) { .page-contact { padding-top: 3rem; } }

/* Footer brand mark. The logo is a white PNG, so it's used as a MASK and the colour
   comes from the gradient behind it — that way it can be blue without needing a
   recoloured asset. Shares the existing metallic-shimmer keyframes. */
.foot-brand {
  /* The blue runs the whole width of the page: the band reaches back out through the
     footer's own side padding to meet both edges, and takes that padding back on the
     inside, so the mark and the name still begin on the same line as everything else in
     the footer. nowrap and an em-based gap so the two scale as one lockup rather than
     drifting apart or breaking a line. */
  /* The two ends of the band rather than a pair in the corner: the monogram sits on the
     left margin, her name on the right, with the width of the page between them. */
  display: flex; align-items: center; justify-content: space-between; gap: 0.2em;
  flex-wrap: nowrap;
  margin-inline: calc(var(--foot-pad) * -1);
  font-family: var(--sans); font-weight: 400;
  letter-spacing: -0.018em; line-height: 0.86;
  font-size: 8vw;
  padding: .22em var(--foot-pad) .28em;
  position: relative; isolation: isolate;
  /* A white strip laid across the top of the band. Drawn as the band's own top border so
     it runs the same full width the blue does — the blue behind it is an inset layer and
     stops at the padding box, leaving the border its own three pixels. */
  border-top: 3px solid #fff;
}
/* The blue is behind the whole lockup — the mark and the name both stand on it — and it
   runs the full width of the page. */
.foot-brand::after {
  content: ''; position: absolute; inset: 0;
  z-index: 0; background: var(--accent); pointer-events: none;
}
/* Desaturated before the contrast boost: feTurbulence makes independent noise per
   channel, and stretching it while still coloured turns the band into RGB speckle
   rather than grain. Lighter here than the hero — this is a small band of type, not a
   full-bleed panel. */
.foot-brand::before {
  content: ''; position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='fg'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='1.6' intercept='-0.3'/%3E%3CfeFuncG type='linear' slope='1.6' intercept='-0.3'/%3E%3CfeFuncB type='linear' slope='1.6' intercept='-0.3'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23fg)'/%3E%3C/svg%3E");
  opacity: .3; mix-blend-mode: overlay;
}
.foot-brand::before { z-index: 0; }
.foot-mark, .foot-wordmark { position: relative; z-index: 1; }
/* The line of small print, set in the band's own bottom right corner beneath her name.
   Placed rather than in flow, so it cannot push the lockup about; the band is already the
   thing it is measured against. */
.foot-copy {
  position: absolute; right: var(--foot-pad); bottom: .34em;
  z-index: 1;
  font-family: var(--sans); font-size: .58rem; font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase;
  color: rgba(255, 255, 255, .72);
  white-space: nowrap;
}
@media (max-width: 780px) {
  .foot-copy { font-size: .5rem; letter-spacing: .08em; bottom: .3em; }
}
/* Wordmark carries its own metallic, so it needs to be its own element: background-clip
   on the flex parent would have to compete with the mark's masked background. */
/* The name set as the logotype rather than in type: the same wordmark.png the header
   wears, carried as a mask so the white comes from the background and the artwork keeps
   its own drawing. The words stay in the markup and are read out of sight. */
.foot-wordmark {
  /* Sized in em rather than rem, so the name scales with the lockup it stands in and
     holds its proportion to the mark beside it instead of sitting small against it. */
  display: inline-block; height: 0.66em; aspect-ratio: 1196 / 161;
  overflow: hidden; text-indent: -200%; white-space: nowrap;
  /* The name is a mask, so its colour is whatever this background paints. It stands on
     the blue for its whole height now, so that is plain white. */
  background: #fff;
  -webkit-mask: url("wordmark.png") left center / contain no-repeat;
          mask: url("wordmark.png") left center / contain no-repeat;
  vertical-align: middle;
}
@media (max-width: 780px) {
  .site-foot { --foot-pad: 1.6rem; --foot-pad-bottom: 5.5rem; padding: 3rem var(--foot-pad) var(--foot-pad-bottom); }
  /* Social and Contact share a row; the brand takes the full width above them. The
     contact column gets the wider share — its labels are the longer ones. */
  /* One column on a phone: two columns of links at this width left each of them
     narrower than the words in it. The sections stack in reading order instead. */
  .foot-grid { grid-template-columns: 1fr; gap: 2.2rem; }

  /* The clamp bottoms out at 1.06rem on a phone, which left the brand smaller than
     the acknowledgement below it. The mark is sized in em, so it follows. */
  .foot-brand { font-size: 6.8vw; }
  /* Every link is a finger target, not a line of type: 44px of height minimum, which
     is the smallest a thumb reliably hits. `display: flex` is what gives min-height
     something to act on — on an inline anchor it does nothing. The negative margin
     keeps the extra height from pushing the rows apart visually, so the list still
     looks like a list rather than a stack of buttons. */
  .foot-links, .foot-social { gap: 0; }
  .foot-social a, .foot-social .foot-social-item, .foot-links a {
    display: flex; align-items: center;
    min-height: 44px; min-width: 44px; font-size: .74rem;
  }
  /* More air between the two pills on a phone than on a wide screen: they are taller here
     — a finger's worth of height each — and at the desktop's spacing they close up into
     one block. The pills keep their own shape rather than being stretched by the rule
     above, which turns these links into full-width rows. */
  .foot-links { gap: 1.9rem; }
  .foot-links a[href^="mailto:"],
  .foot-links a[href*="wa.me"] {
    display: inline-flex; width: max-content; min-width: 178px;
  }
  .foot-ack { margin-top: 2.4rem; font-size: .66rem; line-height: 1.65; }
  .foot-meta { grid-template-columns: 1fr; gap: .3rem; margin-top: 2rem; }
  .foot-meta p { font-size: .58rem; }
}
.foot-mark {
  display: inline-block; height: 0.84em; aspect-ratio: 600 / 429;
  /* A highlight travels across the mark. The PNG is used as a mask, so the background
     is what shows through it: whites and pale blues here, since the greys it used to
     carry were mixed for a white footer and go muddy on the accent. Rides the same
     metallic-shimmer keyframes as the rest of the site. */
  background-image: linear-gradient(115deg,
    #ffffff 0%, #c9d4f5 10%, #ffffff 18%, #93a5dd 28%,
    #ffffff 40%, #dbe3ff 52%, #ffffff 62%, #8fa2dc 74%,
    #ffffff 86%, #c9d4f5 94%, #ffffff 100%);
  background-size: 260% auto; background-position: 0% center;
  animation: metallic-shimmer 7s linear infinite;
  -webkit-mask: url("brand.svg") center / contain no-repeat;
          mask: url("brand.svg") center / contain no-repeat;
}
@media (prefers-reduced-motion: reduce) { .foot-mark { animation: none; } }



/* ── Blue accents ───────────────────────────────
   The accent already appears on the small labels; these extend it to the things a
   visitor acts on, so the blue reads as "this is live" rather than decoration. */

/* Selecting text on the page picks up the accent rather than the browser default. */
::selection { background: var(--accent); color: #fff; }

/* Links answer in blue on hover, on the pages and in the footer. */
.foot-social a:hover, .foot-links a:hover,
body.page-light .about-index a:hover,
.contact-email a:hover { color: var(--accent); }

/* Prices, which are the one number on the shop worth finding quickly. */
.print-price { color: var(--accent); }

/* Form focus: a visible blue ring rather than the browser's default outline. */
.cf-field input:focus-visible,
.cf-field select:focus-visible,
.cf-field textarea:focus-visible,
.print-size select:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* Anything focused by keyboard gets the same treatment, so tabbing is followable. */
a:focus-visible, button:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 3px;
}
/* On the dark menu dock a dark blue ring would vanish, so it lightens there. */
.nav-links a:focus-visible, .bag-checkout:focus-visible {
  outline-color: #8fa4ff;
}

/* A few more, chosen where a word is doing work rather than just sitting there. */

/* The label on the pinned gallery, alongside the card numbers it sits above. */

/* The rule under every page title. It was currentColor, which made it black on black
   text; the accent turns it into a mark rather than an underline. */
.accent-rule { background: var(--hot); }

/* "email" in the contact heading — the one word in the sentence that is the point. */
.contact-title em { color: var(--hot); }

/* The only link in the About list, so it should read as one. */
.about-index a { color: var(--accent); }

/* The order button answers in blue before it opens the mail draft. */
.print-order:hover { border-color: var(--accent); color: var(--accent); }


/* ── Featured cards: tilt, glare, and the split hero ── */

/* The card carries the scroll reveal's transform, so the tilt lives on this wrapper.
   Both are needed at once and neither can overwrite the other. */
.wc-tilt {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; justify-content: flex-end;
  transform: perspective(1000px) rotateX(0deg) rotateY(0deg);
  transition: transform .6s cubic-bezier(.16, 1, .3, 1);
}
/* Faint sheen that follows the pointer across the surface. Its own layer so it can
   fade independently of the tilt, and inert to the mouse so it never steals the move
   events the tilt is reading. */
.wc-tilt::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(circle at var(--gx, 50%) var(--gy, 50%),
    rgba(255, 255, 255, .22), rgba(255, 255, 255, 0) 55%);
  opacity: var(--glare, 0); transition: opacity .45s ease;
  mix-blend-mode: soft-light;
}
@media (prefers-reduced-motion: reduce), (hover: none), (pointer: coarse) {
  .wc-tilt { transition: none; }
  .wc-tilt::after { display: none; }
}

/* The image under the badge steps back so the badge reads first, and the frame itself
   grows a tenth under the pointer. The growth is on .wc-frame rather than on .wc-tilt,
   which carries an inline transform written by the tilt on every mouse move and would
   wipe a scale declared here. Being a transform it costs no layout, so the row does not
   shuffle as cards grow; the hovered card is simply lifted over its neighbours. */
@media (hover: hover) and (pointer: fine) {
  .wc-num { transition: opacity .2s ease; }
  /* The number can stay: the label no longer shares its line. */

  .wc-frame img, .wc-frame video { transition: opacity .45s ease; }
  .work-card:hover .wc-frame img, .work-card:hover .wc-frame video { opacity: .92; }

  /* The hover growth is a number rather than a transform now, so it can be multiplied
     by the carousel's own scale instead of overwriting it. Registering the property is
     what lets a variable be eased; without that support it simply snaps, which is a
     fair way to lose it. */
  .work-card:hover { z-index: 2; }
  .work-card:hover .wc-frame { --hov: 1.1; }
}

/* The two-panel hover that used to live here — one half widening while the other dimmed
   — went with the second panel. Its opacity transition had to go too: it eased a value
   the scroll writes every frame, which left the photograph lagging seconds behind. */

/* ── Mobile hero: stacked composition on the first screen ──
   The hero is 156svh tall so the frame can settle before the curtain parts, which puts
   everything below roughly two thirds of it off the fold. The frame takes the top 45%,
   the grainy ground takes the rest, and the copy sits between the two — all of it
   above the first screen's floor. */
@media (max-width: 780px) {
  .hero-inner { padding: 0 1.6rem calc(var(--hero-h) - 100dvh + 1.2rem); }
}

/* The current page's link is a filled white pill, so its type goes dark — this outranks
   the blanket white applied to every link in the dock. */
body.page-light .nav-links a.active,
body.page-hero .nav-links a.active,
body.page-hero.nav-on-light .nav-links a.active,
body.page-hero.nav-on-light.nav-glass .nav-links a.active,
body.nav-glass .nav-links a.active { color: #000; }

/* ── What a phone does not pay for ───────────────────────────────────────────────
   Scrolling on a phone is limited by painting, not by script — the whole scroll pass
   measures under a tenth of a millisecond against a sixteen millisecond frame. What
   costs is redrawing large areas, and three things here were doing exactly that on
   every scrolled frame. Each is a texture or a hint, not structure, so a phone goes
   without and the desktop keeps all of it. */
@media (max-width: 780px) {
  /* The page-wide grain covers the viewport and sits above everything, so the screen
     had to be redrawn through it continuously. At 4.5% on a small display it was
     giving up very little for what it cost. */
  body::after { display: none; }
  /* The hero's grain used to come back at this width in a cheaper form. There is no
     grain over the hero at all now, on any screen. */

  /* The grid never drifts sideways at this width — updateGridDrift leaves it alone —
     so this was holding a five-thousand pixel layer of twenty-five photographs on the
     compositor for a movement that never happens. */
  .s2-pan .s2-grid { will-change: auto; }
  /* The hero photograph now fades as well as travels, so say so: naming both is what
     lets it be handled as a layer rather than repainted. */
}

/* The bar's blur is a desktop-only luxury. A backdrop filter has to resample whatever
   passes beneath it on every frame, and this one is pinned over the whole scroll — the
   same class of cost that made the hero stutter on a phone. Solid there instead, which
   at this size and against these grounds is nearly indistinguishable. */
@media (max-width: 780px) {
  .nav-links,
  .nav-contact,
  .nav-about,
  .nav-portfolio {
    -webkit-backdrop-filter: none; backdrop-filter: none;
    background: transparent;
  }
  /* Both come off the phone: a column of pills down the corner of a 390px screen takes
     half the hero with it. Every destination is still in the page — they are behind the
     plus beside her name — so nothing is lost by taking the shortcuts away here. */
  .nav-contact, .nav-about-wrap, .nav-portfolio { display: none; }
}

/* ── Intro page ──────────────────────────────────────────────────────────────────
   One mark, as large as the screen will carry, centred on both axes. It is a heading
   rather than a decorative box: the words are in the markup and hidden from sight, so
   the page still says whose it is to a search engine or a screen reader while showing
   only the monogram. The artwork is the same brand.svg the header wears, carried as a
   mask so the colour is set here — black, since this page has a white ground. */
.intro {
  min-height: 100svh;
  display: grid; place-items: center;
  padding: 3vmin;
  /* No projection. The object keeps its depth, but with nothing to project it from, the
     layers behind the face are drawn exactly the same size and in exactly the same
     place, so the face covers them and the mark is flat. Squaring the object alone was
     not enough: perspective still shrank the layers behind, and their rim showed at the
     edges of the strokes furthest from the middle. Restore this line and the object is
     three-dimensional again, unchanged. */
  perspective: none;
}
.intro-mark {
  margin: 0;
  width: min(92vmin, 80rem); aspect-ratio: 600 / 429;
  background: #000;
  -webkit-mask: url("brand.svg") center / contain no-repeat;
          mask: url("brand.svg") center / contain no-repeat;
  /* The text is kept for machines and read out of sight, rather than deleted. */
  overflow: hidden; text-indent: -200%; white-space: nowrap;
}
/* Once the stack is in, the mark itself stops drawing — its layers carry it between
   them. The perspective lives on the page's frame rather than here, so the object is
   seen from the viewer's position and turning it foreshortens rather than skews. */
.intro-mark.is-object {
  position: relative; overflow: visible;
  background: none;
  -webkit-mask: none; mask: none;
  -webkit-transform-style: preserve-3d; transform-style: preserve-3d;
  /* Square on. The object is still built — 26 layers of real depth — but seen straight
     down its own axis every layer behind the face is concentric with it and smaller, so
     the face covers them completely and it reads as a flat mark. One line here turns it
     back into something you can see the thickness of. */
  transform: none;
}
/* The KM mark, top left of page 1, alongside the left rail. It is drawn as a mask so
   the letters take their colour from the page's ink rather than from a flat image, and
   the words stay in the markup for machines and are read out of sight. */
.page-mark {
  margin: 0 0 0 2rem;
  width: 16.665vw; aspect-ratio: 600 / 429;
  background: var(--ink, #000);
  -webkit-mask: url("brand.svg") left top / contain no-repeat;
          mask: url("brand.svg") left top / contain no-repeat;
  overflow: hidden; text-indent: -200%; white-space: nowrap;
}
@media (max-width: 780px) { .page-mark { margin-left: 1rem; width: 23vw; } }

.im-layer {
  position: absolute; inset: 0;
  -webkit-mask: url("brand.svg") center / contain no-repeat;
          mask: url("brand.svg") center / contain no-repeat;
}

/* ── Rails ───────────────────────────────────────────────────────────────────────
   Two thin black columns, one down each edge, each carrying half the menu. The words are
   set on their sides — turned to read upward on the left and downward on the right, so
   each runs away from the corner it starts at rather than both leaning the same way.
   A page that uses them takes `has-rails`, which keeps its own content clear of both. */
.rail {
  position: fixed; top: 2%; bottom: 2%; z-index: 50;
  width: var(--rail-w, 46px);
  padding: 0; margin: 0;
  background: #000;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2.6rem;
  /* Widening on hover. The width is the animated property rather than a scale, so the
     black grows inward from its own edge and the type inside stays the size it was —
     scaling the whole rail would have magnified the words with it. */
  transition: width .35s cubic-bezier(.16, 1, .3, 1),
              top .35s cubic-bezier(.16, 1, .3, 1),
              bottom .35s cubic-bezier(.16, 1, .3, 1);
}
@media (hover: hover) and (pointer: fine) {
  /* On hover the rail grows sideways and also reaches up and down through the
     2% white bands to the edges of the screen. */
  .rail:hover { width: calc(var(--rail-w, 46px) + 18px); top: 0; bottom: 0; }
}
@media (prefers-reduced-motion: reduce) { .rail { transition: none; } }
.rail-left { left: 0; right: auto; }
.rail-right { right: 0; left: auto; }
.rail a {
  writing-mode: vertical-rl; text-orientation: mixed;
  font-family: var(--sans); font-size: .64rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase;
  color: #fff; text-decoration: none; opacity: .82;
  transition: opacity .25s ease;
}
.rail a:hover, .rail a:focus-visible { opacity: 1; }
/* Near the top rather than in the centred group, so it reads as the rail's title rather
   than as one more of its entries. Lifted out of the flow for the same reason — the
   others stay centred as a pair whatever is added above them. */
.rail-top {
  position: absolute; top: 1.6rem; left: 50%;
  transform: translateX(-50%) rotate(180deg);
  writing-mode: vertical-rl; text-orientation: mixed;
  font-family: var(--sans); font-size: .64rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase;
  color: #fff;
}
/* Reading upward on the left: the same turn a book's spine takes. */
.rail-left a { transform: rotate(180deg); }
body.has-rails {
  padding-left: var(--rail-w, 46px); padding-right: var(--rail-w, 46px);
  /* The same band top and bottom, so anything put on the page lines up with the rails
     rather than starting above where they do. */
  padding-top: 2vh; padding-bottom: 2vh;
}
@media (max-width: 780px) { body.has-rails, .rail { --rail-w: 34px; } }

/* ── The contact window ──────────────────────────────────────────────────────
   Black, over everything, and shut until it is asked for. The page behind it is left
   where it is rather than hidden, so closing puts the visitor back exactly where they
   were. */
/* The page stays visible behind: the black is a box floating over it, not the whole
   screen going out. */
.cx-veil {
  position: fixed; inset: 0; z-index: 9999;
  background: rgba(10, 10, 10, .52);
  -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);
  display: flex; align-items: center; justify-content: center;
  padding: clamp(1.25rem, 5vw, 4rem);
  opacity: 0; transition: opacity .3s ease;
}
.cx-veil[hidden] { display: none; }
.cx-veil.is-open { opacity: 1; }
.cx-panel {
  position: relative;
  width: min(520px, 100%);
  padding: clamp(1.9rem, 4vw, 2.9rem);
  background: #0A0A0A;
  border: 1px solid rgba(255, 255, 255, .09);
  box-shadow: 0 34px 90px rgba(0, 0, 0, .55);
  /* Comes up rather than in: a little below and a shade small, settling as it arrives. */
  transform: translateY(18px) scale(.98);
  transition: transform .45s cubic-bezier(.16, 1, .3, 1);
}
.cx-veil.is-open .cx-panel { transform: translateY(0) scale(1); }

/* In the box's own corner now, not the screen's. */
.cx-close {
  position: absolute; top: .55rem; right: .55rem;
  width: 44px; height: 44px; line-height: 1;
  background: none; border: 0; cursor: pointer;
  font-family: var(--sans); font-size: 1.9rem; color: rgba(255, 255, 255, .55);
  transition: color .2s ease;
}
.cx-close:hover { color: #fff; }

.cx-eyebrow {
  margin: 0 0 .9rem; font-family: var(--sans);
  font-size: .68rem; font-weight: 500; letter-spacing: .2em; text-transform: uppercase;
  color: rgba(255, 255, 255, .4);
}
.cx-title {
  margin: 0 0 2.2rem; font-family: var(--sans); font-weight: 500;
  font-size: clamp(1.5rem, 3vw, 2.1rem); line-height: 1.15; letter-spacing: -.025em;
  color: #fff;
}

.cx-field { margin: 0 0 1.5rem; display: flex; flex-direction: column; gap: .5rem; }
.cx-field label {
  font-family: var(--sans); font-size: .66rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase; color: rgba(255, 255, 255, .42);
}
/* Ruled rather than boxed: a line under each field, which is all it needs on black. */
.cx-field input, .cx-field textarea {
  background: none; border: 0; border-bottom: 1px solid rgba(255, 255, 255, .22);
  border-radius: 0; padding: .5rem 0;
  font-family: var(--sans); font-size: 1rem; color: #fff;
  transition: border-color .2s ease;
}
.cx-field textarea { resize: vertical; line-height: 1.5; }
.cx-field input:focus, .cx-field textarea:focus { outline: 0; border-bottom-color: #fff; }
.cx-field input:-webkit-autofill { -webkit-text-fill-color: #fff; }
/* Only after someone has actually filled it in badly — :user-invalid waits for the
   interaction, where :invalid would redden an untouched required field on sight. */
.cx-field input:user-invalid { border-bottom-color: #E0483A; }

.cx-send {
  margin-top: .6rem; padding: .9rem 2rem; border: 1px solid #fff; border-radius: 0;
  background: #fff; color: #0A0A0A; cursor: pointer;
  font-family: var(--sans); font-size: .72rem; font-weight: 500;
  letter-spacing: .16em; text-transform: uppercase;
  transition: background-color .2s ease, color .2s ease, opacity .2s ease;
}
.cx-send:hover { background: none; color: #fff; }
.cx-send[disabled] { opacity: .35; cursor: not-allowed; }
.cx-send[disabled]:hover { background: #fff; color: #0A0A0A; }
.cx-note {
  margin: 1rem 0 0; min-height: 1.2em;
  font-family: var(--sans); font-size: .8rem; line-height: 1.5;
  color: rgba(255, 255, 255, .5);
}
.cx-note.is-bad { color: #E0483A; }

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

/* ── The book behind the page ────────────────────────────────────────────────
   Two pages lying open under everything, drawn by book.js. Under the content (z -1) and
   above the paper tone the body carries, so the seam and the lifted outer edges are laid
   back over the pictures here. The leaf turns about the fold, from the right-hand page to
   the left, and carries a different picture on each face. */
.bk {
  position: fixed; inset: 0; z-index: -1; pointer-events: none; overflow: hidden;
  perspective: 2600px; perspective-origin: 50% 50%;
  background: #f8f7f3;
}
.bk-page, .bk-leaf { position: absolute; top: 0; bottom: 0; width: 50%; overflow: hidden; }
.bk-left { left: 0; } .bk-right { left: 50%; }
.bk svg { display: block; width: 100%; height: 100%; }
.bk-leaf {
  left: 50%; overflow: visible; transform-origin: 0 50%; transform-style: preserve-3d;
  transition: transform 1.9s cubic-bezier(.62, .04, .22, 1);
}
.bk-leaf.is-turning { transform: rotateY(-180deg); }
.bk-face { position: absolute; inset: 0; overflow: hidden; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.bk-back { transform: rotateY(180deg); }
/* A shade that follows the turn: the leaf darkens as it stands up and clears as it lands. */
.bk-face::after {
  content: ""; position: absolute; inset: 0; opacity: 0;
  background: linear-gradient(to right, rgba(0, 0, 0, .22), rgba(0, 0, 0, 0) 70%);
  animation: bkShade 1.9s cubic-bezier(.62, .04, .22, 1) both;
}
.bk-back::after { background: linear-gradient(to left, rgba(0, 0, 0, .22), rgba(0, 0, 0, 0) 70%); }
@keyframes bkShade { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }
/* The fold and the lifted outer edges, over the pictures, same as the page's own tone. */
.bk-seam {
  position: absolute; inset: 0; z-index: 2;
  background:
    linear-gradient(to right,
      rgba(0, 0, 0, 0) 44%, rgba(0, 0, 0, .035) 48.4%, rgba(0, 0, 0, .085) 49.6%,
      rgba(0, 0, 0, .13) 50%, rgba(0, 0, 0, .085) 50.4%, rgba(0, 0, 0, .035) 51.6%,
      rgba(0, 0, 0, 0) 56%),
    linear-gradient(to right,
      rgba(0, 0, 0, .06) 0%, rgba(0, 0, 0, .015) 3.5%, rgba(0, 0, 0, 0) 8%,
      rgba(0, 0, 0, 0) 92%, rgba(0, 0, 0, .015) 96.5%, rgba(0, 0, 0, .06) 100%);
}
.bk-leaf { z-index: 1; }
@media (prefers-reduced-motion: reduce) { .bk-leaf { transition: none; } }
