/* ==========================================================================
   STONEGRAPH — Section Components
   ========================================================================== */

/* ---------- HERO ---------- */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: flex-end;
  padding-top: 130px;
  padding-bottom: clamp(38px, 5vw, 66px);
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 76% 18%, rgba(22,84,79,0.10) 0%, transparent 58%),
    radial-gradient(90% 70% at 8% 92%, rgba(168,124,46,0.12) 0%, transparent 60%),
    var(--sand);
}
.hero__art {
  position: absolute;
  top: -6%;
  right: -14%;
  width: min(70vw, 880px);
  height: 116%;
  opacity: .62;
  pointer-events: none;
  z-index: 0;
  mask-image: linear-gradient(to left, #000 46%, rgba(0,0,0,.22) 82%, transparent 99%);
  -webkit-mask-image: linear-gradient(to left, #000 46%, rgba(0,0,0,.22) 82%, transparent 99%);
}
.hero__inner { position: relative; z-index: 2; width: 100%; }

.hero__title { margin-bottom: 34px; }
.hero__title em {
  font-style: italic;
  font-family: var(--f-display);
  color: var(--teal);
}

.hero__foot {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: clamp(28px, 4vw, 64px);
  align-items: end;
  padding-top: 34px;
  border-top: 1px solid var(--rule);
}
.hero__actions { display: flex; gap: 12px; flex-wrap: wrap; }

/* running spec readout under the hero — the machine half of the brand */
.specs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid var(--rule);
  margin-top: clamp(38px, 5vw, 60px);
}
.specs__item {
  padding: 26px clamp(14px, 2vw, 28px) 4px 0;
  border-right: 1px solid var(--rule);
}
.specs__item:last-child { border-right: 0; }
.specs__n {
  font-family: var(--f-display);
  font-size: clamp(1.9rem, 3.1vw, 2.7rem);
  line-height: 1;
  letter-spacing: -0.02em;
  display: block;
  margin-bottom: 10px;
}
.specs__l { font-family: var(--f-mono); font-size: 0.58rem; letter-spacing: 0.19em; text-transform: uppercase; color: var(--ink-mute); line-height: 1.6; }

/* ---------- MARQUEE (capability ticker) ---------- */
.ticker {
  contain: content;
  background: var(--basalt);
  color: var(--on-dark);
  padding-block: 17px;
  overflow: hidden;
  white-space: nowrap;
  position: relative;
}
.ticker__row {
  display: inline-flex; align-items: center;
  animation: ticker 46s linear infinite;
  will-change: transform;
}
/* Paused only when it is genuinely off screen, never because of scrolling. */
.ticker.is-rest .ticker__row { animation-play-state: paused; }
@media (max-width: 760px) {
  .ticker__row { animation-duration: 32s; }
}
.ticker:hover .ticker__row { animation-play-state: paused; }
.ticker__item {
  font-family: var(--f-mono);
  font-size: 0.66rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--on-dark-mute);
  padding-inline: 26px;
  display: inline-flex; align-items: center; gap: 26px;
}
.ticker__item::after {
  content: ""; width: 4px; height: 4px; border: 1px solid var(--gold-2);
  transform: rotate(45deg); flex: none;
}
@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* ---------- PILLARS ----------
   The three cards sit directly over the mandala plate. They used to be opaque
   (`background: var(--sand)`, with the 1px gaps showing a `--rule` backdrop
   through), which bit a hard rectangle out of the middle of the drawing: the
   circle was visible above the row and below it and simply absent between the
   two hairlines. It read as a cropped animation, because it was one.

   The cards are transparent now and the hairlines are drawn as real borders
   rather than as a background showing through gaps, so the plate runs behind
   all three unbroken. The hover wash is a translucent tint for the same
   reason — an opaque one would put the crop back on whichever card the
   pointer is over. */
.pillars {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0;
  background: none;
  border-block: 1px solid var(--rule);
}
.pillar {
  background: none;
  padding: clamp(30px, 3.4vw, 46px) clamp(22px, 2.4vw, 36px);
  position: relative;
  transition: background-color .5s var(--ease);
}
.pillar + .pillar { border-left: 1px solid var(--rule); }
.pillar:hover { background-color: rgba(247, 241, 232, .55); }
.pillar__i { font-family: var(--f-mono); font-size: 0.56rem; letter-spacing: 0.22em; color: var(--gold-ink); display: block; margin-bottom: 22px; }
.pillar__t { font-family: var(--f-display); font-size: clamp(1.25rem, 1.65vw, 1.5rem); margin-bottom: 14px; }
.pillar__d { font-size: 0.93rem; color: var(--ink-soft); line-height: 1.7; }

/* ---------- COLLECTIONS RAIL ---------- */
.rail-shell { position: relative; }
.rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: clamp(268px, 25vw, 372px);
  gap: 20px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-padding-inline-start: clamp(20px, 4.5vw, 68px);
  padding-bottom: 22px;
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* The strip is its own paint and layout island. Without this, scrolling it
     invalidates against the rest of the band. */
  contain: paint;
}
.rail::-webkit-scrollbar { display: none; }
/* Snapping and a programmatic smooth scroll are two things steering the same
   scroller. Press an arrow and the browser animates toward the target while
   the snap engine keeps grabbing the nearest card and re-aiming — the strip
   crawls, overshoots, then jerks back into place, which is the "extreme lag"
   on the arrows. The arrows already land exactly on a card edge, so snap is
   simply lifted for the length of the animation and restored once it lands.
   Dragging and flicking still snap, untouched. */
.rail.is-railing { scroll-snap-type: none; }
.rail-bleed {
  padding-inline: clamp(20px, 4.5vw, 68px);
  margin-inline: calc(-1 * clamp(20px, 4.5vw, 68px));
}

.card {
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  background: var(--marble);
  position: relative;
  transition: transform .6s var(--ease);
}
.card:hover { transform: translateY(-6px); }
.card__media { position: relative; overflow: hidden; }
.card__media .plate { transition: transform .7s var(--ease); }
.card:hover .card__media .plate { transform: scale(1.045); }
.card__no {
  position: absolute; top: 14px; left: 14px; z-index: 5;
  font-family: var(--f-mono); font-size: 0.55rem; letter-spacing: 0.2em;
  color: rgba(255,255,255,.8); text-shadow: 0 1px 6px rgba(0,0,0,.5);
}
.card__body { padding: 24px 24px 28px; display: flex; flex-direction: column; flex: 1; }
.card__t { font-family: var(--f-display); font-size: 1.35rem; margin-bottom: 11px; line-height: 1.16; }
.card__d { font-size: 0.9rem; color: var(--ink-soft); line-height: 1.68; margin-bottom: 22px; }
.card__k { font-family: var(--f-mono); font-size: 0.55rem; letter-spacing: 0.17em; text-transform: uppercase; color: var(--gold-ink); }

.rail-nav { display: flex; gap: 8px; }
.rail-btn {
  width: 44px; height: 44px;
  border: 1px solid var(--rule);
  display: grid; place-items: center;
  transition: background .4s, border-color .4s, color .4s;
}
.rail-btn:hover { background: var(--teal); border-color: var(--teal); color: var(--marble); }
.rail-btn[disabled] { opacity: .3; pointer-events: none; }

/* ---------- WORK (portfolio) ---------- */
.works { display: grid; grid-template-columns: repeat(12, 1fr); gap: clamp(20px, 2.6vw, 40px); }
.work { position: relative; display: block; }
.work__media { position: relative; overflow: hidden; }
/* A photograph filling a media slot. The slot decides the crop through its own
   aspect-ratio and the plate covers it, so a portrait original can sit in a
   landscape slot without being squashed. */
.work__media .plate,
.card__media .plate {
  position: absolute; inset: 0;
  display: block;
  background: var(--stone-150);
}
.work__media .plate img,
.card__media .plate img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.work__media .plate { transition: transform .7s var(--ease); }
.work:hover .work__media .plate { transform: scale(1.05); }
.work__veil {
  position: absolute; inset: 0; z-index: 4;
  background: linear-gradient(to top, rgba(20,18,15,0.72) 0%, rgba(20,18,15,0.14) 42%, transparent 70%);
  opacity: .85; transition: opacity .6s;
}
.work:hover .work__veil { opacity: 1; }
.work__cap { position: absolute; left: 0; bottom: 0; z-index: 5; padding: clamp(20px, 2.3vw, 34px); width: 100%; }
.work__meta {
  font-family: var(--f-mono); font-size: 0.55rem; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--gold-2); margin-bottom: 12px; display: flex; gap: 14px; flex-wrap: wrap;
}
.work__t { font-family: var(--f-display); color: var(--marble); font-size: clamp(1.35rem, 2.2vw, 2rem); line-height: 1.1; margin-bottom: 8px; }
.work__s { color: rgba(247,242,233,.72); font-size: 0.9rem; max-width: 44ch; }
.work__line { height: 1px; background: var(--gold-2); width: 0; transition: width .8s var(--ease); margin-top: 16px; }
.work:hover .work__line { width: 72px; }

.w-7 { grid-column: span 7; } .w-5 { grid-column: span 5; }
.w-6 { grid-column: span 6; } .w-12 { grid-column: span 12; }
.w-8 { grid-column: span 8; } .w-4 { grid-column: span 4; }

/* ---------- SPLIT FEATURE (offset, never 50/50) ---------- */
.feature { position: relative; }
.feature__media { grid-column: 1 / span 7; position: relative; }
.feature__text  { grid-column: 8 / span 5; align-self: center; }
.feature--flip .feature__media { grid-column: 6 / span 7; grid-row: 1; }
.feature--flip .feature__text  { grid-column: 1 / span 4; grid-row: 1; }
.feature__stamp {
  position: absolute; z-index: 6;
  background: var(--teal); color: var(--marble);
  padding: 18px 22px;
  right: -18px; bottom: 34px;
  max-width: 210px;
}
.feature__stamp .mono { font-size: 0.55rem; color: var(--gold-2); display: block; margin-bottom: 7px; }
.feature__stamp p { font-size: 0.86rem; line-height: 1.5; }

/* ---------- PROCESS (the hybrid: machine then hand) ---------- */
.process { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; background: var(--rule-dark); }
.proc {
  background: var(--basalt);
  padding: clamp(30px, 3.6vw, 52px);
  position: relative;
}
.proc__k { font-family: var(--f-mono); font-size: 0.56rem; letter-spacing: 0.24em; text-transform: uppercase; color: var(--gold-2); margin-bottom: 26px; display: block; }
.proc__t { font-family: var(--f-display); font-size: clamp(1.5rem, 2.3vw, 2.05rem); color: var(--on-dark); margin-bottom: 16px; line-height: 1.12; }
.proc__d { color: var(--on-dark-mute); font-size: 0.95rem; line-height: 1.75; }
.proc__list { margin-top: 26px; }
.proc__list li {
  font-family: var(--f-mono); font-size: 0.6rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--on-dark-mute); padding: 11px 0; border-top: 1px solid var(--rule-dark);
  display: flex; justify-content: space-between; gap: 14px;
}
.proc__list li span:last-child { color: var(--gold-2); }

/* Light variant — flips the dark process cards to sit on a sand background
   so the section reads as content, not as part of the dark footer below. */
.process--light { background: var(--rule); }
.process--light .proc { background: var(--marble); }
.process--light .proc__k { color: var(--accent); }
.process--light .proc__t { color: var(--ink); }
.process--light .proc__d { color: var(--ink-soft); }
.process--light .proc__list li { color: var(--ink-mute); border-top-color: var(--rule); }
.process--light .proc__list li span:last-child { color: var(--accent); }

/* ---------- TIMELINE ---------- */
.tl { position: relative; }
.tl__axis { position: absolute; left: 0; top: 8px; bottom: 8px; width: 1px; background: var(--rule); }
.tl__item { position: relative; padding-left: clamp(34px, 5vw, 74px); padding-bottom: clamp(42px, 5vw, 68px); }
.tl__item:last-child { padding-bottom: 0; }
.tl__dot {
  position: absolute; left: -4px; top: 9px;
  width: 9px; height: 9px; background: var(--sand);
  border: 1px solid var(--teal); transform: rotate(45deg);
}
.tl__item--now .tl__dot { background: var(--teal); }
.tl__yr { font-family: var(--f-mono); font-size: 0.62rem; letter-spacing: 0.22em; color: var(--gold-ink); display: block; margin-bottom: 12px; }
.tl__t { font-family: var(--f-display); font-size: clamp(1.35rem, 2vw, 1.8rem); margin-bottom: 12px; line-height: 1.14; }
.tl__d { color: var(--ink-soft); font-size: 0.98rem; line-height: 1.75; max-width: 58ch; }

/* ---------- STAT BAND ---------- */
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--rule-dark); }
.stat { background: var(--basalt); padding: clamp(28px, 3.2vw, 44px) clamp(20px, 2.4vw, 32px); }
.stat__n { font-family: var(--f-display); font-size: clamp(2rem, 3.4vw, 3rem); line-height: 1; color: var(--marble); display: block; margin-bottom: 14px; }
.stat__l { font-family: var(--f-mono); font-size: 0.57rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--on-dark-mute); line-height: 1.6; }

/* Light variant — sits on sand, clearly separate from the dark footer. */
.stats--light { background: var(--rule); }
.stats--light .stat { background: var(--marble); }
.stats--light .stat__n { color: var(--accent); }
.stats--light .stat__l { color: var(--ink-mute); }

/* ---------- MARKETS ---------- */
.markets { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--rule); border: 1px solid var(--rule); }
.market { background: var(--sand); padding: clamp(24px, 2.8vw, 38px); transition: background .5s; }
.market:hover { background: var(--marble); }
.market__c { font-family: var(--f-display); font-size: 1.3rem; margin-bottom: 9px; }
/* overflow-wrap, because these are single long words set in spaced-out mono.
   "ARCHITECTURAL" comes to 125px at the size phones get and its column is
   111px, so with no space to break on it simply hung over the edge of the
   panel. Ordinary wrapping cannot help a word that contains no break
   opportunity; this gives it one. */
.market__d { font-family: var(--f-mono); font-size: 0.56rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-mute); line-height: 1.7; overflow-wrap: anywhere; }

/* ---------- JOURNAL ---------- */
.post {
  border-top: 1px solid var(--rule);
  display: grid;
  grid-template-columns: 90px minmax(0, 1fr) 200px;
  gap: clamp(20px, 3vw, 46px);
  padding-block: clamp(28px, 3.2vw, 44px);
  align-items: start;
  transition: background .5s;
}
.post:last-of-type { border-bottom: 1px solid var(--rule); }
.post__n { font-family: var(--f-mono); font-size: 0.6rem; letter-spacing: 0.2em; color: var(--gold-ink); padding-top: 7px; }
/* An entry's photograph or film.
   Journal entries could always be given a cover in the panel; until the entries
   themselves were rendered from the database there was nowhere for one to go.
   It sits above the headline inside the middle column, so the entry reads
   picture-then-title the way a magazine page does, and the aspect ratio is set
   inline from the entry's own setting — which means the row never resizes once
   the image arrives. */
.post__cover {
  position: relative;
  overflow: hidden;
  margin-bottom: clamp(14px, 1.6vw, 20px);
  background: var(--stone-100, rgba(0, 0, 0, .05));
}
/* Fitted, not filled.
   The slot's ratio comes from the entry's own coverAspect setting and falls
   back to 16/9, so it has nothing to do with the shape of the file posted to
   it. Under `cover` a portrait photograph on a default entry lost about half
   of itself before it was ever seen. The ratio still governs the LAYOUT — the
   row keeps its shape and nothing shifts when the file arrives — it just no
   longer governs how much of the picture survives. The margin falls on the
   slot's own stone ground, which is already set above. */
.post__cover img,
.post__cover video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
  display: block;
}
.post__t { font-family: var(--f-display); font-size: clamp(1.35rem, 2.1vw, 1.85rem); line-height: 1.18; margin-bottom: 12px; }
.post__x { color: var(--ink-soft); font-size: 0.96rem; line-height: 1.72; max-width: 62ch; }
.post__side { display: flex; flex-direction: column; gap: 14px; align-items: flex-start; }
.post__tag { font-family: var(--f-mono); font-size: 0.55rem; letter-spacing: 0.17em; text-transform: uppercase; color: var(--ink-mute); }
.post__full {
  display: none;
  grid-column: 2 / 3;
  padding-top: 22px;
  border-top: 1px solid var(--rule-soft);
  margin-top: 22px;
}
.post.is-open .post__full { display: block; }
.post__full h3 {
  font-family: var(--f-display); font-size: 1.2rem; margin: 26px 0 10px; color: var(--teal);
}
.post__full p { color: var(--ink-soft); line-height: 1.78; font-size: 0.96rem; }
.post__full p + p { margin-top: 1em; }
.post__full ul { margin-top: 14px; }
.post__full li {
  padding-left: 20px; position: relative; color: var(--ink-soft);
  font-size: 0.95rem; line-height: 1.72; margin-bottom: 9px;
}
.post__full li::before {
  content: ""; position: absolute; left: 0; top: 11px;
  width: 5px; height: 5px; border: 1px solid var(--gold-ink); transform: rotate(45deg);
}
.post__toggle .lbl-more { display: inline; }
.post__toggle .lbl-less { display: none; }
.post.is-open .post__toggle .lbl-more { display: none; }
.post.is-open .post__toggle .lbl-less { display: inline; }

/* ---------- FAQ / ACCORDION ---------- */
.acc__item { border-top: 1px solid var(--rule); }
.acc__item:last-child { border-bottom: 1px solid var(--rule); }
.acc__btn {
  width: 100%; text-align: left;
  display: flex; justify-content: space-between; align-items: center; gap: 24px;
  padding-block: clamp(20px, 2.2vw, 28px);
  font-family: var(--f-display); font-size: clamp(1.08rem, 1.5vw, 1.32rem);
  transition: color .35s;
}
.acc__btn:hover { color: var(--teal); }
.acc__sign { position: relative; width: 13px; height: 13px; flex: none; }
.acc__sign::before, .acc__sign::after {
  content: ""; position: absolute; background: var(--teal); transition: transform .45s var(--ease);
}
.acc__sign::before { left: 0; top: 6px; width: 13px; height: 1px; }
.acc__sign::after  { left: 6px; top: 0; width: 1px; height: 13px; }
.acc__item.is-open .acc__sign::after { transform: rotate(90deg); }
.acc__panel { max-height: 0; overflow: hidden; transition: max-height .6s var(--ease); }
.acc__inner { padding-bottom: 26px; color: var(--ink-soft); line-height: 1.75; max-width: 68ch; font-size: 0.96rem; }

/* ---------- CONTACT FORM ---------- */
.form { display: grid; grid-template-columns: repeat(2, 1fr); gap: 22px; }
.field { display: flex; flex-direction: column; gap: 9px; }
.field--full { grid-column: 1 / -1; }
.field label { font-family: var(--f-mono); font-size: 0.57rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--ink-mute); }
.field input, .field select, .field textarea {
  font-family: var(--f-body); font-size: 0.97rem; color: var(--ink);
  background: transparent; border: 0; border-bottom: 1px solid var(--rule);
  padding: 11px 0; transition: border-color .4s;
  border-radius: 0; -webkit-appearance: none; appearance: none;
}
.field select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%237C7264' fill='none' stroke-width='1'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 2px center; }
.field textarea { resize: vertical; min-height: 116px; }
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-bottom-color: var(--teal); }
.field input::placeholder, .field textarea::placeholder { color: var(--ink-mute); opacity: .55; }
.form__note { font-family: var(--f-mono); font-size: 0.55rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--ink-mute); line-height: 1.8; }
.form__msg { grid-column: 1 / -1; font-size: 0.9rem; color: var(--teal); display: none; }
.form__msg.is-on { display: block; }

/* ---------- CONTACT DETAIL LIST ---------- */
.cdl__item { border-top: 1px solid var(--rule); padding-block: 20px; }
.cdl__item:last-child { border-bottom: 1px solid var(--rule); }
.cdl__k { font-family: var(--f-mono); font-size: 0.55rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--ink-mute); display: block; margin-bottom: 7px; }
.cdl__v { font-size: 1rem; color: var(--ink); }

/* ---------- PAGE HEADER (inner pages) ---------- */
.phead {
  position: relative;
  padding-top: clamp(140px, 16vw, 210px);
  padding-bottom: clamp(48px, 6vw, 88px);
  overflow: hidden;
  background:
    radial-gradient(80% 100% at 88% 0%, rgba(22,84,79,0.10) 0%, transparent 62%),
    var(--sand);
}
.phead__art { position: absolute; pointer-events: none; z-index: 0; }
/* A page-head drawing lives in the right margin, clear of the crumb, headline
   and lede — which is handled by where it is placed, not by fading it out.
   An earlier version faded the left 46% of every plate to guarantee it could
   never touch text. It guaranteed that, and it also meant half of every
   drawing was a ghost: the mandala on Collections looked like it had been
   rubbed out down one side. Only the outer edge softens now, so the plate
   stops at a soft margin instead of a cut line, and the drawing itself stays
   at full strength. */
.phead__art {
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 13%) !important;
  mask-image: linear-gradient(to right, transparent 0%, #000 13%) !important;
}
.phead__crumb { font-family: var(--f-mono); font-size: 0.57rem; letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-mute); margin-bottom: 26px; display: flex; gap: 10px; align-items: center; }
.phead__crumb a { transition: color .3s; }
.phead__crumb a:hover { color: var(--teal); }

/* ---------- CTA BAND ---------- */
.cta { background: var(--teal); color: var(--marble); position: relative; overflow: hidden; }
.cta__inner { position: relative; z-index: 2; display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 40px; align-items: end; }
.cta h2 { color: var(--marble); }
.cta p { color: rgba(247,242,233,.76); }
.cta .btn { border-color: rgba(247,242,233,.4); color: var(--marble); }
.cta .btn::before { background: var(--marble); }
.cta .btn:hover { color: var(--teal); border-color: var(--marble); }
.cta__watermark {
  position: absolute; right: -4%; top: 50%; transform: translateY(-50%);
  font-family: var(--f-display); font-size: clamp(9rem, 22vw, 22rem);
  color: rgba(247,242,233,.05); line-height: .8; z-index: 1; pointer-events: none;
  user-select: none; white-space: nowrap;
}

/* ---------- RESPONSIVE ---------- */
@media (max-width: 1100px) {
  /* Two across, not one.
     This rule used to read `span 12`, which made every card on the homepage's
     editorial grid full-width on every screen below 1100px. Five commissions,
     each a 3:2 photograph with a caption under it, one after another: about
     1,750px of vertical stack on a phone for a section that is 1,880px tall on
     a laptop while showing the same five pieces arranged. That is the single
     biggest reason the page ran on and on — the arrangement was not adapting
     to the screen, it was being abandoned at it.

     At six columns each the mix of 7/5 and 6/6 still resolves into pairs, so
     the section keeps its rhythm and loses roughly half its height. */
  .works .w-7, .works .w-5, .works .w-6, .works .w-8, .works .w-4 { grid-column: span 6; }
  .feature__media, .feature__text,
  .feature--flip .feature__media, .feature--flip .feature__text { grid-column: 1 / -1; grid-row: auto; }
  .feature__stamp { position: static; max-width: none; margin-top: 18px; }
  .specs { grid-template-columns: repeat(2, 1fr); }
  .specs__item:nth-child(2) { border-right: 0; }
  .specs__item:nth-child(3), .specs__item:nth-child(4) { border-top: 1px solid var(--rule); }
  .stats, .markets { grid-template-columns: repeat(2, 1fr); }
  .hero__art { right: -34%; opacity: .3; }
}
@media (max-width: 900px) {
  /* Stacked: the separator moves from the side of each card to the top of it,
     otherwise the column rule hangs off a single-column list. */
  .pillars { grid-template-columns: 1fr; }
  .pillar + .pillar { border-left: 0; border-top: 1px solid var(--rule); }
  /* .process is NOT collapsed here any more — it holds two panels, and two
     panels sit side by side perfectly well down to a large phone. Stacking
     them from 900px meant a tablet, and a phone held landscape, got a single
     column and twice the scrolling for no reason. It stacks at 620px instead,
     where each panel's internal key/value rows would genuinely start to
     crowd. */
  .hero__foot { grid-template-columns: 1fr; }
  .cta__inner { grid-template-columns: 1fr; }
  .post { grid-template-columns: 1fr; gap: 14px; }
  .post__n { padding-top: 0; }
  .post__full { grid-column: 1 / -1; }
  .form { grid-template-columns: 1fr; }
}
/* Inline grid spans are set per-section for editorial control on desktop.
   Below tablet they must all collapse, so these override the inline style. */
@media (max-width: 1100px) {
  .process[style*="repeat(3"],
  .process[style*="repeat(4"] { grid-template-columns: repeat(2, 1fr) !important; }
}
@media (max-width: 900px) {
  .grid > [style*="grid-column"] { grid-column: 1 / -1 !important; }
  /* An inline-styled process of three or four panels comes down to two here
     rather than to one, for the same reason as above. */
  .process[style*="grid-template-columns"] { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
  .grid > * + * { margin-top: 4px; }
}

@media (max-width: 620px) {
  .stats, .markets { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 16px; }

  /* The selected-work grid on a phone.
     Two tiles a row, with the lead commission held at full width — a grid of
     identical tiles has no hierarchy, and the first piece is the one that
     should be seen properly. The 12-column track is dropped for a 2-column one
     so the span classes cannot fight it. */
  .works {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }
  .works > .w-7, .works > .w-5, .works > .w-6,
  .works > .w-8, .works > .w-4, .works > .w-12 { grid-column: span 1; }

  /* The first and last tiles take the full width.
     Five tiles in a two-column flow leaves the last one alone in its row with
     an empty half beside it — a hole, not a composition. Opening both ends
     gives the section a shape instead: a lead commission, a pair in the
     middle, and a closing piece. It also restores the hierarchy the editorial
     grid has on a laptop, where the tiles are deliberately not all equal. */
  .works > .work:first-child,
  .works > .work:last-child { grid-column: span 2; }

  /* A caption under a 170px tile has to be smaller than one under a 350px
     tile, or the title wraps to three lines and the tile is mostly type. The
     two full-width tiles keep the full-size caption. */
  .works > .work:not(:first-child):not(:last-child) .work__t,
  .works > .w-5 .work__t { font-size: 0.98rem; line-height: 1.25; }
  .works > .work:not(:first-child):not(:last-child) .work__s,
  .works > .w-5 .work__s { font-size: 0.52rem; letter-spacing: 0.12em; }

  /* Now the panels stack — this is where their internal key/value rows would
     genuinely start to crowd, rather than at 900px. */
  .process,
  .process[style*="grid-template-columns"] { grid-template-columns: 1fr !important; }
  /* The three figures under the hero stay a row.
     They were stacked one per line here, which turned "7th / 4 / 1990" — three
     words and three numbers — into roughly 300px of scrolling directly beneath
     the headline, and read as a list of unrelated facts rather than a spec
     strip. Side by side they are one statement, they are the shape this block
     has on a laptop, and they cost about 90px instead of 300px. Each column is
     roughly 115px on a 390px screen, which the mono label wraps into happily. */
  .specs { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .specs__item {
    border-top: 0;
    border-right: 1px solid var(--rule);
    padding: 18px clamp(8px, 2.6vw, 14px) 2px 0;
  }
  .specs__item:last-child { border-right: 0; }
  .specs__l { font-size: 0.54rem; letter-spacing: 0.14em; }

  /* The footer stacked five blocks in one column and ran to 1,486px — nearly
     two full screens of link list after the page had already ended. The four
     link columns sit two-up; the brand block keeps the full width. */
  /* grid-column is deliberately not set here any more — the footer block in
     site.css pairs the columns two-up at this width. This rule dates from
     the folding accordion, where one column per row was correct because
     each was a single tappable line. The folding is off; the stacking went
     with it. min-width stays: it lets a long link wrap inside its column
     instead of widening the track. */
  .foot__col { min-width: 0; }

  /* Folded columns (site.js adds .is-folded on phones). The heading becomes
     the control and carries a chevron; the list collapses to nothing. */
  .foot__col[data-foldable] { border-top: 1px solid var(--rule-dark); padding-top: 12px; }
  .foot__col[data-foldable] .foot__head {
    cursor: pointer; display: flex; align-items: center; justify-content: space-between;
    margin-bottom: 0; padding: 6px 0; user-select: none;
  }
  .foot__col[data-foldable] .foot__head::after {
    content: ""; width: 7px; height: 7px; flex: none; margin-left: 12px;
    border-right: 1px solid currentColor; border-bottom: 1px solid currentColor;
    transform: rotate(45deg) translateY(-2px);
    transition: transform .35s var(--ease);
  }
  .foot__col[data-foldable].is-folded .foot__head::after { transform: rotate(-45deg) translateY(1px); }
  .foot__col[data-foldable] .foot__list {
    overflow: hidden; max-height: 60vh;
    transition: max-height .45s var(--ease), opacity .35s;
    opacity: 1;
  }
  .foot__col[data-foldable].is-folded .foot__list { max-height: 0; opacity: 0; }

  /* Sections were carrying desktop-sized air on a phone. */
  .band { padding-block: clamp(38px, 9vw, 52px); }
  .band--tight { padding-block: clamp(30px, 7vw, 42px); }
}

/* ==========================================================================
   THE MARK
   The Stonegraph symbol, used the way a foundry uses its punch: in the
   masthead, and as a small repeated marker before section labels.
   ========================================================================== */
.brand { display: flex; align-items: center; gap: 12px; }
.brand__logo {
  width: 34px; height: 38px;
  object-fit: contain;
  flex: none;
  transition: transform .6s var(--ease), opacity .4s;
}
.brand:hover .brand__logo { transform: translateY(-2px); }
.brand__text { display: flex; flex-direction: column; gap: 2px; }
.brand__mark { font-size: 1.24rem; letter-spacing: .14em; line-height: 1; }
.brand__sub  { font-size: .5rem; letter-spacing: .28em; }
.foot .brand__logo { filter: invert(1) brightness(1.6) saturate(0); opacity: .8; }

/* Small mark before a section label — replaces the plain rule */
:root { --mark-src: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAAwCAYAAACMuVOlAAAIAElEQVR42sWZfZCVZRnGf+c9h2VZWBSWlhYXUJAARVkSBgFLydaiDEEwm1Fg/KipjLQsonTSySawxtScyBmYaXJKKzTHGlGkLxHBSkNEwSVgS0KW/IAFFmE/zumf3+M8vHPOstkR35l3znve83zcz31f93Xdz3Og/NeHgaScA5ZrsIyfpwEfBfLlNDQp4zgJMAH4FNA/Zfx7bmQGyOq9A0CFn4VyOSFbJuO6gDOB7wIHff+8hoZ2BU7wFXtoIHAN8CxwH1AF/BVYDUwtgd0TZuApwI+BvUAb8H2gTkOqgd8AR4HfAxcBuRNlaDBwuuHcCzwMnBdNHnt5MvBzjV0FDLNd8m4ZmPGuAbYCfwamlAhn2pBpQDOwpAy50O3Vy8FnAw8Bs1Lvi+Eu8XeAS0yeMyPKetcSZSlwvc+5HiZGWMR64PzoXaZcPJlIMyOBz/j5mr+laaUAjC0ybqCiNuCrtqGntJT00MAbzeQFwGBgqJMWUh4cDiy2TzEvPQOMA/7keHX/b7aHEC0EOoHlZnHfIiGu8HkJ0G6iJEXgEBZ+PvBr4Cm1/h2pX1jd+4FdwB/14PESayPwT+AXxxkXSX8n8LN3amTOTlcAhyLlqCgy4Rhgns/bNPIPhnWeGE4bWBGVdbuESaZU6JNuVpwH6oFHzMrEUMZ9c3pjBnC/Rg4D+gEXAPuB3baLDejw+7OO009853pqZOIgjSZIs+/Sq+wSqx3iq1EsHrB9NfAScMR2+VS2F1Sit4CvAAMi43uU7dcDW4DNFg9JytAhwJeBQX6vB16wsLgfeAz4u9l7sp5eoOGxevUCrgPetP2k4+EzZPM5hmmlk6fxlDUsV4q/G31/G7DWYmIdsAg4A9gOtKj3pZRmoKFviqDRLT5/KPBP70ZVQttXgFfN0BzwaWAi8Am9vNPE+Gk3HgqyOdKx5qX1PYkmLgC9gYuBX+qBrHhKq0fA173AYWA+8EEVZQmwCbjB8L5lQpHCZZxEOWCHFDYz7ckkNXkigF9MKUrsxd4a8CNghJhrE/xrNToLzNGTAw31De6Bst0wynojEDvibSMToNaJDwJvFDEw8d0Is/hqP1cphfXejwOV6vsG8VgJ3ARcC5xaIvQF23YA42NpTSI8PAlcKuX0KwLc0GkrcJle6QIagP+YaNNVkml+/xAwWtydayY3Fwl9GHuz8HpC1aqKDTgH+LcUkCuRUBP0Qhbo47s64Bb3NuOArwMPAt9Tpe4Fvi3Bo0dDITKqSKEc5rtNCowVjvGu8OQilXN4vgq4vIQQzLCymStBzzQJ5pRov0jYpOktZPrHjWwu7nhE144ukjDh+WngLuCO6JQCvbpGnusD3KyXNgG/i0JWAC5UZucqlxSBVBizLo3bBjsNSilCNroBviButmrAWVH7/ha0AMtSteJUy7Ij9p+bGj9JRe0y4B8h3OHHTvHSL2pcMDHCndML9SZKpdvYybY9AOzz+xZgj+8vl8wrHOOQns9GY+dT3HjUCCSxmuSA1kiSOsXneJ+3aEBGzT7DexjwRSlrh7ieLj6xSL5ZmX3VHWaThXOrCz7dpN0ehTuxTy+9D054WMyELega4HU99Nvot/hqBB6QMzMWHgsMfZU0UqrvUiWz4NzLouyf5dzvizuMVTUuNAxNqsdNkvVrwL/kuZCFITQ1Us0UJ2nw/eesiAakthkZYIXG7fHw4C7gZeBW214M/AU4KTZynDgbLlduMNThWqwGv2iIwn46UMYHoqQ5Vc1+PKKZisjAmY61JqqgcN4WtykzlMhjEqdDOazS9a1SSK0TLvVMp1oM5u3Toee3idlGPT7HneFOcd7uXXBjt8PsvUO6qfFdVsNajewxRvZyBVmr6WYroSVRMv3ETue6r5kt6ENmPqEXEyOzItqGjDHLz4vebfA5B3zDzd7D4vQUdwXt6XB3GLZwTYx2iIkGPqcxj+ixmmgiLCCGA59NbXXDEcsK+z+lY4KTapXdcF0hFx+zbTlbDw4tIotxkXuWZPyy+FkYLSJjgbLY2jI2YL0YbBICE1L9SBXYs63us+mid5Sey0Sey0YymdXAmVbu64BvmZ21ttluSA8Z0skebp2kcY8BH1HX49PfbOr8sk36qUhXY5+MZLGn1wiT6k6TarBVTzbK1luExf9y9VUUcsHqB/1r40mtrzPLN7qLm+SK31DfqwX2SvseEb+zTLQr5ceHLN02eawyw0Xt1xkDoq3x0yZhzveHZZsaYF9GoM8HvqM6vGnJ3xKJ/0EXcppJhkS8Tbq4zgHH+f5sk6MN+JXf09deF9wqm9RHIX7d+ZcDq+J4X6SM3ep5d5UN+3ga0Sk9NLqVHep/Ns1K50oZYmrEsXd7wD9E3d6tN1tVlLzE3l+eHQ18yYPWadYMb1ceOSf4pqEbY+eXbPioq24w1LucpK/emqj+/0DtXmZSHZC2KvXmWPnwFeBvwMfcoNVK7ruNRAtwjzTVmf5Ha6BY3B8dheTlrPl6uBAtaKlE/IyebBBbC13gA3pmn3cYc7nVUSjxunzea60wqLs/ACrFYbUlWd7jj2Y9NlgpK0hBq02eq6Wd5/ToHo1aZIm2Q4Vp19tTHKPJBXYBXzPsvaNqqOi/AJ12bjecTRL0UXG42dOKKWbsSAfuEGejPNOcZP/DJlte41cb3kuiynww8Hm9ezQ6CDvu33YBpy94YpZ1dc0S832RLA7RuLVm6EQxt1HM9XbR9wC3i9kWqWaz0FjnfEXP0P8Li6IfeGmLkHYAAAAASUVORK5CYII="); }

.eyebrow--mark::before {
  content: "";
  width: 15px; height: 17px;
  background: currentColor;
  -webkit-mask: var(--mark-src) center / contain no-repeat;
  mask: var(--mark-src) center / contain no-repeat;
  opacity: .85;
}

/* ==========================================================================
   PROJECT CARD
   For a named commission: badge, date, title, one line, and a description.
   Media sits in a fixed frame so an uploaded photo can never break the grid.
   ========================================================================== */
.pcard {
  display: flex;
  flex-direction: column;
  background: var(--marble);
  border: 1px solid var(--rule-soft);
  position: relative;
  overflow: hidden;
  transition: transform .7s var(--ease), border-color .5s, box-shadow .7s var(--ease);
}
.pcard:hover {
  transform: translateY(-5px);
  border-color: var(--rule);
  box-shadow: 0 26px 60px -30px rgba(36, 31, 24, .38);
}
.pcard__media { position: relative; overflow: hidden; background: var(--stone-200); }
.pcard__media .plate,
.pcard__media img,
.pcard__media video { transition: transform .7s var(--ease); }
.pcard:hover .pcard__media .plate,
.pcard:hover .pcard__media img,
.pcard:hover .pcard__media video { transform: scale(1.04); }

.pcard__badges {
  position: absolute; top: 13px; left: 13px; z-index: 6;
  display: flex; gap: 6px; flex-wrap: wrap;
}
.pcard__badge {
  font-family: var(--f-mono); font-size: .53rem; letter-spacing: .16em;
  text-transform: uppercase; padding: 5px 9px;
  /* Same reasoning as .piece__more: these sit over photographs on the
     portfolio wall and are always visible, so the blur ran for the whole
     length of every scroll. A slightly more opaque chip reads identically. */
  background: rgba(30, 26, 20, .9); color: var(--on-dark);
}
.pcard__badge--live { background: var(--accent); color: var(--marble); }

.pcard__body { padding: 24px 24px 26px; display: flex; flex-direction: column; flex: 1; }
.pcard__meta {
  font-family: var(--f-mono); font-size: .55rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-mute);
  display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 13px;
}
.pcard__t { font-family: var(--f-display); font-size: 1.42rem; line-height: 1.14; margin-bottom: 8px; }
.pcard__sub { font-size: .88rem; color: var(--accent); margin-bottom: 13px; }
.pcard__d { font-size: .92rem; color: var(--ink-soft); line-height: 1.7; margin-bottom: 22px; }

/* ==========================================================================
   MEDIA FRAME
   Every uploaded image or video drops into one of these. The frame owns the
   aspect ratio and the crop, so nothing the client uploads can distort the
   layout — portrait, landscape or phone video all sit correctly.
   ========================================================================== */
.frame { position: relative; overflow: hidden; background: var(--stone-200); }
/* An <iframe> has no intrinsic shape to preserve — it is a document, and it
   should fill what it is given. A picture or a film does, so those are fitted
   with the rest of the work; see the rule below this one. */
.frame > img,
.frame > video,
.frame > iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: var(--focus, center);
  display: block; border: 0;
  z-index: 2;
}
.frame--contain > img,
.frame--contain > video { object-fit: contain; background: var(--stone-150); }

/* Pictures and film in a frame are fitted, for the reason given at length in
   the uploaded-film rule: the frame's ratio is authored and the file's is
   whatever it was shot or photographed at, and where the two disagree the work
   is what should survive, not the slot. The iframe above is deliberately not
   included — a document has no shape to keep. */
.frame > img,
.frame > video {
  object-fit: contain;
}
.frame__note {
  position: absolute; inset: 0; z-index: 1;
  display: grid; place-content: center; gap: 8px; text-align: center;
  padding: 20px; color: var(--stone-500);
  font-family: var(--f-mono); font-size: .55rem; letter-spacing: .18em;
  text-transform: uppercase; line-height: 1.9;
}
.frame__note b { display: block; color: var(--ink-mute); font-weight: 500; }

/* ==========================================================================
   CONTACT PANEL
   A single overlay reachable from anywhere, rather than a page you navigate
   away to. Direct channels first, the form second.
   ========================================================================== */
.panel {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  padding: clamp(16px, 4vw, 48px);
  visibility: hidden;
}
.panel.is-open { visibility: visible; }
.panel__veil {
  position: absolute; inset: 0;
  /* A FULL-SCREEN BLUR IS WHY THE PANEL CATCHES AS IT OPENS.
     Blurring the backdrop means the browser must snapshot the whole page
     behind the veil and blur it before the first frame of the fade can be
     shown — on a page of 2,800 nodes and several hundred kilobytes of inline
     SVG, that snapshot is the hitch felt on the tap. The veil then stays
     blurred for as long as the panel is open, so anything moving behind it
     keeps the pass alive.

     The veil is a dimmer, not a lens: raising the alpha from .55 to .72 hides
     the page more completely than the blur did, and costs one composited
     opacity fade, which is free. */
  background: rgba(20, 17, 12, .72);
  opacity: 0; transition: opacity .45s var(--ease);
}
.panel.is-open .panel__veil { opacity: 1; }
.panel__card {
  position: relative;
  width: min(940px, 100%);
  max-height: calc(100vh - clamp(32px, 8vw, 96px));
  overflow-y: auto;
  background: var(--marble);
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  opacity: 0;
  transform: translateY(22px) scale(.985);
  transition: opacity .6s var(--ease), transform .7s var(--ease);
}
.panel.is-open .panel__card { opacity: 1; transform: none; }

.panel__aside { background: var(--stone-900); color: var(--on-dark); padding: clamp(28px, 3.4vw, 42px); }
.panel__aside .display { color: var(--marble); }
.panel__main  { padding: clamp(28px, 3.4vw, 42px); }

.panel__close {
  position: absolute; top: 12px; right: 12px; z-index: 4;
  width: 42px; height: 42px; display: grid; place-items: center;
  color: var(--marble); transition: transform .5s var(--ease), opacity .3s;
}
.panel__close:hover { transform: rotate(90deg); }

/* Direct channels — logo-style clickable rows */
.chan { margin-top: 26px; }
.chan__row {
  display: flex; align-items: center; gap: 14px;
  padding: 13px 0;
  border-top: 1px solid var(--rule-dark);
  color: var(--on-dark-mute);
  transition: color .35s, padding-left .45s var(--ease);
}
.chan__row:last-child { border-bottom: 1px solid var(--rule-dark); }
.chan__row:hover { color: var(--on-dark); padding-left: 7px; }
.chan__icon {
  width: 30px; height: 30px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--rule-dark);
  transition: border-color .35s, background .35s;
}
.chan__row:hover .chan__icon { border-color: var(--accent-dark); background: rgba(194, 170, 126, .12); }
.chan__icon svg { width: 14px; height: 14px; }
.chan__l { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.chan__k { font-family: var(--f-mono); font-size: .5rem; letter-spacing: .22em; text-transform: uppercase; color: var(--stone-400); }
.chan__v { font-size: .9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chan__go { margin-left: auto; opacity: 0; transform: translateX(-5px); transition: all .45s var(--ease); }
.chan__row:hover .chan__go { opacity: 1; transform: none; }

@media (max-width: 820px) {
  .panel__card { grid-template-columns: 1fr; }
  .panel__aside { order: 2; }
}

/* Nav call-to-action */
.btn--nav {
  padding: 11px 18px;
  font-size: .6rem;
  border-color: var(--rule);
  margin-left: 6px;
}
/* Mobile breakpoint moved to 1200px to match the nav panel breakpoint in
   site.css — kept here so the CTA resizes in lockstep with the panel. */
@media (max-width: 1200px) {
  .btn--nav { width: 100%; margin: 28px 0 0; justify-content: center; padding: 18px; font-size: 0.78rem; }
}

/* ==========================================================================
   HERO FILM
   The workshop reel sits where the drawing used to. It plays on arrival,
   muted and looping, and is scrimmed hard on the left so the headline keeps
   its contrast no matter which frame is showing.
   ========================================================================== */
.hero__film {
  position: absolute;
  inset: 0 0 0 auto;
  width: min(72vw, 1080px);
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.hero__film video {
  width: 100%; height: 100%;
  object-fit: cover;
  /* Where the crop is anchored is set per-site from the panel and written onto
     the shell as --focal. Which part of a frame survives a crop is a fact about
     the footage, so it cannot be decided once here for every film the owner
     will ever upload. The fallback is the behaviour that shipped. */
  object-position: var(--focal, center);
  display: block;
}
/* No still layer is allowed in a live hero. This also neutralises stale cached
   HTML while the current renderer emits no poster markup at all. */
.hero__film .hero__film-poster,
.hero__film > picture { display: none !important; }
.hero__film::after {
  content: "";
  position: absolute; inset: 0;
  background:
    linear-gradient(90deg, var(--stone-100) 4%, rgba(233,227,213,.86) 26%, rgba(233,227,213,.34) 55%, rgba(233,227,213,.16) 100%),
    linear-gradient(0deg, rgba(233,227,213,.72) 0%, transparent 34%, transparent 74%, rgba(233,227,213,.62) 100%);
}
.hero--film { background: var(--stone-100); }

/* ==========================================================================
   THE WALL
   A museum hang. Every piece gets an identical frame, because uniformity is
   what makes a wall of very different carvings read as one collection.
   ========================================================================== */
/* Uniform grid rather than masonry columns.
   The wall used to be `columns: 3` which is a masonry layout - each piece
   kept its own aspect ratio and flowed independently in each column, so
   the page read as a ragged mosaic where nothing aligned. The reader sees
   this as 'images going up and down' with no professionalism.

   The wall is now a strict 3-column CSS grid where every tile is forced to
   the same 4/5 portrait aspect ratio. Tiles align in clean horizontal rows,
   and the gallery reads as a museum hang of identical frames. */
.wall {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(12px, 1.6vw, 22px);
  align-items: start;
}
.wall > * {
  margin-bottom: 0;
  display: block;
  width: 100%;
}
.wall--2 { grid-template-columns: repeat(2, 1fr); }
.wall--4 { grid-template-columns: repeat(4, 1fr); }

.piece {
  display: block;
  position: relative;
  background: var(--stone-150);
  overflow: hidden;
  cursor: zoom-in;
  /* Suppresses the iOS and Android long-press link preview. It has to be on
     the anchor: the preview is raised for the LINK, not for the image inside
     it, and it does not go through the contextmenu event protect.js cancels.
     manipulation also removes the 300ms double-tap-to-zoom wait, so opening
     the lightbox feels immediate. */
  -webkit-touch-callout: none;
  touch-action: manipulation;
  /* Transform only. `box-shadow` was transitioned alongside it for .8s, and a
     large blurred shadow cannot be composited — every frame of that transition
     is a repaint of the tile and the area around it. On a wall of sixty-six,
     with tiles passing under the cursor as the page moves, that was a steady
     stream of repaints. The shadow still appears; it just arrives with the
     lift instead of being animated into place, which is indistinguishable at
     this size and free. */
  transition: transform .45s var(--ease);
}
.piece:hover { transform: translateY(-4px); box-shadow: 0 30px 60px -34px rgba(36,31,24,.5); }
.piece__img { position: relative; overflow: hidden; display: block; -webkit-touch-callout: none; }

/* ==========================================================================
   EMPTY SLOTS — a card with no photograph in it does not appear
   --------------------------------------------------------------------------
   `work__media--flat` is the marker render.ts looks for when it places an
   uploaded photograph into a card. A card still carrying one has no media,
   so there is nothing to show and the frame, the caption and the grey ground
   are all there is — which reads as a broken tile rather than an empty one.

   Hidden rather than deleted from the markup, because the marker is also the
   only way media can ever get INTO these cards: they are static, and only
   their slot is filled from the database. Remove the card and the upload
   path goes with it. Hide it, and the card returns by itself the moment a
   photograph is uploaded — the same marker that hides it is the one the
   renderer consumes.

   Where :has() is unsupported none of this matches and the cards show as
   they did before. Content visible is the right way for a rule like this to
   fail; content hidden by a selector the browser could not parse is not. */
.work:has(.work__media--flat),
.card:has(.work__media--flat),
.pcard:has(.work__media--flat) {
  display: none !important;
}

/* The feature blocks are an image beside the studio's own writing. Only the
   empty image half goes; the paragraph stays and takes the width. Hiding the
   whole block would be deleting copy to fix a layout. */
.feature__media:has(.work__media--flat) {
  display: none !important;
}

/* Removing an empty media half must also release its grid tracks. Otherwise
   the surviving paragraph stays in its old four/five-column slot and the
   page presents a large, apparently missing image beside it. */
.feature:has(.feature__media .work__media--flat) .feature__text,
.feature--flip:has(.feature__media .work__media--flat) .feature__text {
  grid-column: 1 / -1 !important;
  grid-row: auto !important;
  width: min(100%, 72ch);
  margin-inline: auto;
}

/* A rail with no cards left in it is a horizontal scroller of nothing, and it
   keeps its own padding and scrollbar gutter. Same for the works grid: with
   every tile hidden the grid gap still reserves space between things that are
   not there. */
.rail:not(:has(.card:not(:has(.work__media--flat)))),
.works:not(:has(.work:not(:has(.work__media--flat)))) {
  display: none !important;
}

/* An emptied gallery wall collapses to nothing on its own — it has no
   children and no height — but it still contributes its grid gap to the
   section around it. */
.wall:empty {
  display: none !important;
}

/* ---- the save routes, closed before the script runs -------------------
   protect.js applies all of this too, and did so alone until now — which
   meant that on a slow connection there was a window between the
   photographs painting and the script running where they were live and
   unguarded. A stylesheet is parsed before anything is painted, so the
   rules that CAN live here now do, and the script is what catches whatever
   is added to the page afterwards.

   -webkit-touch-callout is the property that suppresses the iOS long-press
   sheet — the one carrying Save to Photos and Download Linked File. It is
   Safari-only; other engines ignore it and are covered by the contextmenu
   handler in protect.js instead. */
.piece img, .piece picture,
.frame img, .frame picture,
.plate img, .plate picture,
.pcard img, .pcard picture,
.work__media img, .work__media picture,
.card__media img, .card__media picture,
.post__cover img, .post__cover picture,
.hero__film img, .hero__film video,
.lb__img, .lb__stage,
video {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* Every other route into the lightbox is an anchor around a photograph too,
   and each one raised the same preview. Named here so a tile added to any of
   these galleries later behaves like the ones already in them. */
.frame a, a.frame,
.plate a, a.plate,
.pcard a, a.pcard,
.work__media a, a.work__media,
.card__media a, a.card__media,
.lb a, .lb__stage a {
  -webkit-touch-callout: none;
  touch-action: manipulation;
}
.piece__img img {
  width: 100%; height: 100%;
  object-fit: cover;
  background: var(--stone-150);
  transition: transform .7s var(--ease), filter .5s;
  display: block;
}
/* The zoom was here and had to go: with the picture fitted to the slot, any
   scale above 1 is a crop, and it persisted for as long as the cursor stayed.
   Scaling .piece__img instead is not an option — a.piece around it also has
   overflow:hidden, so that only moves the crop up a level. The tile still
   answers the cursor through the lift on .piece, which cannot crop. */
.piece:hover .piece__img img { transform: none; }

.piece__veil {
  position: absolute; inset: 0; z-index: 2;
  background: linear-gradient(to top, rgba(20,17,12,.78) 0%, rgba(20,17,12,.16) 38%, transparent 62%);
  opacity: 0; transition: opacity .6s var(--ease);
}
.piece:hover .piece__veil { opacity: 1; }

.piece__cap {
  position: absolute; left: 0; bottom: 0; z-index: 3;
  padding: 18px 18px 17px; width: 100%;
  transform: translateY(9px);
  opacity: 0;
  transition: opacity .55s var(--ease), transform .6s var(--ease);
}
.piece:hover .piece__cap { opacity: 1; transform: none; }
.piece__t { font-family: var(--f-display); color: var(--marble); font-size: .98rem; line-height: 1.2; margin-bottom: 5px; }
.piece__m {
  font-family: var(--f-mono); font-size: .53rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--accent-dark);
  display: flex; gap: 10px; flex-wrap: wrap;
}
.piece__zoom {
  position: absolute; top: 14px; right: 14px; z-index: 3;
  width: 30px; height: 30px; display: grid; place-items: center;
  border: 1px solid rgba(247,241,232,.5); color: var(--marble);
  opacity: 0; transform: scale(.9);
  transition: opacity .5s var(--ease), transform .5s var(--ease);
}
.piece:hover .piece__zoom { opacity: 1; transform: none; }

/* Always-on caption below the frame, for sections without hover (touch) */
.piece__below { padding: 13px 2px 0; }
.piece__below .piece__t { color: var(--ink); font-size: .95rem; margin-bottom: 4px; }
.piece__below .piece__m { color: var(--ink-mute); }

.piece__below { padding: 11px 1px 0; }
/* Touch gets the caption too — it just does not wait for a hover that will
   never come. These three were `display: none` on any device without a
   pointer, which meant every photograph on Collections and on the home page
   carried no title, no material and no zoom affordance on a phone. Most of
   this audience is on a phone. The scrim and the caption are simply on, and
   the tile reads as a finished editorial card rather than a bare crop. */
@media (hover: none) {
  .piece__veil { opacity: 1; }
  .piece__cap  { opacity: 1; transform: none; }
  .piece__zoom { opacity: .78; transform: none; }
  .piece:hover { transform: none; box-shadow: none; }
}

@media (max-width: 1000px) {
  .wall, .wall--4 { grid-template-columns: repeat(2, 1fr); }
}
/* Two up, not one.
   A single column put eight carvings one under another and made The Wall
   4,537px on a 390px phone — five and a half screens of scrolling through one
   section, which is most of why the page felt endless. Two columns halve it
   and the pieces still read: these are square-ish reliefs, not text. */
@media (max-width: 600px) {
  .wall, .wall--2, .wall--4 { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .wall > * { margin-bottom: 0; }
  .piece__cap { padding: 12px 11px 11px; }
  .piece__t { font-size: .84rem; }
  .piece__m { font-size: .46rem; letter-spacing: .13em; }
  .piece__below .piece__t { font-size: .82rem; }
}
/* On the narrowest phones two columns of caption get cramped, so the caption
   below the frame carries the naming instead of the overlay. */
@media (max-width: 380px) {
  .wall, .wall--2, .wall--4 { gap: 8px; }
}

/* ==========================================================================
   LIGHTBOX
   ========================================================================== */
.lb {
  position: fixed; inset: 0; z-index: 300;
  display: grid; place-items: center;
  padding: clamp(14px, 3vw, 46px);
  visibility: hidden;
}
.lb.is-open { visibility: visible; }
.lb__veil {
  position: absolute; inset: 0;
  background: rgba(14,11,8,.93);
  opacity: 0; transition: opacity .5s var(--ease);
}
.lb.is-open .lb__veil { opacity: 1; }
.lb__stage {
  position: relative; z-index: 2;
  max-width: min(1100px, 100%);
  max-height: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 16px;
  opacity: 0; transform: scale(.97);
  transition: opacity .55s var(--ease), transform .65s var(--ease);
}
.lb.is-open .lb__stage { opacity: 1; transform: none; }
.lb__img {
  max-width: 100%;
  max-height: calc(100vh - clamp(120px, 18vw, 200px));
  object-fit: contain;
  display: block;
  box-shadow: 0 40px 90px -40px rgba(0,0,0,.9);
}
.lb__cap { text-align: center; }
.lb__cap .piece__t { color: var(--marble); font-size: 1.15rem; }
.lb__cap .piece__m { justify-content: center; }

.lb__btn {
  position: absolute; z-index: 4;
  width: 48px; height: 48px; display: grid; place-items: center;
  color: var(--marble); border: 1px solid rgba(247,241,232,.28);
  background: rgba(20,17,12,.5);
  transition: background .35s, border-color .35s, transform .35s var(--ease);
}
.lb__btn:hover { background: rgba(247,241,232,.14); border-color: rgba(247,241,232,.6); }
.lb__prev { left: clamp(10px, 2vw, 28px); top: 50%; transform: translateY(-50%); }
.lb__next { right: clamp(10px, 2vw, 28px); top: 50%; transform: translateY(-50%); }
.lb__prev:hover { transform: translateY(-50%) translateX(-3px); }
.lb__next:hover { transform: translateY(-50%) translateX(3px); }
.lb__close { top: clamp(10px, 2vw, 24px); right: clamp(10px, 2vw, 28px); }
.lb__close:hover { transform: rotate(90deg); }
.lb__count {
  position: absolute; bottom: clamp(12px, 2vw, 26px); left: 50%; transform: translateX(-50%);
  z-index: 4; font-family: var(--f-mono); font-size: .58rem; letter-spacing: .22em;
  color: var(--stone-400);
}
@media (max-width: 620px) {
  .lb__prev { left: 8px; } .lb__next { right: 8px; }
  .lb__btn { width: 42px; height: 42px; }
}


/* ==========================================================================
   PHONE
   Most of this audience arrives on a phone, so the small screen is treated as
   the primary layout rather than a fallback. Nothing is cropped, nothing is
   hidden behind a hover, and the film keeps out of the type's way.
   ========================================================================== */
@media (max-width: 760px) {
  /* Hero: the film is a cinematic band across the top, not a full-height
     backdrop.
     `inset: 0` stretched it over the entire hero — 375 wide by 1047 tall on a
     phone. The reel and its poster are 16:9, so `object-fit: cover` drew them
     at 1861x1047 and threw away EIGHTY PERCENT of the width: you were looking
     at a narrow vertical strip out of the middle of a mural and could not tell
     what it was.

     Held to a band a little taller than the footage's own shape, the crop
     falls to roughly a third and the mural actually reads. The scrim then
     carries it down into the sand so the headline still sits on quiet
     ground. */
  .hero { min-height: auto; padding-top: 96px; padding-bottom: 34px; }
  .hero__film {
    inset: 0 0 auto 0;
    width: 100%;
    /* Height bumped from 344px to 460px so the 16:9 reel is no longer
       brutally cropped on a phone. The mural reads at this size and the band
       still leaves the headline on quiet ground. */
    height: clamp(310px, 52vh, 460px);
  }
  .hero__film::after {
    background:
      linear-gradient(180deg, rgba(233,227,213,.62) 0%, rgba(233,227,213,.34) 30%, rgba(233,227,213,.55) 62%, rgba(233,227,213,.97) 96%, rgba(233,227,213,1) 100%);
  }
  /* Bias the crop upward: the carving that matters sits in the top half of
     the frame, and centring it put the reader's eye on the plinth. The 50% 38%
     that was hard-coded here is now only the default — the panel sets --focal-m
     per site, because a different reel wants a different anchor. */
  .hero__film video { object-position: var(--focal-m, 50% 38%); }
  .hero__title { margin-bottom: 22px; }
  .hero__foot { padding-top: 24px; gap: 22px; }
  .hero__actions { width: 100%; }
  .hero__actions .btn { flex: 1 1 auto; justify-content: center; }

  /* Section rhythm tightens so the work stays close together */
  .sec-head { margin-bottom: 26px; }
  .sec-head__aside { padding-top: 14px; }
  .specs { margin-top: 30px; }

  /* Panel behaves like a sheet */
  .panel { padding: 0; align-items: stretch; }
  .panel__card {
    width: 100%; max-height: 100vh; max-height: 100dvh;
    grid-template-columns: 1fr;
    transform: translateY(28px);
  }
  .panel__main  { padding: 26px 20px 30px; order: 1; }
  .panel__aside { padding: 24px 20px 26px; order: 2; }
  .panel__close { background: rgba(20,17,12,.5); border: 1px solid var(--rule-dark); }

  /* Lightbox: give the image the whole screen */
  .lb { padding: 0; }
  .lb__stage { gap: 10px; padding: 56px 10px 62px; }
  .lb__img { max-height: calc(100dvh - 130px); }
  .lb__cap { padding-inline: 14px; }

  /* Cards and rails */
  .rail { grid-auto-columns: 82vw; }
  .pcard__body { padding: 20px 18px 22px; }
  .work__cap { padding: 16px; }
  .cta__watermark { display: none; }
}

@media (max-width: 480px) {
  .specs__n { font-size: 1.7rem; }
  .lede { font-size: .96rem; }
  .btn { width: 100%; justify-content: center; }
  .hero__actions { flex-direction: column; }
}

/* Never let a photograph or a plate spill out of its column */
.wall img, .frame img, .plate img, .pcard img { max-width: 100%; }

/* ==========================================================================
   SMALL-SCREEN LEGIBILITY
   Mono labels are set tight on desktop, where they read as machine data.
   At phone size that becomes hard to read, so the utility scale steps up
   and gives back some tracking to keep line lengths honest.
   Lives at the end of the cascade so it beats the component defaults.
   ========================================================================== */
@media (max-width: 720px) {
  .mono, .eyebrow, .brand__sub, .nav__link, .btn, .tlink,
  .specs__l, .stat__l, .market__d, .pillar__i, .proc__k,
  .card__k, .card__no, .work__meta, .work__meta span, .plate__tag,
  .post__n, .post__tag, .tl__yr, .phead__crumb, .phead__crumb a, .phead__crumb span,
  .foot__head, .form__note, .field label, .cdl__k,
  .ticker__item, .proc__list li, .feature__stamp .mono,
  .foot__bottom .mono, .foot__bottom p, .foot__nav a, .foot__list a,
  .piece__m, .piece__m span, .piece__below .piece__m, .piece__below .piece__m span,
  .pcard__badge, .pcard__meta, .pcard__meta span, .pcard__sub,
  .lb__count, .lb__cap .piece__m, .lb__cap .piece__m span {
    /* Bumped from 0.7rem to 0.82rem so mono labels are still legible at phone
       arm's length. The previous size read as machine noise rather than text. */
    font-size: 0.82rem;
    letter-spacing: 0.13em;
  }
  .plate__tag, .card__no { font-size: 0.76rem; }
  .piece__below .piece__t { font-size: 1.08rem; }
  .pcard__sub { letter-spacing: normal; font-size: .98rem; }
  .foot__list a, .cdl__v { letter-spacing: normal; font-size: 1.02rem; }

  /* Tap targets: 44px minimum, per WCAG 2.5.5 */
  .tlink { min-height: 44px; align-items: center; }
  .tlink::after { bottom: 11px; width: calc(100% - 22px); }
  .btn { padding: 17px 24px; font-size: 0.78rem; }
  .rail-btn { width: 48px; height: 48px; }
  .acc__btn { padding-block: 20px; min-height: 56px; }
  .foot__list a { display: inline-block; padding-block: 6px; }

  .d-xxl { line-height: 1.06; }
  .hero { padding-top: 112px; }
  .hero__title { margin-bottom: 28px; }
  /* Lede and body read larger on phones so a reader is not squinting at the
     spec text below a hero. The previous .96rem was below body copy size. */
  .lede { font-size: 1.06rem; }
  .body, .proc__d, .post__x { font-size: 1rem; line-height: 1.7; }
  .feature__stamp p { font-size: 0.94rem; }
  .card__d { font-size: 0.96rem; }
  .work__s { font-size: 0.96rem; }
  .acc__btn { font-size: clamp(1.15rem, 4.5vw, 1.4rem); }
  .proc__t { font-size: clamp(1.55rem, 6vw, 1.95rem); }
  .stat__n { font-size: clamp(2.2rem, 9vw, 2.8rem); }
  .specs__n { font-size: clamp(2rem, 8.5vw, 2.6rem); }
}

@media (max-width: 400px) {
  .mono, .eyebrow, .work__meta, .work__meta span, .plate__tag { letter-spacing: 0.12em; }
}


/* ==========================================================================
   THE AGENT
   A launcher carrying the Stonegraph mark, and a panel that opens over the
   page. The mark does not spin — a spinning logo reads as a loading state and
   cheapens a carving. Instead light rakes slowly across it, the way it moves
   over cut stone, and the sweep quickens while the agent is thinking.
   ========================================================================== */
.ag-launch {
  contain: layout paint;
  position: fixed;
  right: clamp(14px, 2vw, 26px);
  bottom: clamp(14px, 2vw, 26px);
  z-index: 150;
  width: 60px; height: 60px;
  display: grid; place-items: center;
  padding: 0;
  border-radius: 50%;
  /* Marble, with a stone hairline and one soft shadow.
     This was a bronze metal gradient with a bronze rim, a bronze glow and two
     inset highlights — five saturated things on a 60px circle, over pages that
     are already warm and mid-toned, so it read as ornament rather than as a
     control. A pale disc against the page reads as expensive precisely because
     nothing about it is trying: the contrast carries it and the colour stays
     out of the way. The one accent left is the mark itself. */
  background:
    radial-gradient(120% 120% at 30% 22%, #FFFFFF 0%, rgba(255,255,255,0) 52%),
    var(--marble);
  border: 1px solid var(--stone-200);
  box-shadow:
    0 16px 34px -14px rgba(20,17,12,0.42),
    0 2px 6px -2px rgba(20,17,12,0.14),
    inset 0 1px 0 rgba(255,255,255,0.9);
  transition: transform .55s var(--ease), border-color .4s, box-shadow .55s var(--ease);
}
.ag-launch:hover {
  /* The reference lifts and scales and does nothing to the colour. A pale disc
     has nowhere to glow to, and the shadow deepening under it is a clearer
     signal that the thing is now raised. */
  transform: translateY(-3px) scale(1.05);
  border-color: var(--stone-300);
  box-shadow:
    0 22px 44px -14px rgba(20,17,12,0.5),
    0 3px 10px -2px rgba(20,17,12,0.18),
    inset 0 1px 0 rgba(255,255,255,0.95);
}
.ag-launch__mark {
  position: relative; width: 29px; height: 33px; overflow: hidden;
}
.ag-launch__mark img {
  width: 100%; height: 100%; object-fit: contain;
  /* Masked rather than inverted: the silhouette is filled with the brand
     bronze, so it reads as the mark instead of a grey smudge. */
  opacity: 0;
}
.ag-launch__mark {
  /* The accent, not the light bronze: the disc under it is marble now, and
     --accent-dark was chosen to carry on basalt. */
  background: var(--accent);
  -webkit-mask: var(--mark-src) center / contain no-repeat;
  mask: var(--mark-src) center / contain no-repeat;
  /* No drop-shadow. It existed to lift the silhouette off a dark metal
     gradient; on marble it only muddies the edge. */
}
/* ---- the sweep ----
   The reference rotates its mark continuously. That suits an abstract spiral
   and does not suit this one: the Stonegraph mark is an architectural
   silhouette with a top and a bottom, and a mark that is upside down half the
   time is not the mark. So it holds still and the LIGHT moves instead — which
   is what a polished surface does, and the only gesture this shape can make
   without ceasing to be itself.

   The gradient was already here; the keyframes it named were never written, so
   it has sat inert since it was added. 9s with the travel packed into the first
   fifth: a catch of light, then a long rest. A sweep that ran continuously
   would be a loading bar. */
.ag-launch__mark::after {
  content: "";
  position: absolute; inset: -40% -120%;
  background: linear-gradient(105deg, transparent 44%, rgba(255,252,244,0.85) 50%, transparent 56%);
  transform: translateX(-100%);
  animation: mark-rake 9s var(--ease) 1.2s infinite;
  pointer-events: none;
}
@keyframes mark-rake {
  0%        { transform: translateX(-100%); }
  18%, 100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */
  .ag-launch__mark::after { animation: none; opacity: 0; }
}
/* A quiet ring that breathes out — enough to be noticed, not enough to nag.
   Softened along with the disc: at 0.65 against bronze it was a second rim,
   and against marble it only has to be visible. */
.ag-launch::after {
  content: "";
  position: absolute; inset: -2px;
  border-radius: 50%;
  border: 1px solid rgba(111,90,60,0.45);
  opacity: 0;
  animation: ag-ring 4.5s ease-out infinite;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */
  .ag-launch::after { animation: none; }
}
@keyframes ag-ring {
  0%   { opacity: .55; transform: scale(1); }
  70%, 100% { opacity: 0; transform: scale(1.55); }
}
/* the label only appears on hover, on a pointer device */
.ag-launch__t {
  position: absolute; right: calc(100% + 10px); top: 50%;
  transform: translateY(-50%) translateX(6px);
  white-space: nowrap;
  background: var(--stone-900); color: var(--on-dark);
  padding: 8px 12px;
  font-family: var(--f-mono); font-size: .58rem; letter-spacing: .16em;
  text-transform: uppercase;
  opacity: 0; pointer-events: none;
  transition: opacity .4s, transform .5s var(--ease);
}
@media (hover: hover) {
  .ag-launch:hover .ag-launch__t { opacity: 1; transform: translateY(-50%); }
}
@media (hover: none) { .ag-launch__t { display: none; } }
.ag-launch__dot { display: none; }

@media (max-width: 620px) {
  .ag-launch { right: 14px; bottom: 14px; width: 56px; height: 56px; }
}

.ag.is-thinking .ag-head__mark::after { animation-duration: 1.5s; }

@media (max-width: 620px) {
  .ag-launch { right: 12px; bottom: 12px; padding: 9px 13px 9px 10px; }
  .ag-launch__t { font-size: .56rem; letter-spacing: .13em; }
}

/* ---------- panel ---------- */
.ag {
  position: fixed; inset: 0; z-index: 220;
  display: grid; place-items: end center;
  /* The row is capped at the container. Left as the default `auto` it grew to
     the card's max-content height — which includes the whole message log — so
     the row outgrew the viewport and `place-items: end` aligned the card to
     the bottom of the row rather than the bottom of the screen, carrying the
     composer off the bottom of the display. minmax(0, 1fr) is what stops a
     grid track from being sized by its content. */
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
  padding: clamp(12px, 3vw, 40px);
  /* The phone's on-screen keyboard and the browser's own toolbars both eat
     into the viewport. dvh tracks what is actually visible; the vh line above
     it is the fallback for anything that does not know dvh. */
  height: 100vh;
  height: 100dvh;
  visibility: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
.ag.is-open { visibility: visible; }
.ag__veil {
  position: absolute; inset: 0;
  /* Same fix, same reason as .panel__veil above — this is the other overlay
     that was reported as catching on open. */
  background: rgba(20, 17, 12, .68);
  opacity: 0; transition: opacity .42s var(--ease);
}
.ag.is-open .ag__veil { opacity: 1; }
/* The card reads as a piece of the studio's own stationery — warm stone paper,
   a hairline of bronze catching the top edge, and a shadow deep enough to lift
   it off the page. It arrives with a slight rise and settle rather than a
   pop. */
.ag__card {
  position: relative; z-index: 2;
  width: min(560px, 100%); height: min(720px, 100%);
  /* Bounded as well as sized. The height above is what the card wants; this
     is what it may never exceed, whatever the log is holding. */
  max-height: 100%;
  min-height: 0;
  display: flex; flex-direction: column;
  background:
    radial-gradient(120% 80% at 50% 0%, rgba(201,163,90,0.10) 0%, transparent 58%),
    linear-gradient(180deg, var(--stone-050) 0%, var(--marble) 42%);
  border: 1px solid rgba(201,163,90,0.34);
  border-radius: 3px;
  box-shadow:
    0 44px 96px -44px rgba(20,17,12,.72),
    0 0 0 1px rgba(201,163,90,0.10),
    inset 0 1px 0 rgba(255,246,228,0.55);
  opacity: 0; transform: translateY(46px) scale(.982);
  transition: opacity .6s var(--ease), transform .75s var(--ease);
}
.ag.is-open .ag__card { opacity: 1; transform: none; }

/* A thread of bronze light along the top edge, drawn once as the panel opens. */
.ag__card::before {
  content: "";
  position: absolute; left: 0; right: 0; top: 0; height: 1px; z-index: 6;
  background: linear-gradient(90deg, transparent, var(--gold-2) 22%, #F3E3BE 50%, var(--gold-2) 78%, transparent);
  transform: scaleX(0); transform-origin: center;
  transition: transform 1.1s var(--ease) .18s;
}
.ag.is-open .ag__card::before { transform: scaleX(1); }

.ag-head {
  display: flex; align-items: center; gap: 12px;
  padding: 16px 16px 15px 18px;
  background:
    radial-gradient(140% 140% at 18% 0%, rgba(194,170,126,0.22) 0%, rgba(30,26,20,0) 55%),
    linear-gradient(180deg, #2A251D 0%, #1E1A14 100%);
  color: var(--on-dark);
  flex: none;
  border-bottom: 1px solid rgba(194,170,126,0.22);
}
.ag-head__mark { position: relative; width: 26px; height: 30px; flex: none; overflow: hidden; }
.ag-head__mark img { opacity: 0; }
.ag-head__mark {
  background: var(--accent-dark);
  -webkit-mask: var(--mark-src) center / contain no-repeat;
  mask: var(--mark-src) center / contain no-repeat;
}
/* The same catch of light as the launcher. This header keeps its dark ground —
   the card below it is marble, and a pale header on a pale card has nothing to
   sit against — so the mark here stays the light bronze that carries on it. */
.ag-head__mark::after {
  content: ""; position: absolute; inset: -40% -120%;
  background: linear-gradient(105deg, transparent 44%, rgba(255,246,228,0.6) 50%, transparent 56%);
  transform: translateX(-100%);
  animation: mark-rake 9s var(--ease) 1.2s infinite;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */
  .ag-head__mark::after { animation: none; opacity: 0; }
}
.ag-head__l { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ag-head__n { font-family: var(--f-display); font-size: 1rem; line-height: 1; color: var(--marble); }
.ag-head__s { font-family: var(--f-mono); font-size: .5rem; letter-spacing: .2em;
              text-transform: uppercase; color: var(--accent-dark); }
.ag-head__x { margin-left: auto; width: 36px; height: 36px; display: grid; place-items: center;
              color: var(--on-dark-mute); transition: color .3s, transform .5s var(--ease); }
.ag-head__x:hover { color: var(--marble); transform: rotate(90deg); }

/* `min-height: 0` is the whole reason this scrolls rather than pushes. A flex
   item's automatic minimum size is its content, so without it the log always
   claimed its full scroll height and grew the card instead of overflowing
   inside it — which is what carried the composer off the screen. */
.ag-log { flex: 1 1 0; min-height: 0; overflow-y: auto; padding: 20px 18px 8px; scroll-behavior: smooth; }
.ag-msg { margin-bottom: 16px; max-width: 92%; }
.ag-msg--me { margin-left: auto; }
.ag-msg__b {
  padding: 11px 14px; font-size: .93rem; line-height: 1.62;
  background: var(--stone-150); color: var(--ink);
}
.ag-msg--me .ag-msg__b { background: var(--stone-800); color: var(--on-dark); }
.ag-msg__b p + p { margin-top: .7em; }
.ag-msg__b a { color: var(--accent); border-bottom: 1px solid currentColor; }
.ag-msg--me .ag-msg__b a { color: var(--accent-dark); }
.ag-msg__b strong { font-weight: 600; }
.ag-msg__b ul { margin-top: .5em; }
.ag-msg__b li { padding-left: 15px; position: relative; margin-bottom: 5px; }
.ag-msg__b li::before { content: ""; position: absolute; left: 0; top: 9px; width: 4px; height: 4px;
                        border: 1px solid var(--accent); transform: rotate(45deg); }
.ag-msg__k { font-family: var(--f-mono); font-size: .5rem; letter-spacing: .2em;
             text-transform: uppercase; color: var(--ink-mute); margin-bottom: 6px; }
.ag-msg--me .ag-msg__k { text-align: right; }

/* quick prompts */
.ag-chips { display: flex; flex-wrap: wrap; gap: 7px; padding: 0 18px 14px; flex: none; }
.ag-chip {
  font-family: var(--f-mono); font-size: .55rem; letter-spacing: .13em; text-transform: uppercase;
  padding: 8px 11px; border: 1px solid var(--rule); color: var(--ink-soft);
  transition: background .35s, color .35s, border-color .35s;
}
.ag-chip:hover { background: var(--stone-900); color: var(--marble); border-color: var(--stone-900); }

/* The ask line. A bronze rule under the field draws itself in on focus, so
   typing feels like the studio leaning in rather than a form waking up. */
.ag-ask {
  display: flex; gap: 8px; align-items: center;
  padding: 12px 14px; flex: none;
  border-top: 1px solid var(--rule);
  background: linear-gradient(180deg, transparent, rgba(201,163,90,0.05));
  position: relative;
}
.ag-ask::after {
  content: "";
  position: absolute; left: 14px; right: 62px; bottom: 10px; height: 1px;
  background: var(--gold-2);
  transform: scaleX(0); transform-origin: left;
  transition: transform .6s var(--ease);
}
.ag-ask:focus-within::after { transform: scaleX(1); }
.ag-ask input {
  flex: 1; font-family: var(--f-body); font-size: .95rem; color: var(--ink);
  background: transparent; border: 0; padding: 12px 4px; border-radius: 0;
}
.ag-ask input::placeholder { color: var(--ink-mute); opacity: .8; }
.ag-ask input:focus { outline: none; }
.ag-ask button {
  width: 44px; height: 44px; display: grid; place-items: center; flex: none;
  color: var(--marble); border-radius: 2px;
  background:
    radial-gradient(120% 120% at 30% 20%, rgba(255,238,206,0.35) 0%, transparent 44%),
    linear-gradient(150deg, #6F5A3C 0%, #4A4237 55%, #2A251D 100%);
  border: 1px solid rgba(201,163,90,0.5);
  box-shadow: inset 0 1px 0 rgba(255,238,206,0.28), 0 6px 14px -8px rgba(20,17,12,.7);
  transition: box-shadow .4s, transform .4s var(--ease), border-color .35s;
}
.ag-ask button:hover:not(:disabled) {
  transform: translateX(2px);
  border-color: rgba(201,163,90,0.9);
  box-shadow: inset 0 1px 0 rgba(255,238,206,0.42), 0 0 20px -4px rgba(201,163,90,0.6);
}
.ag-ask button:disabled { opacity: .38; }

/* The studio's replies carry a bronze margin rule, the way a marked-up
   drawing does. */
.ag-msg--ag .ag-msg__b {
  border-left: 2px solid var(--gold-2);
  padding-left: 14px;
  background: linear-gradient(90deg, rgba(201,163,90,0.07), transparent 60%);
}

.ag-dots { display: inline-flex; gap: 4px; align-items: center; }
.ag-dots i { width: 5px; height: 5px; background: var(--ink-mute); border-radius: 50%;
             animation: ag-dot 1.3s ease-in-out infinite; }
.ag-dots i:nth-child(2) { animation-delay: .18s; }
.ag-dots i:nth-child(3) { animation-delay: .36s; }
/* Reduced motion still gets a thinking state, just a still one: the dots hold
   at a legible strength rather than vanishing, because they are the only
   indication that a reply is on its way. */
@media (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */
  .ag-dots i { animation: none; opacity: .6; }
}
@keyframes ag-dot { 0%,100% { opacity: .25; transform: translateY(0); }
                    50%     { opacity: 1;   transform: translateY(-3px); } }

.ag-note { padding: 0 18px 12px; font-family: var(--f-mono); font-size: .5rem;
           letter-spacing: .14em; text-transform: uppercase; color: var(--ink-mute); }

@media (max-width: 620px) {
  .ag { padding: 0; place-items: stretch; }
  .ag__card { width: 100%; height: 100%; max-height: 100%; border: 0; }
  .ag-chip { font-size: .6rem; }
  .ag-msg { max-width: 96%; }
}

/* The composer never shrinks and never scrolls away. Everything above it in
   the card is allowed to give; this is not. The safe-area inset keeps the
   send button clear of the iPhone home indicator. */
.ag-ask {
  flex: none;
  padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
}
.ag-chips { flex: none; }
.ag-head { flex: none; }


/* Agent controls meet the 44px tap minimum on touch devices */
@media (max-width: 760px) {
  .ag-head__x { width: 44px; height: 44px; }
  .ag-chip { padding: 13px 14px; min-height: 44px; display: inline-flex; align-items: center; }
  .ag-ask { padding: 10px 12px; }
  .ag-ask button { width: 46px; height: 46px; }
  .ag-ask input { padding: 12px 4px; font-size: 16px; }  /* 16px stops iOS zooming on focus */
  .ag-launch { min-height: 46px; }
}

/* ========================================================================== 
   THE AGENT — STONE, BRASS & CHARCOAL FINISH

   A final, deliberately scoped finish for the public assistant. The first
   version used the site's generic sand surfaces directly, which made the
   conversation read like an unstyled form. These local tokens create a
   tighter material hierarchy: honed limestone for the writing surface,
   charcoal for the studio and visitor chrome, and restrained brass only for
   live/interactive details.

   Motion is limited to one-shot opacity/transform entrances. There are no
   animated filters, large moving gradients or layout properties, so opening
   the assistant does not compete with scrolling or video playback.
   ========================================================================== */
.ag {
  --ag-charcoal: #181510;
  --ag-charcoal-2: #262119;
  --ag-charcoal-3: #342d23;
  --ag-brass: #9a7c4b;
  --ag-brass-bright: #d6bd88;
  --ag-brass-pale: #ead9b5;
  --ag-limestone: #eee7d9;
  --ag-parchment: #f7f2e8;
  --ag-paper: #fdfaf3;
  --ag-copy: #2b261e;
  --ag-copy-soft: #5b5143;
  --ag-hairline: rgba(63, 51, 35, .16);
  --ag-dark-line: rgba(222, 198, 148, .23);
}

/* A dense neutral veil gives the panel contrast without the continuously
   repainted backdrop blur that used to sit over the entire page. */
.ag__veil {
  background: rgba(12, 10, 7, .64);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transition: opacity .36s ease-out;
}

.ag__card {
  width: min(568px, 100%);
  height: min(748px, 100%);
  overflow: hidden;
  isolation: isolate;
  border: 1px solid rgba(214, 189, 136, .46);
  border-radius: 12px;
  background: var(--ag-parchment);
  box-shadow:
    0 34px 88px -30px rgba(0, 0, 0, .78),
    0 12px 30px -20px rgba(0, 0, 0, .72),
    0 0 0 1px rgba(255, 249, 235, .08);
  transform: translate3d(0, 24px, 0) scale(.988);
  transition: opacity .36s ease-out, transform .54s var(--ease);
}

.ag.is-open .ag__card { transform: translate3d(0, 0, 0) scale(1); }

.ag__card::before {
  height: 2px;
  background: linear-gradient(
    90deg,
    transparent 0%,
    var(--ag-brass) 16%,
    var(--ag-brass-pale) 50%,
    var(--ag-brass) 84%,
    transparent 100%
  );
  transition: transform .85s var(--ease) .12s;
}

.ag-head {
  min-height: 74px;
  gap: 14px;
  padding: 15px 16px 15px 20px;
  background:
    radial-gradient(circle at 12% -45%, rgba(214, 189, 136, .20), transparent 47%),
    linear-gradient(135deg, var(--ag-charcoal-3) 0%, var(--ag-charcoal-2) 35%, var(--ag-charcoal) 100%);
  border-bottom-color: var(--ag-dark-line);
  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .28);
}

.ag-head__mark {
  width: 30px;
  height: 35px;
  background: var(--ag-brass-bright);
}

/* In the open panel the glint crosses once, then rests. This reads as an
   arrival detail rather than a perpetual loading indicator. */
.ag-head__mark::after { animation: none; }
.ag.is-open .ag-head__mark::after {
  animation: ag-mark-arrive 1.05s var(--ease) .28s both;
}
@keyframes ag-mark-arrive {
  from { transform: translate3d(-100%, 0, 0); }
  to   { transform: translate3d(100%, 0, 0); }
}

.ag-head__l { gap: 5px; }
.ag-head__n {
  color: #faf5e9;
  font-size: 1.08rem;
  letter-spacing: .015em;
}
.ag-head__s {
  display: flex;
  align-items: center;
  gap: 7px;
  color: var(--ag-brass-bright);
  font-size: .52rem;
  letter-spacing: .22em;
}
.ag-head__s::before {
  content: "";
  width: 5px;
  height: 5px;
  flex: none;
  border-radius: 50%;
  background: var(--ag-brass-bright);
  box-shadow: 0 0 0 3px rgba(214, 189, 136, .11);
  animation: ag-status-breathe 2.8s ease-in-out infinite;
}
@keyframes ag-status-breathe {
  0%, 100% { opacity: .48; transform: scale(.82); }
  45%      { opacity: 1; transform: scale(1); }
}

.ag-head__x {
  border: 1px solid transparent;
  border-radius: 50%;
  color: #c8bdab;
  transition: color .24s ease-out, border-color .24s ease-out,
              background-color .24s ease-out, transform .4s var(--ease);
}
.ag-head__x:hover {
  color: #fffaf0;
  border-color: var(--ag-dark-line);
  background: rgba(255, 250, 240, .06);
  transform: rotate(90deg);
}
.ag-head__x:focus-visible {
  outline: 2px solid var(--ag-brass-bright);
  outline-offset: 2px;
}

.ag-log {
  padding: 26px 22px 12px;
  color: var(--ag-copy);
  background:
    radial-gradient(circle at 100% 0%, rgba(154, 124, 75, .075), transparent 34%),
    linear-gradient(115deg, rgba(255, 255, 255, .36), transparent 38%),
    var(--ag-parchment);
  scrollbar-color: rgba(154, 124, 75, .48) transparent;
  scrollbar-width: thin;
}
.ag-log::-webkit-scrollbar { width: 6px; }
.ag-log::-webkit-scrollbar-track { background: transparent; }
.ag-log::-webkit-scrollbar-thumb {
  border: 2px solid transparent;
  border-radius: 6px;
  background: rgba(154, 124, 75, .48);
  background-clip: padding-box;
}

.ag-msg {
  max-width: 86%;
  margin-bottom: 20px;
  animation: none;
}

/* Each turn arrives in two beats: the speaker label settles first and the
   stone-paper reply follows a fraction later. The opening line is replayed by
   agent.js only after the panel is visible, so this detail is now actually
   seen instead of finishing behind an aria-hidden dialog. */
.ag-msg--arriving .ag-msg__k {
  animation: ag-message-label .34s var(--ease) var(--ag-arrival-delay, 0ms) both;
}
.ag-msg--arriving .ag-msg__b {
  animation: ag-message-body .5s var(--ease)
             calc(var(--ag-arrival-delay, 0ms) + 75ms) both;
}
@keyframes ag-message-label {
  from { opacity: 0; transform: translate3d(0, 5px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes ag-message-body {
  from { opacity: 0; transform: translate3d(0, 13px, 0) scale(.985); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}

/* When the waiting dots become an answer, give the new words their own quiet
   resolve instead of replacing the indicator in a single hard frame. */
.ag-msg--answer-ready .ag-msg__b {
  animation: ag-answer-resolve .46s var(--ease) both;
}
@keyframes ag-answer-resolve {
  from { opacity: .2; transform: translate3d(0, 7px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

.ag-msg__k {
  margin: 0 2px 7px;
  color: #665a49;
  font-size: .54rem;
  letter-spacing: .22em;
}
.ag-msg__b {
  padding: 13px 15px;
  border: 1px solid rgba(85, 69, 47, .14);
  border-radius: 3px 12px 12px 12px;
  background:
    linear-gradient(135deg, rgba(255, 255, 255, .78), rgba(255, 255, 255, .28)),
    var(--ag-paper);
  color: var(--ag-copy);
  box-shadow: 0 8px 24px -22px rgba(24, 21, 16, .7);
  font-size: .95rem;
  line-height: 1.66;
}

/* The renderer creates `.ag-msg` for a studio turn, not `.ag-msg--ag`.
   Target the actual DOM so the brass drawing-rule treatment is no longer
   silently skipped. */
.ag-msg:not(.ag-msg--me) .ag-msg__b {
  border-left: 2px solid var(--ag-brass);
  padding-left: 15px;
}

.ag-msg--me .ag-msg__b {
  border-color: rgba(214, 189, 136, .22);
  border-radius: 12px 3px 12px 12px;
  background: linear-gradient(145deg, var(--ag-charcoal-3), var(--ag-charcoal));
  color: #f8f2e6;
  box-shadow: 0 12px 28px -22px rgba(0, 0, 0, .85);
}
.ag-msg--me .ag-msg__k { color: #665a49; }
.ag-msg__b a { color: #765a30; text-underline-offset: 3px; }
.ag-msg--me .ag-msg__b a { color: var(--ag-brass-pale); }
.ag-msg__b li::before { border-color: var(--ag-brass); }

.ag-chips {
  gap: 8px;
  padding: 0 22px 16px;
  background: var(--ag-parchment);
}
.ag-chip {
  border-color: rgba(85, 69, 47, .20);
  border-radius: 999px;
  background: rgba(253, 250, 243, .6);
  color: #554a3b;
  transition: color .24s ease-out, background-color .24s ease-out,
              border-color .24s ease-out, transform .3s var(--ease);
}
.ag-chip:hover {
  border-color: var(--ag-brass);
  background: var(--ag-charcoal-2);
  color: #faf5e9;
  transform: translate3d(0, -1px, 0);
}
.ag-chip:focus-visible {
  outline: 2px solid var(--ag-brass);
  outline-offset: 2px;
}

.ag-ask {
  gap: 10px;
  padding: 14px 16px calc(14px + env(safe-area-inset-bottom, 0px));
  border-top-color: rgba(85, 69, 47, .18);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .54), rgba(238, 231, 217, .44)),
    var(--ag-limestone);
  box-shadow: 0 -14px 34px -32px rgba(24, 21, 16, .72);
}
.ag-ask::after {
  left: 20px;
  right: 76px;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(90deg, var(--ag-brass), var(--ag-brass-bright));
  transition: transform .42s var(--ease);
}
.ag-ask input {
  min-width: 0;
  padding: 13px 7px;
  color: var(--ag-copy);
  caret-color: var(--ag-brass);
}
.ag-ask input::placeholder { color: #6d6253; opacity: 1; }
.ag-ask button {
  width: 46px;
  height: 46px;
  position: relative;
  overflow: hidden;
  border: 1px solid rgba(214, 189, 136, .48);
  border-radius: 4px;
  background:
    radial-gradient(circle at 28% 18%, rgba(234, 217, 181, .20), transparent 34%),
    linear-gradient(145deg, #5d4b30, var(--ag-charcoal-2) 68%);
  color: #fff9ec;
  box-shadow:
    inset 0 1px 0 rgba(255, 245, 222, .18),
    0 10px 22px -16px rgba(0, 0, 0, .9);
  transition: border-color .24s ease-out, background-color .24s ease-out,
              transform .34s var(--ease), box-shadow .24s ease-out;
}
.ag-ask button svg {
  position: relative;
  z-index: 1;
  transition: opacity .2s ease-out, transform .32s var(--ease);
}
.ag-ask button::after {
  content: "";
  position: absolute;
  inset: 14px;
  border: 1px solid rgba(234, 217, 181, .34);
  border-top-color: var(--ag-brass-pale);
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
}
.ag-ask.is-dispatching button svg {
  animation: ag-send-flight .38s var(--ease) both;
}
.ag.is-thinking .ag-ask button {
  opacity: 1;
  cursor: wait;
}
.ag.is-thinking .ag-ask button svg { opacity: 0; transform: translate3d(7px, 0, 0); }
.ag.is-thinking .ag-ask button::after {
  animation: ag-send-wait .82s linear .14s infinite;
}
@keyframes ag-send-flight {
  0%   { opacity: 1; transform: translate3d(0, 0, 0); }
  48%  { opacity: 0; transform: translate3d(10px, 0, 0); }
  49%  { opacity: 0; transform: translate3d(-7px, 0, 0); }
  100% { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes ag-send-wait {
  0%   { opacity: 0; transform: rotate(0deg); }
  18%  { opacity: 1; }
  100% { opacity: 1; transform: rotate(360deg); }
}
.ag-ask button:hover:not(:disabled) {
  border-color: var(--ag-brass-bright);
  transform: translate3d(2px, 0, 0);
  box-shadow:
    inset 0 1px 0 rgba(255, 245, 222, .24),
    0 12px 24px -14px rgba(0, 0, 0, .9);
}
.ag-ask button:focus-visible {
  outline: 2px solid var(--ag-brass);
  outline-offset: 2px;
}
.ag-ask button:disabled { opacity: .46; }

.ag-dots i { background: var(--ag-brass); }

/* Interior choreography. Only opacity and transforms move, so opening the
   assistant remains smooth while a hero film is playing behind it. Reopening
   gets the same restrained welcome, but conversation bubbles do not all
   replay; only the first, previously-hidden greeting is replayed once. */
.ag.is-entering .ag-head__l {
  animation: ag-head-copy-arrive .48s var(--ease) .12s both;
}
.ag.is-entering .ag-head__x {
  animation: ag-close-arrive .4s var(--ease) .2s both;
}
.ag.is-entering .ag-log {
  animation: ag-surface-arrive .5s ease-out .08s both;
}
.ag.is-entering .ag-ask {
  animation: ag-composer-arrive .58s var(--ease) .28s both;
}
.ag.is-entering .ag-chip {
  animation: ag-chip-arrive .42s var(--ease) .32s both;
}
.ag.is-entering .ag-chip:nth-child(2) { animation-delay: .38s; }
.ag.is-entering .ag-chip:nth-child(3) { animation-delay: .44s; }
.ag.is-entering .ag-chip:nth-child(4) { animation-delay: .50s; }
@keyframes ag-head-copy-arrive {
  from { opacity: 0; transform: translate3d(-9px, 0, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes ag-close-arrive {
  from { opacity: 0; transform: rotate(-28deg) scale(.86); }
  to   { opacity: 1; transform: rotate(0) scale(1); }
}
@keyframes ag-surface-arrive {
  from { opacity: 0; transform: translate3d(0, 5px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes ag-composer-arrive {
  from { opacity: 0; transform: translate3d(0, 18px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes ag-chip-arrive {
  from { opacity: 0; transform: translate3d(0, 8px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* The launcher belongs to the same material family as the open panel. */
.ag-launch {
  border-color: rgba(105, 85, 55, .24);
  background:
    radial-gradient(circle at 31% 24%, #fffdf7 0%, rgba(255, 253, 247, 0) 44%),
    linear-gradient(145deg, #fbf7ee, #e7ddca);
  box-shadow:
    0 18px 38px -18px rgba(16, 13, 9, .52),
    0 3px 9px -5px rgba(16, 13, 9, .32),
    inset 0 1px 0 rgba(255, 255, 255, .9),
    inset 0 0 0 4px rgba(255, 255, 255, .24);
}
.ag-launch:hover {
  border-color: rgba(154, 124, 75, .54);
  box-shadow:
    0 24px 48px -18px rgba(16, 13, 9, .60),
    0 4px 12px -6px rgba(16, 13, 9, .34),
    inset 0 1px 0 rgba(255, 255, 255, .94),
    inset 0 0 0 4px rgba(255, 255, 255, .28);
}
.ag-launch__mark { background: #725a37; }
.ag-launch__t {
  border: 1px solid rgba(214, 189, 136, .26);
  border-radius: 3px;
  background: var(--ag-charcoal);
  color: #f8f2e6;
  box-shadow: 0 10px 24px -15px rgba(0, 0, 0, .76);
}
.ag-launch:focus-visible {
  outline: 2px solid #8b6d42;
  outline-offset: 4px;
}

@media (max-width: 620px) {
  .ag__card { border-radius: 0; }
  .ag-head { min-height: 72px; padding-inline: 17px 12px; }
  .ag-log { padding: 22px 15px 10px; }
  .ag-msg { max-width: 92%; margin-bottom: 18px; }
  .ag-msg__b { padding: 12px 14px; font-size: .94rem; }
  .ag-msg:not(.ag-msg--me) .ag-msg__b { padding-left: 14px; }
  .ag-chips { padding-inline: 15px; }
  .ag-ask {
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
  }
  .ag-ask::after {
    left: 16px;
    right: 70px;
    bottom: calc(10px + env(safe-area-inset-bottom, 0px));
  }
  .ag-ask input { padding: 12px 4px; font-size: 16px; }
}

@media (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */
  .ag__veil,
  .ag__card,
  .ag__card::before,
  .ag-head__x,
  .ag-msg,
  .ag-chip,
  .ag-ask::after,
  .ag-ask button,
  .ag-launch {
    animation: none !important;
    transition-duration: .01ms !important;
  }
  .ag-head__l,
  .ag-head__s::before,
  .ag-head__x,
  .ag-log,
  .ag-ask,
  .ag-ask button svg,
  .ag-ask button::after,
  .ag-msg__k,
  .ag-msg__b {
    animation: none !important;
    opacity: 1;
    transform: none;
  }
  .ag-head__mark::after { animation: none !important; opacity: 0; }
  .ag-msg { opacity: 1; transform: none; }
  .ag.is-thinking .ag-ask button svg { opacity: 0; }
  .ag.is-thinking .ag-ask button::after { opacity: .72; }
}

/* ==========================================================================
   LINEAGE
   The timeline is the one place on the site where the order of things is the
   point, so it gets an animation nothing else has: a thread that draws down
   the axis as you arrive, and a marker that lands on each generation in turn.
   ========================================================================== */
.tl--lineage { position: relative; }
/* Stretched between top and bottom rather than given a percentage height:
   the timeline's own height is content-driven, so a percentage would resolve
   to nothing. Scaling the Y axis animates on the compositor as a bonus. */
.tl__thread {
  position: absolute; left: 0; top: 8px; bottom: 8px;
  width: 1px;
  background: linear-gradient(to bottom, var(--accent) 0%, var(--accent) 72%, transparent 100%);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 2.6s cubic-bezier(.22,.61,.36,1);
}
.tl--lineage.is-drawn .tl__thread { transform: scaleY(1); }

.tl__item--gen .tl__dot {
  opacity: 0;
  transform: rotate(45deg) scale(.3);
  transition: opacity .5s var(--ease), transform .7s cubic-bezier(.34,1.56,.64,1);
  transition-delay: calc(.55s + var(--gen) * .58s);
}
.tl--lineage.is-drawn .tl__item--gen .tl__dot { opacity: 1; transform: rotate(45deg) scale(1); }

/* a ring settles onto each marker as the thread reaches it */
.tl__item--gen .tl__dot::after {
  content: ""; position: absolute; inset: -7px;
  border: 1px solid var(--accent);
  opacity: 0;
}
.tl--lineage.is-drawn .tl__item--gen .tl__dot::after {
  animation: gen-settle 1.5s ease-out forwards;
  animation-delay: calc(.65s + var(--gen) * .58s);
}
@keyframes gen-settle {
  0%   { opacity: 0; transform: scale(2.4); }
  45%  { opacity: .85; }
  100% { opacity: 0; transform: scale(1); }
}

.tl__item--gen .tl__yr,
.tl__item--gen .tl__t {
  opacity: 0; transform: translateX(-10px);
  transition: opacity .7s var(--ease), transform .8s var(--ease);
  transition-delay: calc(.7s + var(--gen) * .58s);
}
.tl__item--gen .tl__t { transition-delay: calc(.8s + var(--gen) * .58s); }
.tl--lineage.is-drawn .tl__item--gen .tl__yr,
.tl--lineage.is-drawn .tl__item--gen .tl__t { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */
  .tl__thread { transform: scaleY(1) !important; transition: none; }
  .tl__item--gen .tl__dot,
  .tl__item--gen .tl__yr,
  .tl__item--gen .tl__t { opacity: 1 !important; transform: none !important; }
  .tl__item--gen .tl__dot::after { animation: none; }
}


/* A `body.is-scrolling` rule used to sit here to rest the launcher while the
   page moved. Nothing ever set that class on <body>, so it never once applied
   — and it should not be revived: a class on <body> or <html> that descendant
   selectors hang off measured 8ms to toggle on this page, which is more than
   the repaint it was avoiding. The launcher has nothing left to rest anyway;
   see the note on mark-rake below. */
.ag:not(.is-open) .ag-head__mark::after { animation-play-state: paused; }

/* ==========================================================================
   CONTACT PANEL — the drawing side
   The form is gone. Asking someone to classify their own enquiry before they
   have said hello is friction, and it was the weakest thing on the page.
   The peacock takes the space and the direct channels do the work.
   ========================================================================== */
.panel__main {
  position: relative;
  display: flex; flex-direction: column; justify-content: flex-end;
  overflow: hidden;
  min-height: 380px;
  background: var(--stone-050);
}
/* The drawing used to sit in a mostly empty 800-unit box, so hanging it well
   off the right edge only ever cut away blank space. The peacock now fills
   its viewBox, and the same offset sliced the fan straight down one side —
   so it is brought back inside the panel and the mask is opened up to reach
   past the feathers rather than through them. */
.panel__art {
  position: absolute;
  right: 0; top: 50%;
  transform: translateY(-50%);
  width: min(96%, 540px);
  aspect-ratio: 1/1;
  opacity: .5;
  filter: saturate(.42) sepia(.2);
  pointer-events: none;
  z-index: 0;
  mask-image: radial-gradient(82% 82% at 52% 48%, #000 70%, transparent 100%);
  -webkit-mask-image: radial-gradient(82% 82% at 52% 48%, #000 70%, transparent 100%);
}
.panel__art svg { width: 100%; height: 100%; }
.panel__pitch { position: relative; z-index: 2; }
.panel__pitch .display { margin-bottom: 14px; }
.panel__pitch .body { font-size: .92rem; margin-bottom: 24px; max-width: 34ch; }

@media (max-width: 820px) {
  .panel__main { min-height: 300px; }
  .panel__art { right: 0; width: min(92%, 400px); opacity: .34; }
}

/* A work card with no photograph yet. It reads as a deliberate plate rather
   than a hole — carved stone tone, the mark set into it, and the caption
   where it always sits. When a project card gets its real photograph from the
   panel, this is simply covered over. */
.work__media--flat {
  position: relative;
  aspect-ratio: 3 / 2;
  background:
    radial-gradient(120% 100% at 30% 20%, rgba(194,170,126,.10), transparent 60%),
    linear-gradient(155deg, var(--stone-700) 0%, var(--stone-800) 55%, var(--stone-900) 100%);
}
.work__media--flat::before {
  content: "";
  position: absolute; inset: 0;
  background: var(--mark-src) center 38% / 54px auto no-repeat;
  opacity: .12;
  filter: invert(1);
}
.work__media--flat .work__veil { background: none; }


/* ==========================================================================
   FIRST PAINT
   The page arrives visible. It did not used to: site.js sits at the foot of
   the body, so by the time it ran the browser had already painted the whole
   page — and its first act was to set the body to opacity 0 and wait for
   `document.fonts.ready`, i.e. for three families to come back from Google,
   capped at 600ms. Every single navigation therefore went content, blank,
   then a 350ms fade of the entire document. That blank is what reads as a
   page "sticking" when you move between pages, and it costs up to a second of
   largest-contentful-paint for a jolt that `font-display: swap` produces
   anyway when the fonts land.

   Nothing hides the body now. The classes below are kept because a stale
   cached copy of site.js may still be setting them, and `.is-ready` must mean
   visible if it is.
   ========================================================================== */

/* `scroll-behavior: smooth` was on <html>, which applies it to every scroll
   the browser performs on the document — including the scroll restoration
   that runs when you come back to a page. Instead of arriving where you left
   off, the page arrived at the top and then animated down through the whole
   document, which is the other half of the "page change lags" report. Smooth
   is now asked for explicitly, per scroll, by the code that wants it. */
@media (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */ html { scroll-behavior: auto; } }

/* There is deliberately no `html.is-scrolling` rule here.
   Suppressing tile hover during a scroll was tried, by toggling a class on the
   root element. Measured on Collections, that toggle cost 8ms in the median
   and 17ms at the tail — half a frame to a whole one, spent twice per gesture,
   which is more than the hover churn it was meant to prevent. A class on the
   root that any descendant selector depends on is never cheap on a page with
   two thousand nodes.

   The expensive half of the hover was the transition on `box-shadow`, which
   repaints a large blurred shadow every frame it runs. That transition is gone
   (see `.piece` below); what remains is transform and opacity, which the
   compositor handles and which cost nothing to start, however many of them
   the cursor triggers on the way past. */

/* momentum scrolling on touch devices, and stop the bounce where the page
   has its own scroll containers (the contact panel, the agent, the admin). */
html, body {
  -webkit-overflow-scrolling: touch;
  /* Preserve native edge feedback. Suppressing it made a one-pixel gesture at
     the end of a long collection feel like the page had stuck. Panels keep
     their own contained overscroll above. */
  overscroll-behavior-y: auto;
}

body { transition: opacity .35s ease; }
body.is-pausing { opacity: 0; }
body.is-pausing.is-ready { opacity: 1; }
/* Without JS the page is always visible — no flash of blank screen ever. */
.no-js body, body.is-ready { opacity: 1; }

/* Reveal-on-scroll is now opt-in too. If IntersectionObserver never fires
   (old browser, slow script, or the reveal is already past the fold when JS
   arrives), the safety net in site.js toggles .is-in. But the rule below
   makes content visible by default and only hides it if a script explicitly
   opts the document into reveal mode via the .js-reveal class on <html>. */
.reveal { opacity: 1; transform: none; transition: none; }
.reveal-line { transform: none; transition: none; }
.rise-mask > span { transform: none; transition: none; }

/* Durations here were 1s to 1.5s, with per-block delays of up to half a second
   stacked on top through --rd. A block therefore took up to two seconds to
   finish arriving, and at any ordinary scrolling speed you had already gone
   past it — the page was permanently a beat behind the reader, which is what
   "everything lags" describes as much as any dropped frame does.

   They are roughly a third faster now, and the stagger is scaled rather than
   removed, so the sequencing the design depends on survives while the total
   wait comes down. The scale factor is applied where --rd is consumed, so the
   inline `--rd:.15s` values scattered through the markup did not need to be
   touched. */
html.js-reveal .reveal {
  opacity: 0;
  /* 30px, up from 16. Sixteen was a nudge — enough to be technically present
     and not enough for anyone to notice it had happened, which is the whole of
     the "motion is so low it is not visible" report. Thirty is still well
     under the amplitude that reads as a page throwing itself around. */
  transform: translateY(30px);
  transition: opacity .62s var(--ease), transform .62s var(--ease);
  transition-delay: calc(var(--rd, 0s) * 0.6);
}
html.js-reveal .reveal.is-in { opacity: 1; transform: none; }
html.js-reveal .reveal-line {
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .8s var(--ease);
  transition-delay: calc(var(--rd, 0s) * 0.6);
}
html.js-reveal .reveal-line.is-in { transform: scaleX(1); }
html.js-reveal .rise-mask > span {
  display: block;
  /* Clears the padded clip box (see .rise-mask in site.css). At the old 106%
     the line's cap tops peeked into the padding before it started rising. */
  transform: translateY(135%);
  transition: transform .78s var(--ease);
  transition-delay: calc(var(--rd, 0s) * 0.6);
}
html.js-reveal .rise-mask.is-in > span { transform: none; }

/* Photographs settle rather than simply appear: each arrives fractionally
   over-scaled and eases back to true as its card reveals, on the card's own
   --rd delay so a grid resolves in sequence instead of all at once. It is the
   move a lens makes finding focus, and it is the difference between a page of
   pictures and a page that feels made.

   The scale sits on the <img>; the hover scale sits on the <picture> around
   it, so the two compose instead of cancelling. */
/* The entrance was a zoom out from 1.06, which meant every photograph spent
   the first second of its life with three per cent missing from each edge —
   and stayed there if the reveal observer never fired, which is the state a
   reader with JavaScript blocked or a slow connection would see.

   A rise and a fade carries the same "settling into place" reading at this
   distance and cannot crop anything, because translate moves the picture
   within a slot it already fits rather than growing it past one. */
html.js-reveal .reveal .plate img {
  transform: translateY(14px);
  opacity: 0;
  transition: transform .9s var(--ease), opacity .7s var(--ease);
  transition-delay: calc(var(--rd, 0s) * 0.6);
}
html.js-reveal .reveal.is-in .plate img { transform: none; opacity: 1; }

/* The caption lifts with the image it belongs to. */
.work__cap { transition: transform .6s var(--ease); }
.work:hover .work__cap { transform: translateY(-4px); }

@media (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */
  /* opacity as well as transform: the entrance now fades, and without this
     a reader with motion reduced would be left with a picture stuck at zero
     opacity — no animation at all is fine, an invisible photograph is not. */
  html.js-reveal .reveal .plate img { transform: none !important; opacity: 1 !important; }
  .work:hover .work__cap { transform: none; }
}

/* Movement off, fade kept — see the reduced-motion block in site.css for the
   reasoning. This rule used to also force `opacity: 1 !important`, and because
   this stylesheet loads after site.css it won that fight, which meant every
   reveal on the site was pinned fully visible from first paint. Together with
   the JS having skipped the observer entirely, that is why a phone with
   Battery Saver on showed a completely static page. Only the transform is
   forced here now; opacity is left to the reveal to animate. */
@media (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */
  html.js-reveal .reveal,
  html.js-reveal .reveal-line,
  html.js-reveal .rise-mask > span { transform: none !important; }
}

/* ---------- VARIATION BADGE ----------
   A tile that stands for several photographs of the same piece says so, and
   the lightbox walks those angles before moving on. Without it a wall either
   repeats itself four times or hides work the studio actually did. */
.piece__more {
  position: absolute; top: 14px; left: 14px; z-index: 5;
  font-family: var(--f-mono); font-size: .5rem; letter-spacing: .16em;
  text-transform: uppercase;
  padding: 5px 9px;
  color: var(--marble);
  /* Opaque enough to read without a backdrop blur behind it.
     `backdrop-filter: blur(3px)` was here, and on a touch device the rule at
     the foot of this block makes every one of these badges visible — so the
     browser was re-blurring the photograph behind each badge on every frame of
     every scroll. backdrop-filter is the most expensive thing you can put in a
     scroll path, and at 9px of type over a dark chip nobody can see the blur. */
  background: rgba(20,17,12,.78);
  border: 1px solid rgba(201,163,90,.55);
  opacity: 0; transform: translateY(-4px);
  transition: opacity .45s var(--ease), transform .45s var(--ease);
}
.piece:hover .piece__more,
.piece:focus-visible .piece__more { opacity: 1; transform: none; }
@media (hover: none) { .piece__more { opacity: 1; transform: none; } }

/* The lightbox while the next photograph decodes. The picture that is still
   on screen simply dims — it never blanks — so pressing an arrow reads as a
   response rather than a stall. */
/* The stage is never empty now — the tile's own thumbnail is standing in from
   the moment of the press, and the full file replaces it when it lands. So
   this state no longer means "nothing to look at yet", it means "what you are
   looking at is about to get sharper". Dimming it to 55% behind a spinner
   would be actively worse than the picture it is covering.

   Instead the stand-in is softened a hair, and it resolves. The spinner only
   appears if the wait runs past 450ms — long enough that on any normal
   connection it is never seen at all, and short enough to reassure on a bad
   one. */
.lb .lb__img { transition: opacity .28s var(--ease), filter .32s var(--ease); }
.lb.is-loading .lb__img { filter: blur(6px) saturate(1.06); }

.lb.is-loading .lb__stage::before {
  content: "";
  position: absolute; left: 50%; top: 50%; z-index: 8;
  width: 26px; height: 26px; margin: -13px 0 0 -13px;
  border: 1px solid rgba(247,242,233,.28);
  border-top-color: var(--gold-2);
  border-radius: 50%;
  opacity: 0;
  animation: lb-spin .7s linear infinite, lb-wait .001s linear .45s forwards;
}
@keyframes lb-spin { to { transform: rotate(360deg); } }
@keyframes lb-wait { to { opacity: 1; } }
/* ==========================================================================
   PATCH ADDITIONS - image aspect-ratio standardization + rail arrow smoothness
   ========================================================================== */

/* The block that used to stand here pinned every wall tile and every rail
   card to a single 4/5 frame, on the reasoning that one ratio across the
   site reads as a museum hang rather than a ragged mosaic. It does — but
   only if the photographs are that shape, and these are not. See
   PHOTOGRAPHS SHOW WHOLE at the foot of this file. */

/* ==========================================================================
   RAIL ARROW SMOOTHNESS - eliminate the press-then-crawl jerk.
   The previous .4s hover transition on .card media plus a transform on the
   card itself meant every press invalidated the next card's hover state
   mid-flight. The transform is short and crisp now, and the card media
   no longer transitions while a rail glide is in progress. */
.rail.is-railing .card__media .plate { transition: none; }
.rail.is-railing .card { transition: none; }
.rail.is-railing .card:hover { transform: none; }

/* ==========================================================================
   LIGHTBOX SMOOTHNESS - the arrow press transitions the image with a
   compositor-only opacity/transform rather than re-arranging layout. */
.lb .lb__img {
  transition: opacity .3s var(--ease), transform .3s var(--ease), filter .3s var(--ease);
  will-change: opacity, transform;
}
.lb.is-step .lb__img { transform: scale(.985); opacity: .85; }

/* ==========================================================================
   PAGE-CHANGE STUTTER FIX - the previous body transition on opacity ran
   0.35s on every navigation regardless of state, which read as a fade
   blank between pages. The class is no longer toggled, but the transition
   is also shortened to .2s and gated to actually running only on
   .is-pausing, so it never fires as a side-effect. */
body { transition: opacity .2s ease; }
body:not(.is-pausing) { transition: none; }

/* ==========================================================================
   HEADING VISIBILITY - a soft warm shadow on display headings so they
   never look washed out where they sit over a faint art plate or a
   radial gradient. Pure cosmetic but it stops the 'invisible heading'
   report on the page head and on the hero. */
.display { text-shadow: 0 1px 0 rgba(245, 241, 232, 0.6); }
.band--dark .display, .foot .display, .proc .display, .stat .display {
  text-shadow: 0 1px 0 rgba(20, 17, 12, 0.6);
}

/* ==========================================================================
   ART CROPPING FIX - the phead__art mask previously cut the left 13% of
   every drawing to a hard transparent edge, which read as the artwork
   being sliced off where it met the page. The mask now softens over a
   narrower margin so the full drawing is visible. */
.phead__art {
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 4%) !important;
  mask-image: linear-gradient(to right, transparent 0%, #000 4%) !important;
}
/* And the phead section itself no longer clips art to its own bounds -
   several phead drawings were placed with right:4% and overflowed the
   section, so a hard clip hid part of them. */
.phead { overflow: visible; }

/* ==========================================================================
   MOBILE PARITY (continued) - every premium effect that runs on desktop
   also runs on phone. Earlier breakpoints quietly dropped: pillar hover
   wash, .work caption lift, .piece zoom icon, lightbox smooth swipe,
   ambient plate breathe, hero reel. Most of these are already restored
   by removing their @media (max-width: ...) exceptions, but a few
   needed to be re-stated explicitly so they survive on touch devices. */
@media (max-width: 760px) {
  /* Captions stay visible on the wall on every device. */
  .piece__cap { opacity: 1 !important; transform: none !important; }
  .piece__zoom { opacity: .78 !important; transform: none !important; }
  .piece__veil { opacity: 1 !important; }
  /* Card media continues to scale on hover via :active on touch. */
  .card:active .card__media .plate { transform: scale(1.03); }
  .work:active .work__media .plate { transform: scale(1.03); }
  /* The work caption lifts on tap, the same move it makes on hover. */
  .work:active .work__cap { transform: translateY(-3px); }
  /* The work__line draws in on tap. */
  .work:active .work__line { width: 72px; }
}

/* ==========================================================================
   WALL CAPTION HEIGHT - equalise so tiles in the same row align cleanly.
   The .piece__below caption has variable text length, so even with a
   uniform image aspect ratio the total tile height differed and the
   grid rows looked uneven. The caption is now a fixed minimum height
   so the bottom edge of every tile in a row sits on the same line. */
.wall .piece__below {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}


/* ==========================================================================
   LEG-STUCK / SCROLL STUTTER FIX
   The page felt like it was "sticking" while scrolling on both desktop and
   mobile. The root causes were:
   1. CSS `content-visibility: auto` was skipping sections but the contain
      was too aggressive — when a section entered view, the layout work it
      had been skipping all ran at once, causing a frame hitch.
   2. The `:not(:has(.art))` selector skipped sections containing artwork,
      but every section with art was still being painted eagerly even when
      far off-screen.
   3. The body opacity transition was 0.2s on `body:not(.is-pausing)` which
      is correct, but the .is-pausing class was never set anywhere, so the
      transition was effectively disabled — yet the `transition: opacity`
      property itself still triggers style invalidation when the body's
      opacity changes for any other reason (e.g., when the contact panel
      opens).
   4. The reveal animations had transition-delay up to 0.6s, so when the
      user scrolls quickly past a section, reveals are still pending and
      force paint work even after the section is off-screen.

   Fixes applied below:
   - Remove the body opacity transition entirely (it does nothing useful).
   - Cap reveal transition-delays so a section's content settles quickly.
   - Add `contain: layout style paint` to the .band so each band is its
     own paint island. This is the single biggest scroll perf win.
   - Add `content-visibility: auto` ONLY to bands that are far from the
     viewport (rootMargin-aware via the existing IntersectionObserver).
   ========================================================================== */
body { transition: none !important; }
body.is-pausing { opacity: 1; }

/* Each band is its own paint island — scrolling no longer invalidates
   style across the whole document. The .art-gate inside still composites
   correctly because it's positioned absolutely and is its own stacking
   context (translateZ(0) is already set in art.css). */
.band { contain: style; }

/* Reveal delays are scaled down — was up to 0.6s, now capped at 0.18s so
   a section's content is visible almost immediately when scrolled to. */
html.js-reveal .reveal {
  transition-duration: .42s !important;
  transition-delay: calc(var(--rd, 0s) * 0.35) !important;
}
html.js-reveal .reveal-line {
  transition-duration: .55s !important;
  transition-delay: calc(var(--rd, 0s) * 0.35) !important;
}
html.js-reveal .rise-mask > span {
  transition-duration: .5s !important;
  transition-delay: calc(var(--rd, 0s) * 0.35) !important;
}

/* ==========================================================================
   PAGE TRANSITION — add a subtle cross-fade between pages so navigation
   feels continuous rather than a hard jump. Uses view-transition-name
   which is supported in Chromium browsers and degrades gracefully
   elsewhere. */
@supports (view-transition-name: none) {
  /* The rule that was missing.
     The two selectors below have been in this file all along, styling a
     cross-fade that never once ran: a multi-page site does not get view
     transitions unless it asks for them, and nothing asked. So every
     navigation between Story, Collections, Portfolio and the rest was a hard
     white cut, with a styled 0.18s fade sitting in the stylesheet unused.

     One at-rule turns it on. Chromium cross-fades the old page into the new
     one; every other browser ignores the at-rule entirely and navigates
     exactly as it does today. 0.28s rather than the 0.18s below, because at
     0.18 the fade reads as a flicker rather than as a movement — long enough
     to be seen, short enough that nobody waits for it. */
  /* Cross-document snapshots are deliberately not enabled. They retain a
     full-page bitmap while the next document and its films initialise, which
     adds visible latency and occasional black frames on mobile browsers. */

  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.28s;
    animation-timing-function: var(--ease, cubic-bezier(.22,.61,.36,1));
  }

  /* The header does not cross-fade with the page. It is in the same place on
     every page, so fading it out and a near-identical one back in makes the
     one fixed thing on the site flicker at every navigation. Naming it takes
     it out of the root snapshot and holds it still while the page changes
     underneath — which is the whole trick behind navigation that feels like
     an app rather than a series of documents. */
  .nav { view-transition-name: site-header; }
  ::view-transition-old(site-header),
  ::view-transition-new(site-header) { animation: none; mix-blend-mode: normal; }

  /* Someone who has asked for less motion gets the navigation without it. */
  @media (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */
    ::view-transition-old(root),
    ::view-transition-new(root) { animation-duration: 0.01s; }
  }
}

/* ==========================================================================
   IMAGE LOADING — prevent layout shift and image reflow during scroll.
   Images now have a stable background color while loading so the page
   doesn't jump as they arrive. */
.plate, .piece__img, .card__media, .work__media, .pcard__media, .frame {
  background: var(--stone-200);
  position: relative;
}
.plate img, .piece__img img, .card__media img, .work__media img,
.pcard__media img, .frame img {
  background: var(--stone-200);
}

/* ==========================================================================
   NAV — draws out of the way going down, returns on the way up.
   The bar transforms again, so it keeps its compositor hint; see the nav
   block lower in this file and .nav--hidden in site.css. */
/* No `will-change` on .nav — it would make the header a containing block
   for the fixed menu panel inside it. See the nav block further down. */
/* NO BACKDROP BLUR ON THE BAR THAT IS ON SCREEN DURING EVERY SCROLL.
   -------------------------------------------------------------------------
   backdrop-filter re-reads and re-blurs whatever is behind the element, and a
   sticky header has the whole page moving behind it — so the blur is
   recomputed on every frame of every scroll, for the entire visit, on the one
   element that is never off screen. It is the most expensive thing that can
   be put on a fixed bar, and this file says so in three other places where it
   was already taken out for exactly that reason.

   It had been removed here too — but only inside a max-width: 480px query.
   That covers a phone and nothing else: an iPad, a Mac, a desktop and any
   Android tablet all kept the blur and all paid for it on every frame. The
   removal is now unconditional, which is what the earlier note intended.

   Nothing is lost visually. The alpha was already doing most of the work at
   0.92 and goes to 0.98, which is the value the phone rule had settled on —
   and the argument for it is already written into .nav--stuck::before below:
   a fixed bar is opaque or it is not a bar. */
.nav--stuck {
  background: rgba(233, 227, 213, 0.98);
}






/* ==========================================================================
   TEXT BELOW IMAGES — NO OVERLAY CAPTIONS
   The user reported text on images is unreadable. The overlay captions
   (.piece__cap, .work__cap) sat on top of photographs with a dark gradient
   veil, which made both the text AND the image hard to read.
   
   Now: the overlay caption and veil are hidden entirely. The below-image
   caption (.piece__below) carries the title and material label underneath
   the photograph, where text belongs.
   ========================================================================== */

/* Hide the overlay caption on pieces — text goes below the image */
.piece__cap {
  display: none !important;
}
.piece__veil {
  display: none !important;
}
.piece__zoom {
  display: none !important;
}

/* Hide the overlay caption on works — text goes below the image */
.work__cap {
  position: static !important;
  padding: 16px 0 0 !important;
  width: 100% !important;
  transform: none !important;
  opacity: 1 !important;
}
.work__veil {
  display: none !important;
}
.work__t {
  color: var(--ink) !important;
}
.work__meta {
  color: var(--accent) !important;
}
.work__s {
  color: var(--ink-soft) !important;
}
.work__line {
  background: var(--accent) !important;
}

/* Make piece__below always visible and properly styled */
.piece__below {
  display: block !important;
  padding: 12px 0 0 !important;
}
.piece__below .piece__t {
  color: var(--ink) !important;
  font-size: 1rem !important;
  margin-bottom: 4px !important;
}
.piece__below .piece__m {
  color: var(--ink-mute) !important;
  font-size: 0.7rem !important;
}

/* Remove the zoom cursor since there's no overlay */
.piece {
  cursor: pointer;
}

/* ==========================================================================
   SMALLER IMAGES ON MOBILE — prevent oversized tiles
   The 4/5 portrait ratio on a 2-column mobile grid made each tile ~280px
   wide by 350px tall, which is too big. Now mobile uses a wider 1/1 ratio
   so tiles are shorter and more fit on screen.
   ========================================================================== */
@media (max-width: 760px) {
  /* Works grid: smaller min-height */
  .works {
    grid-auto-rows: minmax(180px, 1fr) !important;
  }
  .works .work {
    min-height: 180px !important;
  }
}

@media (max-width: 480px) {
  .works {
    grid-auto-rows: minmax(160px, 1fr) !important;
  }
  .works .work {
    min-height: 160px !important;
  }
}

/* ==========================================================================
   SCROLL LAG FIX — remove expensive operations
   1. Remove CSS filters on SVG art (sepia/saturate/contrast/brightness)
      These force re-rasterization of the entire SVG on every paint.
   2. Keep ambient animation compositor-only and pause it off screen.
   3. Avoid will-change on nav because it changes the fixed menu containing block.
   4. Remove backdrop-filter from nav (expensive on scroll).
   ========================================================================== */

/* Remove the expensive art filters */
.art > svg, .hero__art > svg, .phead__art > svg {
  filter: none !important;
}
.art[data-art="peacock"] > svg,
.art[data-art="peacock-grove"] > svg {
  filter: none !important;
}

/* The plate breathe and capability ticker are transform/opacity-only. Their
   existing IntersectionObservers pause them off screen, so removing them was
   a visible loss of finish without buying scroll performance. */

/* Nav: no backdrop-filter, and the transform IS transitioned — but there is
   deliberately no `will-change` on this element, and that is not an
   oversight.

   `will-change: transform` was added here when the header started hiding on
   scroll again, on the reasoning that a hint belongs on the thing that
   animates. It broke the phone menu, completely and invisibly on a laptop.
   `will-change: transform` promotes the element to a containing block for
   `position: fixed` descendants — the same as an actual transform does — and
   the menu panel is `position: fixed; inset: 0` INSIDE this header. So
   "cover the viewport" started resolving against the header strip instead:
   the panel measured 375x128 on a 390x844 phone, and all five links sat
   below its bottom edge, clipped away. The header is 77px tall, so the menu
   was a 128px sliver with nothing readable in it.

   The transition composites perfectly well without the hint. A hint that
   changes layout semantics is not worth a frame that was never dropped.

   The same trap applies to the transform itself while the bar is hidden,
   which is why site.js refuses to hide the header while the menu is open and
   .nav--open.nav--hidden forces the transform back off (site.css). */
.nav {
  transition: transform .46s cubic-bezier(.22,.61,.36,1),
              background-color .35s,
              border-color .35s !important;
}
.nav--stuck {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  background: rgba(233, 227, 213, 0.98) !important;
}

/* Spine: remove will-change (it's on a fixed element, not needed) */
.spine__fill, .spine__head {
  will-change: auto !important;
}

/* The agent's ring and typing dots are deliberately NOT switched off here,
   unlike the ticker and the filters above them.

   Both animate opacity and transform on pseudo- or leaf elements only, which
   the compositor handles without touching layout, paint or the main thread —
   so neither costs the frames this pass was written to recover. The ring is
   the launcher's only signal that it is live (the reference widget uses the
   same pulse for the same reason), and the dots are the assistant's only
   "thinking" state: pinned at opacity 0.5 with no animation, a visitor who
   had just asked a question saw three static grey dots and no sign that
   anything was happening.

   Anyone who has asked for less movement still gets none — see the
   prefers-reduced-motion guards beside each animation further up. */

/* The availability pulse is a six-pixel compositor animation; it stays as the
   footer's live-workshop signal. */

/* ==========================================================================
   WORK TILE STRUCTURE FIX
   The work__cap div is inside work__media which has overflow:hidden and
   the image is absolutely positioned covering the entire container.
   Even with position: static on work__cap, it's hidden behind the image.
   
   Fix: make work__media a flex column, image on top, caption below.
   ========================================================================== */
.works .work__media {
  display: flex !important;
  flex-direction: column !important;
  overflow: visible !important;
  aspect-ratio: auto !important;
  height: auto !important;
  position: relative !important;
  min-height: 0 !important;
  /* The loading placeholder ground belongs behind the PICTURE, not behind the
     caption. IMAGE LOADING below paints --stone-200 on .work__media to hold
     the box while a photograph arrives — which was right when the caption was
     an overlay on top of the image, and stopped being right the moment the
     caption moved into the flow underneath it. The caption then sat on a grey
     panel a shade darker than the band, and the bronze meta line on it came
     out at 3.9:1 instead of the 5.1:1 the same colour gets on sand.

     .plate, inside, still carries the placeholder — so nothing about the
     no-layout-shift behaviour changes; only the strip under the picture goes
     back to being the band it is standing on. */
  background: transparent !important;
}

/* The plate takes the photograph's own shape; see PHOTOGRAPHS SHOW WHOLE. */
.works .work__media .plate {
  position: relative !important;
  inset: auto !important;
  width: 100% !important;
  height: auto !important;
  flex: none !important;
}

/* The caption flows below the image naturally */
.works .work__media .work__cap {
  position: static !important;
  padding: 14px 0 0 !important;
  width: 100% !important;
  transform: none !important;
  opacity: 1 !important;
  z-index: auto !important;
}

/* Remove the dark gradient veil */
.works .work__media .work__veil {
  display: none !important;
}

/* Make the work title dark text on light background, not white on dark */
.works .work__t {
  color: var(--ink) !important;
}
.works .work__meta {
  color: var(--accent) !important;
}
.works .work__s {
  color: var(--ink-soft) !important;
}
.works .work__line {
  background: var(--accent) !important;
}

/* The .work itself should be position relative, not absolute */
.works .work {
  position: relative !important;
  min-height: 0 !important;
  height: auto !important;
}
.works > div {
  height: auto !important;
}

/* Remove the grid-auto-rows fixed height - let tiles be natural height */
.works {
  grid-auto-rows: auto !important;
}
.works > .w-5 {
  grid-template-rows: auto auto !important;
}


/* ==========================================================================
   PREMIUM COLLECTIONS STRUCTURE
   The previous layout had images that were too big (4/5 portrait ratio made
   each tile 350px+ tall), text that was too small (0.95rem titles), and large
   empty gaps between sections. This block creates a premium, compact gallery:
   
   - 3/2 landscape aspect ratio for wall tiles (shorter, more per screen)
   - Larger title text (1.05rem) and material labels (0.78rem)
   - Premium hover: subtle lift + soft shadow + image scale
   - No empty gaps — sections flow continuously
   - Consistent grid with no masonry raggedness
   ========================================================================== */

/* Larger, more readable text below images */
.wall .piece__below {
  padding: 14px 0 0 !important;
  min-height: 0 !important;
}
.wall .piece__below .piece__t {
  font-size: 1.05rem !important;
  font-family: var(--f-display) !important;
  color: var(--ink) !important;
  margin-bottom: 4px !important;
  line-height: 1.2 !important;
}
.wall .piece__below .piece__m {
  font-size: 0.78rem !important;
  letter-spacing: 0.12em !important;
  color: var(--ink-mute) !important;
}
.wall .piece__below .piece__m span {
  font-size: 0.78rem !important;
}

/* Premium hover effect on wall tiles */
.wall .piece {
  transition: transform .4s var(--ease), box-shadow .4s var(--ease) !important;
  background: var(--marble) !important;
  box-shadow: 0 1px 3px rgba(36, 31, 24, 0.08) !important;
}
.wall .piece:hover {
  transform: translateY(-3px) !important;
  box-shadow: 0 12px 28px -10px rgba(36, 31, 24, 0.25) !important;
}
.wall .piece .piece__img {
  overflow: hidden !important;
}
.wall .piece .piece__img img {
  transition: transform .6s var(--ease) !important;
}
.wall .piece:hover .piece__img img {
  transform: scale(1.04) !important;
}

/* Tighter gap between rows so there's no empty space */
.wall {
  gap: clamp(16px, 2vw, 28px) !important;
}

/* Remove the min-height that was causing empty gaps */
.wall .piece__below {
  min-height: 0 !important;
}

/* Mobile: keep 2 columns but with 3/2 ratio (not 1/1 which looked too small) */
@media (max-width: 760px) {
  .wall .piece__below .piece__t {
    font-size: 0.95rem !important;
  }
  .wall .piece__below .piece__m {
    font-size: 0.72rem !important;
  }
  .wall {
    gap: 14px !important;
  }
}

/* ==========================================================================
   HOMEPAGE BEST IMAGES — premium editorial grid
   The homepage "Selected work" section should show only the BEST images to
   attract clients. The works grid uses 3/2 landscape for a clean, editorial
   look. Text below each image is larger and more readable.
   ========================================================================== */
.works .work__cap .work__t {
  font-size: clamp(1.3rem, 2vw, 1.7rem) !important;
  font-family: var(--f-display) !important;
  color: var(--ink) !important;
}
.works .work__cap .work__meta {
  font-size: 0.78rem !important;
  color: var(--accent) !important;
}
.works .work__cap .work__s {
  font-size: 0.95rem !important;
  color: var(--ink-soft) !important;
  line-height: 1.6 !important;
}

/* Premium hover on work tiles */
.works .work {
  transition: opacity .4s var(--ease) !important;
}
.works .work .work__media .plate {
  transition: transform .6s var(--ease) !important;
  overflow: hidden !important;
}
.works .work:hover .work__media .plate {
  transform: scale(1.03) !important;
}
.works .work .work__media .plate img {
  transition: transform .6s var(--ease) !important;
}
/* The plate above is already scaled on hover, and .works .work__media is
   overflow:visible, so the whole tile grows — picture and ground together,
   nothing cropped. This second scale on the picture alone grew it inside a
   box that did not move, which took an edge off every tile under the cursor
   once the photographs were fitted rather than filled. */
.works .work:hover .work__media .plate img {
  transform: none !important;
}

/* ==========================================================================
   REMOVE EMPTY GAPS — sections flow continuously
   The previous content-visibility and min-height rules created large blank
   gaps between sections. Now sections flow naturally without reserved space.
   ========================================================================== */
.band {
  min-height: 0 !important;
}
.band--tight {
  padding-block: clamp(40px, 5vw, 70px) !important;
}
.band {
  padding-block: clamp(50px, 6vw, 90px) !important;
}

/* The `contain-intrinsic-size: auto 0` override that used to sit here is
   gone, together with the `content-visibility: auto` it was overriding (see
   site.css). Between them they reported every off-screen section as zero
   pixels tall, which is what made the page impossible to scroll. */

/* ==========================================================================
   RAIL CARDS — premium compact sizing
   ========================================================================== */
.rail .card {
  background: var(--marble) !important;
  box-shadow: 0 1px 3px rgba(36, 31, 24, 0.08) !important;
  transition: transform .4s var(--ease), box-shadow .4s var(--ease) !important;
}
.rail .card:hover {
  transform: translateY(-3px) !important;
  box-shadow: 0 12px 28px -10px rgba(36, 31, 24, 0.22) !important;
}
.rail .card__t {
  font-size: 1.2rem !important;
  font-family: var(--f-display) !important;
}
.rail .card__d {
  font-size: 0.92rem !important;
  line-height: 1.6 !important;
}


/* ==========================================================================
   FEATURE MEDIA + WALL--2 FIX
   The Garden section uses .feature with inline aspect-ratio:4/5 which made
   images very tall. Force all feature media and wall--2 tiles to 3/2 ratio.
   ========================================================================== */
/* Feature stamp positioning fix */
.feature__stamp {
  position: static !important;
  max-width: none !important;
  margin: 18px 0 0 !important;
  display: block !important;
}



/* ==========================================================================
   GARDEN SECTION FIX — parasol bases need to show full product, not cropped
   The parasol bases are tall products. Using object-fit: cover cuts off the
   base (the main stone item). Now using object-fit: contain so the full
   product is visible. Background is a neutral stone color so the contain
   padding looks intentional.
   ========================================================================== */
/* Lightweight raking light. The old procedural turbulence filters repainted
   large plates while scrolling; these static gradients give the stone ground
   depth without a filter or an image decode. Photographs remain above them. */
.plate::before,
.plate::after {
  display: block !important;
  content: "" !important;
  inset: 0 !important;
  mix-blend-mode: soft-light !important;
  pointer-events: none !important;
}
.plate::before {
  background: linear-gradient(118deg, rgba(255,255,255,.22), transparent 38%, rgba(36,31,24,.09)) !important;
  opacity: .7 !important;
}
.plate::after {
  background: radial-gradient(ellipse at 18% 12%, rgba(255,255,255,.18), transparent 48%) !important;
  opacity: .55 !important;
}

/* Tighter garden section spacing */
#garden .wall {
  /* The variable, not `gap` — `.wall` applies `gap: var(--wall-gap)` and the
     wide-tile height is computed from the same variable. Setting `gap` here
     directly (this selector outranks `.wall`) is what put the garden's
     double-width tile 3.6px out of line with the row beside it. */
  --wall-gap: clamp(16px, 2vw, 24px);
  margin-top: clamp(30px, 4vw, 54px);
}
#garden.band {
  padding-block: clamp(40px, 5vw, 70px) !important;
}

/* Remove empty space - reduce band padding globally */
.band {
  padding-block: clamp(40px, 5vw, 70px) !important;
}
.band--tight {
  padding-block: clamp(30px, 4vw, 50px) !important;
}

/* Remove plate background gradients (the stone texture) */
.plate {
  background: var(--stone-100) !important;
}
.plate--marble,
.plate--sandstone,
.plate--basalt,
.plate--jade {
  background: var(--stone-100) !important;
}

/* ==========================================================================
   GARDEN — the parasol bases, in three tries and then none
   --------------------------------------------------------------------------
   Three blocks used to sit here, written one after another, each trying to
   stop the parasol bases being cut off and each undoing the one before it:
   3/4 portrait with cover, then cover pinned to the top edge, then 1/1 with
   contain on a cream background. The last one won, so the tall products
   ended up letterboxed inside a square — the base shrunk to a strip in the
   middle with cream above and below it, which is what an "empty card" looks
   like.

   None of them was needed. A parasol base photographed at 1122x1402 is not
   cut off by anything if its tile is 1122x1402, and that is now what every
   tile on the site does — see PHOTOGRAPHS SHOW WHOLE at the foot of this
   file. What is left here is the spacing, which was the only part of those
   three blocks that was doing any good.

   The three-column override went with them. It carried an id in the selector
   and !important, so it beat the responsive rules and held Garden at three
   columns down to a 360px phone: tiles about 100px wide, of a carved stone
   base.
   ========================================================================== */
#garden.band {
  padding-block: clamp(35px, 4vw, 60px) !important;
}
#garden .wall {
  --wall-gap: clamp(14px, 1.8vw, 20px);
}
#garden .piece__below {
  min-height: 0 !important;
  padding: 10px 0 0 !important;
}

/* ==========================================================================
   PHONE RHYTHM — spacing that was tuned for a 1400px canvas
   Measured on a 390px screen: the six section headers cost 1,470px between
   them and the two process panels 970px, and a large share of both was
   padding scaled for a desktop column rather than a phone one. None of the
   copy is touched — only the space around it, which on a laptop reads as
   composure and on a phone reads as scrolling past nothing.
   ========================================================================== */
@media (max-width: 760px) {
  /* Process panels: the inner padding floor was 30px on all four sides, set
     for a half-width dark card, not a full-width one. */
  .proc { padding: 24px 20px 26px; }
  .proc__k { margin-bottom: 14px; }
  .proc__t { margin-bottom: 11px; }
  .proc__d { line-height: 1.62; }
  .proc__list { margin-top: 18px; }
  .proc__list li { padding: 9px 0; }
  /* The key/value rows wrap awkwardly at this width — "BEST FOR" breaking
     across two lines against its value. Letting the pair stack keeps each on
     one line and is shorter than the wrapped version. */
  .proc__list li { gap: 4px; flex-wrap: wrap; }
  .proc__list li span:last-child { flex: 1 0 auto; text-align: right; }

  /* Section headers: title and aside stack here, so the gap between them is
     doing twice the work it was asked to do. */
  .sec-head { margin-bottom: 20px; }
  .sec-head__aside { padding-top: 10px; }

  /* The editorial feature blocks (Lineage and friends) */
  .feature__text { padding-top: 16px; }

  /* Footer. The padding here was 18px and, because this sheet loads after
     site.css, it quietly overrode the tighter value set alongside the rest of
     the footer rhythm there. One number, in one place. */
}

/* ==========================================================================
   THE WORKSHOP ADDRESS
   --------------------------------------------------------------------------
   Set the way a maker's mark is set, not the way a data field is: a mono
   label, the studio name in the display serif, the street stacked line by line
   rather than running as one wrapping sentence, and a hairline down the left
   that draws itself when the block arrives. The whole card is the link to the
   map, so the address is the affordance instead of a button beside it.
   ========================================================================== */
.fadr {
  position: relative;
  /* Tightened along with the rest of the footer. These values were set when
     this block sat on its own with room around it; in a column beside three
     link lists they were the difference between the tallest column and the
     rest.

     The 14px indent went with the hairline below. Both existed to mark this
     block as something apart, and it is not — it is one column of four, and
     indenting it left its text out of line with the three heads beside it. */
  margin-top: clamp(12px, 1.4vw, 16px);
  padding-left: 0;
}
/* The gold hairline that used to run down the left of this block is gone. It
   was the strongest signal that the address was a different kind of thing from
   the columns beside it — and in a footer whose whole colour scheme is one
   off-white at varying strength, a bronze rule was the only saturated thing on
   the page. Nothing replaces it: the column heading above already says what
   this block is. */

/* Deliberately identical to .foot__head — same size, same tracking, same
   strength. It sits in the same row as "Studio", "Collections" and
   "Enquiries" and there is no reason for it to be a different colour from
   them, let alone the only bronze text in the footer. */
.fadr__k {
  font-family: var(--f-mono);
  font-size: 10px;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  color: rgba(240, 238, 230, 0.4);
  margin-bottom: 8px;
}
.fadr__body {
  display: block;
  color: inherit;
}
/* Was the display serif at 16-19px in full-strength marble — the largest and
   brightest text in the footer, for a line that is a shop name in a colophon.
   It is now the same face and size as the links around it, one step brighter
   than the address under it because it is the name. On a phone the display
   face was also wrapping it onto three lines. */
.fadr__studio {
  display: block;
  font-family: inherit;
  font-size: 13.5px;
  line-height: 1.45;
  font-weight: 500;
  color: rgba(240, 238, 230, 0.75);
  margin-bottom: 6px;
  letter-spacing: 0;
}
.fadr__lines { display: block; }
/* Each line rises a little after the one above it — the address assembling
   rather than appearing. */
/* 0.845rem resolved to 17px against the page's root size and was set in
   --on-dark-mute, which is very nearly full brightness — so the postal address
   was both the biggest and the whitest text in the footer. Same size and same
   strength as a link now. */
.fadr__line {
  display: block;
  font-size: 13.5px;
  line-height: 1.5;
  color: rgba(240, 238, 230, 0.6);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity .5s var(--ease), transform .5s var(--ease);
  transition-delay: var(--fd, 0s);
}
html.js-reveal .fadr.is-in .fadr__line,
html:not(.js-reveal) .fadr__line { opacity: 1; transform: none; }

/* The second bronze label, at the same strength as the footer's own heads
   now. The arrow still travels on hover — that gesture costs no colour. */
.fadr__go {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 10px;
  font-family: var(--f-mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(240, 238, 230, 0.4);
  opacity: 1;
  transition: color .3s, gap .3s var(--ease);
}
.fadr__body:hover .fadr__go { color: rgba(240, 238, 230, 0.8); gap: 12px; }
/* Hovering brightens the name within the same ramp rather than turning it
   bronze — the only accent left in this footer is the status dot. */
.fadr__body:hover .fadr__studio { color: rgb(240, 238, 230); }

/* The two things a phone can do with this. Deliberately sized as real targets
   rather than text links — on the device most of this audience arrives on,
   calling and WhatsApp are the whole point of an address block. */
.fadr__acts {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 13px;
}
.fadr__act {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 9px 14px;
  border: 1px solid var(--rule-dark);
  color: var(--on-dark);
  font-size: 0.82rem;
  letter-spacing: .01em;
  transition: border-color .35s, background .35s, transform .35s var(--ease);
}
.fadr__act svg { width: 15px; height: 15px; flex: none; opacity: .85; }
.fadr__act:hover {
  border-color: var(--gold-2);
  background: rgba(194,170,126,.09);
  transform: translateY(-2px);
}
.fadr__act--wa:hover {
  border-color: #4d9c6a;
  background: rgba(77,156,106,.12);
}
@media (hover: none) {
  .fadr__act:active { background: rgba(194,170,126,.14); transform: none; }
}
@media (max-width: 620px) {
  .fadr__acts { gap: 8px; }
  .fadr__act { flex: 1 1 calc(50% - 4px); justify-content: center; }
}

/* ---- the same details inside the contact panel ---- */
.chan__addr {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--rule-dark);
}
.chan__k {
  font-family: var(--f-mono);
  font-size: 0.55rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--gold-2);
  margin-bottom: 9px;
}
.chan__addr-t {
  font-family: var(--f-display);
  font-size: 1rem;
  color: var(--marble);
  margin-bottom: 5px;
}
.chan__addr-l {
  font-size: 0.85rem;
  line-height: 1.6;
  color: var(--on-dark-mute);
  max-width: 34ch;
}
.chan__addr-go {
  display: inline-block;
  margin-top: 9px;
  font-family: var(--f-mono);
  font-size: 0.55rem;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--gold-2);
  transition: opacity .3s;
}
.chan__addr-go:hover { opacity: .7; }


/* ==========================================================================
   PHOTOGRAPHS SHOW WHOLE
   --------------------------------------------------------------------------
   Every media slot on the site had been pinned to a fixed shape. Not one
   shape — five, in five successive patches, each overriding the last with
   !important: 4/5, then 3/2, then 1/1 on phones, then 4/3, then 3/4 and 1/1
   again for the garden. Whichever one happened to be written last is the one
   the browser used.

   The photographs are not any of those shapes. They are portraits, almost
   all of them between 3:4 and 4:5. Measured on the homepage against the
   ratios that were in force:

     Selected work, tallest tile   1200x2126 in a 3/2 slot   62% discarded
     Selected work, Shrinathji     1200x1600 in a 3/2 slot   47% discarded
     Collections rail, each card   portrait   in a 3/2 slot   37-56% discarded
     Pieces wall                   1122x1402 in a 4/3 slot   letterboxed,
                                                             40% of the tile
                                                             left empty

   Two failures, one cause. Where the slot used object-fit:cover the carving
   was cropped — top of a temple arch gone, base of a parasol gone. Where a
   later patch switched to object-fit:contain to stop that, the crop became
   empty space instead: the picture shrank into the middle of a grey box and
   the tile read as blank. Cropped photographs and empty cards were the same
   bug seen from two sides, and neither is fixable by choosing a better fixed
   ratio, because there isn't one — the pictures genuinely differ.

   So the slot is not given a ratio at all. Every <img> on this site ships
   width and height attributes, which is enough for the browser to know each
   picture's shape before a single byte of the file arrives. Sized from those,
   each slot reserves exactly the right box: nothing is cropped, no space is
   left over, and the layout still never shifts as the images land — which was
   the thing the fixed ratios were protecting in the first place.

   What this costs: a wall's rows are no longer identical to the millimetre.
   A row is as tall as the tallest picture in it and the shorter ones leave a
   little air beneath their captions. On these photographs, which cluster
   tightly around 3:4, that is a few pixels of variation. It is the right
   trade for having the work visible.

   This block is last in the file deliberately. It is the one that decides
   image geometry, and it should stay last.
   ========================================================================== */

/* ---- the slots ------------------------------------------------------
   `aspect-ratio: auto` and `height: auto` on the container; the picture
   inside is what establishes the height. `[style]` is listed alongside each
   bare selector because the markup sets inline ratios (`aspect-ratio:3/2`
   and friends) that would otherwise still win — an author !important
   declaration beats an inline one, which is exactly why these carry it.

   `.piece__img` and `.piece__img[style]` are NOT in this list any more, and
   that is deliberate. The gallery slot's ratio is now chosen per piece in
   the panel and written inline by the renderer, defaulting to `natural` —
   the photograph's own width and height. That default is precisely the
   behaviour this block exists to produce, so overriding it here would only
   stop the owner's deliberate choices from ever reaching the page. The
   other slots keep the override; nothing about them has changed. */
.work__media,
.work__media[style],
.card__media,
.card__media[style],
.pcard__media,
.pcard__media[style],
.feature__media,
.feature__media[style],
.feature__media .frame,
.feature__media .frame[style],
.feature__media .work__media,
.feature__media .work__media[style],
.frame[style*="aspect-ratio"] {
  aspect-ratio: auto !important;
  height: auto !important;
  min-height: 0 !important;
}

/* ---- the plate ------------------------------------------------------
   The plate was absolutely positioned to fill a slot whose height came from
   its aspect-ratio. With the ratio gone there is no height to fill, so it
   returns to the flow and carries the height itself. */
.work__media .plate,
.card__media .plate,
.pcard__media .plate,
.feature__media .plate {
  position: relative !important;
  inset: auto !important;
  width: 100% !important;
  height: auto !important;
  aspect-ratio: auto !important;
  display: block !important;
}

/* ---- the photograph -------------------------------------------------
   `height: auto` with the intrinsic width/height attributes is what makes
   the box the picture's own shape. object-fit then has nothing left to do —
   the box and the image already agree — so whichever value is inherited from
   the rules above is harmless, and `contain` is named only to make that
   explicit. The padding and background come off with it: both were there to
   dress the empty space that contain used to leave, and there is none now. */
.piece__img img,
.piece__img picture,
.work__media .plate img,
.card__media .plate img,
.pcard__media .plate img,
.pcard__media > img,
.feature__media .frame img,
.feature__media .frame picture,
.feature__media .plate img,
.feature__media > img,
.wall .piece__img img,
.works .work__media .plate img {
  position: static !important;
  inset: auto !important;
  width: 100% !important;
  /* Was `auto`. That is what let a picture taller than its slot render at its
     own height inside a box with overflow:hidden — the slot then cut the
     bottom off. Filling the slot is what gives object-fit something to fit
     into, and fitting is what guarantees the whole piece stays visible. */
  height: 100% !important;
  max-width: 100% !important;
  object-fit: contain !important;
  object-position: center !important;
  padding: 0 !important;
  background: transparent !important;
  display: block !important;
}

/* The slot keeps a quiet stone ground so a picture that is slow to arrive is
   not a white hole in the page. */
.piece__img,
.work__media .plate,
.card__media .plate,
.pcard__media .plate,
.feature__media .frame {
  background: var(--stone-150) !important;
}

/* A slot that is holding a fitted picture needs a floor under it, or the
   fitted margin is the page showing through and the tile looks unfinished.
   The walls set a fixed 3:4 below; this is the ground the picture sits on
   inside it, and it is the same stone the rest of the page is cut from. */
.wall .piece__img,
.rail .card__media,
.pcard__media,
.works .work__media .plate {
  background: var(--stone-150) !important;
}

/* ---- the wrapper between the slot and the photograph ------------------
   Where a <picture class="plate"> sits inside a slot that HAS a definite
   height, it has to fill that height rather than take its own. Left at
   `height: auto` it sized itself from the photograph and came out taller
   than the slot clipping it, so the image filled the wrapper perfectly and
   the wrapper hung 51px out of the bottom of the box — a crop one level
   further out than the one fix 2 removed.

   Only the three families whose slots carry a fixed aspect-ratio are listed.
   `.works .work__media .plate` is deliberately absent: that slot is
   aspect-ratio:auto and takes its own height FROM this element, so telling
   it to fill would be filling nothing and it would collapse. */
.rail .card__media > .plate,
.rail .card__media > picture,
.pcard__media > .plate,
.pcard__media > picture,
.wall .piece__img > picture,
.frame[style*="aspect-ratio"] > picture {
  height: 100% !important;
  width: 100% !important;
  display: block !important;
  aspect-ratio: auto !important;
}

/* Video slots keep the cover crop. A film has no still shape to respect and
   is authored to fill its frame; these are the reel and the project loops.

   `.hero__film` itself is deliberately NOT in this list, and that matters.
   The shell is sized by the hero: absolutely positioned down the right of the
   band on a laptop, and on a phone a strip across the top of a set height —
   `clamp(310px, 52vh, 460px)`, chosen so a 16:9 reel is not brutally cropped
   on a narrow screen. An earlier version of this block did include it, with
   `height: 100% !important`, which beat that clamp and let the reel grow to
   the full height of the hero. On a phone the film then ran the whole way
   down the band and the headline, the standfirst and both buttons sat on top
   of moving footage instead of on quiet ground. Only what is INSIDE the
   shell is stretched to fill it. */
/* The hero reel fills its band and is cropped by intent —
   that shell is a texture behind the headline, not a piece of work in a
   slot. Uploaded film is not in this list any more: see below. */
.hero__film video {
  aspect-ratio: auto;
  object-fit: cover !important;
  height: 100% !important;
}

/* Uploaded film is content, and its slot's shape is a ratio the owner chose
   in the panel rather than anything to do with how the film was shot. Fitted
   rather than filled, for the same reason the photographs are: a dropdown
   should not be able to cut into the work. The near-black ground is already
   set on the slot by the renderer, so the margin reads as letterboxing.

   Not !important, deliberately. The renderer writes object-fit inline on the
   <video>, and an inline declaration should be free to win here — that is
   where the per-upload decision belongs. This rule setting `cover` with
   !important is exactly what silently reversed that. */
.work__media--media {
  aspect-ratio: auto;
  height: 100% !important;
}
.work__media--media video {
  object-fit: contain;
  height: 100% !important;
}
.work__media--media {
  height: auto !important;
}
.work__media--media video {
  position: absolute !important;
  inset: 0 !important;
}

/* ---- nothing is allowed to tower -------------------------------------
   Natural shape, but not without limit. Four of the seventy-six pieces are
   very tall — the backlit contour relief is 1 : 1.85 — and at full column
   width one of those is 1,330px of a single photograph, which on the
   homepage meant one tile taking a screen and a half on its own and the
   composition around it disappearing.

   The cap is expressed against the tile's own width rather than the
   viewport, so it means the same thing in a 170px phone column and a 400px
   one. Where it bites, and only there, object-fit trims the overflow.

   It is set at 1.9 rather than something tighter, and that number is chosen
   against the catalogue: the tallest piece in it is the backlit contour
   relief at 1 : 1.85, so nothing that exists today is cropped by a single
   pixel. A tighter cap was tried first, at 1.58, and it took 15% off the
   four tallest pieces for no benefit — with the walls in columns rather than
   rows, a tall photograph no longer stretches a row or leaves a hole, and at
   a column width of about 400px even the tallest is 740px, which sits inside
   a laptop screen. The cap is now purely a guard rail: it stops some future
   upload of a 1:4 panorama from taking a whole page on its own, and it does
   nothing at all to the work that is here.

   `cqw` needs the tile to declare itself a container, which is what
   container-type does; inline-size contains the width only, so the height
   still comes from the picture. Browsers without container queries ignore
   the max-height and simply show the tall pieces at full height, which is
   where we were a moment ago — the correct direction to fail in. */
.piece__img,
.work__media .plate,
.card__media .plate,
.pcard__media .plate,
.feature__media .frame {
  container-type: inline-size;
  overflow: hidden !important;
}
.piece__img img,
.work__media .plate img,
.card__media .plate img,
.pcard__media .plate img,
.feature__media .frame img {
  max-height: 190cqw;
  /* This said `cover`, and being later in the file it beat the `contain`
     above — so the guard rail against a 1:4 panorama was quietly cropping
     every photograph on the site. The cap can stay; the crop cannot. With
     contain a very tall picture is fitted into the capped box and shown
     whole, which is what the cap was for in the first place. */
  object-fit: contain !important;
  object-position: center !important;
}

/* ---- the walls: one hang, every tile on the same line ----------------
   Columns were tried here and taken out again. They close every gap, which
   was the brief at the time, but they close it by letting each column find
   its own rhythm — so tile tops never line up across the wall and the whole
   thing staggers. Read back, that stagger is what "ऊपर नीचे" meant, and it
   reads as unfinished however tidy the spacing is.

   So: a real grid, every tile identical, every row on one line. That means
   one shape for every photograph, and one shape means a crop. The shape is
   chosen from the catalogue rather than picked: at 3:4 the average piece
   loses 8.9% of its frame, which is the lowest of any single ratio here and
   is unsurprising, because fifty-eight of the seventy-six pieces already sit
   between 0.75 and 0.80. Most tiles lose nothing worth seeing. The lightbox
   still opens the whole photograph, uncropped, which is where anyone looking
   properly is going anyway.

   The exception is below, and it is what keeps this honest. */
.wall {
  display: grid !important;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(14px, 1.8vw, 24px);
  align-items: start;
  /* A double-width tile can leave the end of a row short. `dense` backfills
     it with the next tile that fits, so the wall never has a hole in it. */
  grid-auto-flow: row dense;
  container-type: inline-size;
  --wall-cols: 3;
  --wall-gap: clamp(14px, 1.8vw, 24px);
  /* Stated through the variable, and !important, so the gap the browser uses
     and the gap the wide-tile height is derived from are the same number by
     construction. An earlier patch in this file sets the gap to
     clamp(16px, 2vw, 28px) !important; left to win it resolved to 28px while
     this block computed against 24px, and the wide tile came out 85px taller
     than the row it was supposed to match. */
  gap: var(--wall-gap) !important;
}
.wall > * { margin: 0; display: block; width: 100%; min-width: 0; }
.wall--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); --wall-cols: 2; }
.wall--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); --wall-cols: 4; }

/* The shape is the piece's own.
   This said `aspect-ratio: 3 / 4 !important` and overrode the ratio the owner
   picked in the panel for every tile on the site. It was there to keep rows
   lining up; the card boundary below does that job now, and does it without
   taking the decision away. The inline ratio the renderer writes — the
   owner's choice, or the photograph's own shape when they chose "natural" —
   is what governs. */
.wall .piece__img {
  max-height: none !important;
}

/* ---- the seven landscape pieces --------------------------------------
   Five of the pieces are 4:3 and two are square-ish. Forced into a 3:4
   portrait tile a 4:3 photograph loses 44% of itself — nearly half the
   carving — and that is not a crop, it is a different picture.

   They take two columns instead of one. The tile is then wide enough to suit
   them (about 1.55:1 against their 1.33:1, so the loss drops from 44% to
   about 14%), and because its HEIGHT is set to exactly what a one-column
   tile computes to, the row it sits in is the same height as every other
   row. Structure is kept and the work is kept, which the single-ratio grid
   on its own could not do.

   The height is derived rather than guessed: one column is the wall minus
   its gaps, divided by the column count, and a 3:4 tile is four thirds of
   that. `cqw` reads the wall's own width, so this stays exact at every
   breakpoint instead of drifting by a few pixels as the gap clamp moves. */
.wall > .is-wide { grid-column: span 2; }
.wall > .is-wide .piece__img,
.wall > .is-wide .piece__img[style] {
  aspect-ratio: auto !important;
  /* !important is required, not decorative: PHOTOGRAPHS SHOW WHOLE further up
     sets `height: auto !important` on .piece__img[style]. Without the flag
     here that rule won, the tile fell back to the photograph's natural
     height, and the wide tile stood 628px against the 540px row beside it. */
  height: calc((100cqw - (var(--wall-cols) - 1) * var(--wall-gap)) / var(--wall-cols) * 4 / 3) !important;
}

/* Captions are given one height so tile bottoms land on a line too. Two
   lines of title plus the material row is the tallest that occurs; anything
   shorter simply sits at the top of its box. Without this the images align
   and the cards below them do not, which looks worse than no grid at all. */
.wall .piece__below {
  min-height: 64px;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}
@media (max-width: 600px) { .wall .piece__below { min-height: 54px; } }

/* Two columns from tablet down, and the garden with them — its old
   `#garden .wall { repeat(3, 1fr) !important }` is gone (see the GARDEN
   block above), so it no longer holds three columns on a 360px phone.
   A wide tile spanning 2 of 2 columns is simply full width, which is a
   reasonable thing for a landscape piece to be on a phone. */
@media (max-width: 1000px) {
  .wall, .wall--2, .wall--4, #garden .wall {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
    --wall-cols: 2;
  }
}

/* A phone is a reading surface, not half of a desktop wall. Two columns made
   each portrait only 150--180px wide and, whenever the final row contained a
   single card, left an equally tall empty column beside it. The one-column
   composition removes that structural hole on every collection and keeps a
   Fibonacci 13--21px rhythm between cards. */
@media (max-width: 620px) {
  .wall, .wall--2, .wall--4, #garden .wall {
    grid-template-columns: minmax(0, 1fr) !important;
    grid-auto-flow: row;
    --wall-cols: 1;
    --wall-gap: clamp(13px, 4vw, 21px);
  }
  .wall > .is-wide { grid-column: 1 / -1; }
  .wall > .is-wide .piece__img,
  .wall > .is-wide .piece__img[style] {
    aspect-ratio: var(--piece-ratio, 4 / 3) !important;
    height: auto !important;
  }
  .wall .piece__below { min-height: 0; }
}

/* The renderer counts logical spans, so a landscape (two spans) is accounted
   for as well as the raw number of cards. A single closing tablet card becomes
   a full-width plate: there is no fabricated empty partner beside it. */
@media (min-width: 621px) and (max-width: 1000px) {
  .wall--tail-2-1 > :last-child:not(.is-wide) {
    grid-column: 1 / -1;
    width: 100%;
  }
}

/* Six physical tracks express three visual columns. A normal card spans two
   tracks and a landscape spans four, so ordinary rows are unchanged; the
   half-tracks only exist to centre an incomplete final row. This is the grid
   equivalent of hanging one or two framed works on the centre third rather
   than leaving an accidental right-hand hole. */
@media (min-width: 1001px) {
  .wall:not(.wall--2):not(.wall--4) {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
  .wall:not(.wall--2):not(.wall--4) > * { grid-column: span 2; }
  .wall:not(.wall--2):not(.wall--4) > .is-wide { grid-column: span 4; }

  .wall.wall--tail-3-1:not(.wall--2):not(.wall--4) > :last-child:not(.is-wide) {
    grid-column: 3 / span 2;
  }
  .wall.wall--tail-3-2:not(.wall--2):not(.wall--4) > :last-child.is-wide,
  .wall.wall--tail-3-2:not(.wall--2):not(.wall--4) > .is-wide:nth-last-child(2) {
    grid-column: 2 / span 4;
  }
  .wall.wall--tail-3-2:not(.wall--2):not(.wall--4) > :nth-last-child(2):not(.is-wide) {
    grid-column: 2 / span 2;
  }
  .wall.wall--tail-3-2:not(.wall--2):not(.wall--4) > :last-child:not(.is-wide) {
    grid-column: 4 / span 2;
  }
  /* If the penultimate item is the centred landscape, the final portrait
     backfills the one logical slot left in the preceding row. Without this
     pattern-specific column it would be forced below the centred landscape
     and create a new, right-weighted row. */
  .wall.wall--tail-3-2:not(.wall--2):not(.wall--4):has(> .is-wide:nth-last-child(2))
    > :last-child:not(.is-wide) {
    grid-column: 5 / span 2;
  }
}

/* ==========================================================================
   THE MANDALA COMES OUT FROM BEHIND THE WORDS
   --------------------------------------------------------------------------
   Every other drawing on the site is anchored past the edge of its band —
   `right: -34%` and the like — so on a laptop it lands in the empty margin
   beside the column and never touches the copy. The lotus mandala on the
   Philosophy band is the one exception: it is placed dead centre, at up to
   430px across and 0.55 opacity, and the three philosophy cards sit directly
   on top of it. A rotating, breathing, bronze-tinted drawing behind three
   short paragraphs is not a background, it is competition, and the labels
   are the part that loses.

   On a phone it never looked wrong for one reason only: the inline width is
   `min(52vw, 430px)`, so a 390px screen gets 203px of it. Small enough to
   read as texture. It is the desktop size that is the problem, not the
   drawing.

   So it is treated on a laptop the way every other plate already is: moved
   off the reading column to bleed past the right edge, and dropped to the
   weight of a watermark. The drawing is still there and still turning; it is
   simply no longer underneath anything anyone has to read. The phone keeps
   what it had, which the reader has said is right.
   ========================================================================== */
/* Measured before deciding this: the three philosophy cards run from 77px to
   1,348px inside a 1,440px page — they are the whole band, edge to edge.
   There is no margin for the drawing to be moved into. Anchored hard right
   and pulled 9% off the edge it still lay 175px across the third card, and a
   watermark under a heading is the thing being complained about, not the
   opacity it is drawn at.

   So on a laptop it does not paint. The phone keeps it: there the plate is
   min(52vw, 430px), which is 203px on a 390px screen — small enough to read
   as ground rather than as a picture behind the words, which is why the
   phone was never the complaint. If it should come back on a laptop, the
   place for it is a band with a one-sided column, not this one. */
/* Scoped to the band it actually collides with, and nothing else.
   This rule was first written as `.art[data-art="lotus-mandala"]`, which
   matches the drawing by name wherever it appears — and it appears twice.
   On Collections it is the page-head plate, anchored `right: 4%` in the
   margin beside a short heading, which is the placement every other drawing
   on the site uses and was never the problem. Hiding it there removed
   artwork that was working, on a page whose whole job is to look considered.
   The Philosophy band on the homepage is the only place this drawing sits
   under the copy, so that is the only place it is suppressed. */
@media (min-width: 761px) {
  [data-section="Philosophy"] .art[data-art="lotus-mandala"] { display: none !important; }
}

/* ==========================================================================
   SELECTED WORK — the same hang as the walls
   --------------------------------------------------------------------------
   The homepage grid had the same two problems the walls had, from the same
   cause. Tiles were too big — the largest was 755px wide, and at that
   photograph's own 1:1.76 it stood 1,330px tall, so one piece was a screen
   and a half on a laptop and nothing around it was ever on screen with it.
   And a 12-column layout with 7/5/4/8 spans makes each row as tall as the
   tallest thing in it, so the rows left holes.

   Three equal columns, the same 3:4 tile as the walls, the same double-width
   exception for landscape pieces. A tile is about 400 x 533 on a 1440px
   laptop, so several are in view at once, and the homepage and Collections
   read as the same building. The 12-column span classes are neutralised
   rather than removed, because they are still in the markup on every page.
   ========================================================================== */
.works {
  display: grid !important;
  grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
  gap: clamp(18px, 2.2vw, 34px);
  align-items: start;
  grid-auto-flow: row dense;
  container-type: inline-size;
  --wall-cols: 3;
  --wall-gap: clamp(18px, 2.2vw, 34px);
  gap: var(--wall-gap) !important;
}
.works > *,
.works .w-4, .works .w-5, .works .w-7, .works .w-8 {
  grid-column: auto !important;
  grid-row: auto !important;
  width: 100% !important;
  min-width: 0;
  margin: 0 !important;
}
/* .w-5 ships as a nested grid holding two pieces. `display: contents` lets
   both of them become tiles of the outer grid in their own right, instead of
   one double-height cell that nothing else can line up with. */
.works > .w-5 { display: contents !important; }

.works .work__media .plate {
  aspect-ratio: 3 / 4 !important;
  max-height: none !important;
}
.works > .is-wide,
.works .w-5 > .is-wide { grid-column: span 2; }
.works > .is-wide .work__media .plate,
.works .w-5 > .is-wide .work__media .plate {
  aspect-ratio: auto !important;
  height: calc((100cqw - (var(--wall-cols) - 1) * var(--wall-gap)) / var(--wall-cols) * 4 / 3) !important;
}
@media (max-width: 1000px) {
  .works {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
    --wall-cols: 2;
  }
}
/* Five pieces, so a phone gets one column rather than the two the
   sixty-six-piece walls get: these are the work being led with, and they
   should have the screen. A landscape piece cannot span two of one column,
   so it goes back to being an ordinary tile at its own shape. */
@media (max-width: 700px) {
  .works {
    grid-template-columns: minmax(0, 1fr) !important;
    --wall-cols: 1;
  }
  .works > .is-wide, .works .w-5 > .is-wide { grid-column: auto; }
  .works > .is-wide .work__media .plate,
  .works .w-5 > .is-wide .work__media .plate {
    aspect-ratio: 4 / 3 !important;
    /* !important, or the derived height from the desktop rule above keeps
       applying — that rule is not inside a media query, and an explicit
       height beats an aspect-ratio. With --wall-cols down to 1 it computed a
       full-width box 1.33x TALLER than it is wide, so the one landscape piece
       on the homepage was being cropped 44% into a portrait slot on a phone,
       which is the exact thing the wide tile exists to prevent. */
    height: auto !important;
  }
}

/* The rail's cards keep their own shapes and hang from a common top line.
   Stretching them to a shared height is what would force a crop; aligning
   them at the top costs nothing and crops nothing, and in a strip that
   scrolls sideways the eye reads the top edge as the line anyway. */
.rail { align-items: start !important; }


/* ==========================================================================
   THE PHONE ACTION BAR
   --------------------------------------------------------------------------
   Call, WhatsApp and the enquiry panel, fixed to the foot of the screen on a
   phone. A stone commission starts with a conversation, and on a phone the
   ways to start one were a "Get in touch" button behind the hamburger and a
   footer twelve thousand pixels down.

   It is built out of the site rather than bolted on: basalt ground, the
   bronze the rest of the site accents with, Marcellus nowhere near it —
   these are controls, so they take the mono voice the site uses for controls
   everywhere else. One action carries the weight, the other two sit back;
   three equal bright buttons is a toolbar, not a close.

   It arrives rather than being there from the first frame. While the hero is
   on screen there is already a "Start a commission" button in it, and a
   second one pinned over it is clutter; the bar slides up once the reader is
   past the hero, and it rides down with the header when the header hides, so
   the chrome behaves as one thing rather than two.
   ========================================================================== */
.callbar {
  position: fixed;
  z-index: 88;
  left: 0; right: 0; bottom: 0;
  display: none;                       /* phones only — switched on below */
  gap: 8px;
  padding: 9px clamp(10px, 3vw, 16px);
  /* The home-indicator strip on an iPhone sits over anything at bottom: 0.
     env() resolves to 0 everywhere else, so this costs nothing elsewhere. */
  padding-bottom: calc(9px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(180deg, rgba(30,26,20,.94) 0%, var(--stone-900) 62%);
  border-top: 1px solid rgba(239, 234, 220, 0.14);
  box-shadow: 0 -14px 34px -20px rgba(20, 17, 12, 0.9);
  transform: translateY(115%);
  transition: transform .46s cubic-bezier(.22,.61,.36,1);
}
/* Past the hero. Set by site.js. */
.callbar.is-up { transform: none; }
/* Out of the way behind a panel or the lightbox — both of those own the
   screen while they are open, and the bar's own Enquire button would
   otherwise sit on top of the panel it just opened.

   It does NOT follow the header down. That was tried, on the idea that the
   chrome should behave as one thing; it is wrong, because the two are not
   the same kind of thing. The header is navigation, and hiding it while the
   reader is going somewhere is the point. The bar is the way to make contact,
   and scrolling down through the work is exactly when somebody decides to —
   so tying it to the header meant it was absent at the only moment it was
   wanted, and present only when the reader was already heading back up. */
body.callbar-off .callbar { transform: translateY(115%); }

.callbar__btn {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  /* 46px is a comfortable thumb target and the smallest this should ever be;
     44px is the documented floor on both platforms. */
  min-height: 46px;
  padding: 0 10px;
  border: 1px solid rgba(239, 234, 220, 0.2);
  border-radius: 2px;
  background: rgba(239, 234, 220, 0.05);
  color: var(--on-dark);
  font-family: var(--f-mono);
  font-size: 0.58rem;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
  transition: background-color .28s var(--ease), border-color .28s var(--ease), transform .18s var(--ease);
}
.callbar__btn svg { flex: none; }
.callbar__btn:active { transform: scale(.975); }
.callbar__btn:focus-visible { outline: 2px solid var(--gold-2); outline-offset: 2px; }

/* WhatsApp reads as the warmer of the two quiet actions — it is the one most
   people here actually use — without becoming a second primary. */
.callbar__btn--wa {
  border-color: rgba(194, 170, 126, 0.34);
  color: var(--gold-2);
}

/* The one that carries the weight. The same bronze the site uses for its
   accents, on the basalt the footer already stands on. */
.callbar__btn--primary {
  position: relative;
  overflow: hidden;
  border-color: transparent;
  background: linear-gradient(135deg, var(--gold-2) 0%, #A88B57 52%, var(--gold-2) 100%);
  color: var(--stone-950);
}
/* A slow bar of light crossing the face — the sheen off a polished edge.
   It is a transform on a pseudo-element, so it composites and costs nothing;
   6s apart so it reads as a catch of light rather than a loading bar. */
.callbar__btn--primary::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 38%, rgba(255,252,244,.5) 50%, transparent 62%);
  transform: translateX(-120%);
  animation: callbar-sheen 6s var(--ease) 1.4s infinite;
  pointer-events: none;
}
@keyframes callbar-sheen { 0% { transform: translateX(-120%); } 26%, 100% { transform: translateX(120%); } }

@media (max-width: 860px) {
  /* A mobile browser continuously changes its visual viewport while its URL
     and tool bars collapse. Both fixed and sticky bottom bars can therefore
     cross the photograph or cover copy during that transition. This contact
     band lives immediately after the footer in every document, so keeping it
     in normal flow is deterministic on iOS, Android and embedded browsers:
     all three premium actions remain, no content needs artificial bottom
     padding, and there is never a black overlay across the work. */
  .callbar {
    display: flex;
    position: relative;
    inset: auto;
    z-index: 8;
    transform: none;
    transition: none;
  }
  .callbar.is-up { transform: none; }
  .ag-launch {
    bottom: calc(clamp(14px, 2vw, 26px) + env(safe-area-inset-bottom, 0px));
    transition: transform .55s var(--ease), border-color .4s, box-shadow .55s var(--ease);
  }
  body.callbar-up .ag-launch {
    bottom: calc(clamp(14px, 2vw, 26px) + env(safe-area-inset-bottom, 0px));
  }
}

/* Reduced motion gets the bar without the slide. It is a control, not an
   entrance, so it is simply present once the reader is past the hero. */
@media (max-width: 860px) and (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */
  .callbar { transition: none; }
}

/* On the narrowest phones three labels will not sit on one line at a legible
   size, so the quiet two keep their icons and give up their words. The
   primary keeps its label — it is the one that has to say what it does. */
@media (max-width: 380px) {
  .callbar__btn { letter-spacing: 0.12em; padding: 0 6px; }
  .callbar__btn--wa span,
  .callbar__btn[href^="tel"] span { display: none; }
  .callbar__btn--wa, .callbar__btn[href^="tel"] { flex: 0 0 54px; }
}

/* The sheen only runs while the bar is actually on screen. Tucked away it
   is a 6-second loop compositing behind the bottom edge of the page for the
   whole visit — the same invisible work the off-screen plates were doing. */
.callbar:not(.is-up) .callbar__btn--primary::after,
body.callbar-off .callbar__btn--primary::after {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) and (prefers-reduced-motion: no-preference) { /* OFF by the motion policy — see motion-always.css */
  .callbar { transition: none; }
  .callbar__btn--primary::after { animation: none; opacity: 0; }
}


/* ==========================================================================
   THE HEADER GETS ITS OWN GROUND
   --------------------------------------------------------------------------
   At the top of the homepage the header is transparent by design and the reel
   runs underneath it. The reel is dark stone, not bright marble: measured
   across the band the header occupies, its average relative luminance is
   0.157 and its darkest frame 0.018. Against that, the links — --ink-soft,
   which is a comfortable 7.7:1 on sand — come out at 1.95:1 on average and
   1.57:1 at the darkest point. The floor for text this size is 4.5:1. So the
   navigation and the wordmark really were washed out, and only on the
   homepage, and only above the fold, which is why it survived this long.

   Darkening the text does not fix it and makes it worse: --ink on the darkest
   frame measures 1.06:1, which is very nearly invisible. Dark on dark cannot
   be solved by more dark. What is missing is not ink, it is ground.

   So the header carries a scrim of its own — the same sand the page is built
   from, near-opaque behind the text and gone by 132px, so the reel is
   uninterrupted everywhere except the strip the header actually needs. At the
   height the links sit the scrim is about 93%, which puts them back above
   7:1 over any frame the film can show, and the wordmark above 10:1.

   With a light ground under them the links can also take the darker ink they
   should always have had, and a little more size and weight; at 0.63rem in
   mono at 0.16em tracking they were the quietest thing on a page selling
   hand-carved stone. */
.nav::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 132px;
  background: linear-gradient(180deg,
    rgba(233, 227, 213, 0.96) 0%,
    rgba(233, 227, 213, 0.93) 44%,
    rgba(233, 227, 213, 0.72) 66%,
    rgba(233, 227, 213, 0.28) 84%,
    rgba(233, 227, 213, 0) 100%);
  pointer-events: none;
  z-index: -1;
  transition: opacity .35s var(--ease);
}
/* Once the bar is stuck it has a solid background of its own and the scrim
   would only be doubling it. */
.nav--stuck::before { opacity: 0; }

/* Darker ink, a touch more size, a little more weight. */
.nav__link {
  color: var(--ink) !important;
  font-size: 0.68rem;
  font-weight: 500;
}
.nav__link:hover { color: var(--accent) !important; }
.nav__link[aria-current="page"] { color: var(--accent) !important; }

.brand__mark { color: var(--ink); }
.brand__logo { opacity: 1; }

/* The phone menu is a solid sand panel, so it needs none of this — and the
   scrim must not sit over the panel while it is open. */
.nav--open::before { opacity: 0; }
@media (max-width: 1200px) {
  .nav__link { font-size: 1.1rem; }
}


/* ==========================================================================
   WHERE THE CROP FALLS
   --------------------------------------------------------------------------
   An aligned grid has one tile shape, so a photograph that is not that shape
   loses something. Measured across the sixty-six: twenty-seven lose nothing,
   twenty-one lose under 8%, and ten lose 15% or more — the very tall reliefs
   at 1:1.85 and the near-square parasol bases. That last group cannot be
   fixed by choosing a better ratio; 3:4 is already the least-cost shape for
   this catalogue, and every alternative tested made the average worse.

   What can be fixed is WHICH part goes. Centred, a tall carving loses equally
   from the top and the bottom — and the top is the finial, the arch, the face:
   the reason the piece exists. The bottom is plinth, floor, and the edge of
   the workshop.

   Anchoring at 38% keeps the carving and spends the crop on the floor. The
   same number does nothing at all to the fifty-six pieces that are not
   cropped vertically, and nothing to the landscape pieces in wide tiles,
   which are trimmed at the sides.

   An earlier pass used `center top`, which is this idea taken too far: at 0%
   a piece with any headroom sat jammed against the top edge of its tile.
   38% is just above centre — enough to save the carving, not enough to look
   mis-set. */
.wall .piece__img img,
.works .work__media .plate img {
  object-position: 50% 38% !important;
}


/* ==========================================================================
   EVERY CARD IN A SET IS THE SAME CARD
   --------------------------------------------------------------------------
   The photographs were made to line up and the labels under them were not, so
   the wall still stepped up and down. Measured at 1440px before this block:

     homepage Selected work   caption 150px on two tiles, 94px on three
                              — a 57px step, and 89px of dead space under the
                              short ones. Two of the five carry a sentence of
                              description and three do not.
     Collections walls        a title that wraps to two lines makes its tile
                              31px taller than its neighbours, leaving up to
                              55px of nothing under them.
     portfolio project cards  635 / 728 / 695px, because their photographs
                              are 1.09, 0.87 and 0.94 and were still being
                              laid out at their own shapes.
     homepage Collections rail 575 / 642 / 672 / 672 / 732px, same cause.

   A row is only as tidy as its untidiest card, so the rule is the same one
   the images already follow: within a set, every card is identical. Media
   gets one shape. The label block gets one HEIGHT — not a minimum, a height —
   with room for two lines of title, and the title clamped to two lines so it
   can never push past it. Reserving the second line on every card costs a
   little even air under the one-line titles, which reads as spacing because
   it is the same on all of them; a minimum height would let one long title
   move a whole row, which is the thing being fixed.
   ========================================================================== */

/* ==========================================================================
   THE CARD
   --------------------------------------------------------------------------
   Each piece sits on its own ground with its own edge. That edge is what lets
   the pictures inside differ in height without the wall looking untidy: the
   cards line up, so the variation reads as the hang rather than as a mistake.

   Deliberately quiet. A hairline of the same stone the rest of the page is
   cut from, a 2px radius rather than a fashionable one, and a shadow that
   only arrives on hover. The work is the thing being looked at; the card is
   the mount, and a mount that announces itself is a bad mount.
   ========================================================================== */
.wall > .reveal {
  display: flex;
  flex-direction: column;
  background: var(--marble);
  border: 1px solid var(--rule, rgba(36,31,24,.12));
  border-radius: 2px;
  overflow: hidden;
  /* The grid already stretches every card in a row to the tallest. This makes
     the card actually USE that height, so its bottom edge is the row's bottom
     edge and the caption sits against it. */
  height: 100%;
  transition: border-color .4s var(--ease), box-shadow .45s var(--ease),
              transform .45s var(--ease);
}
.wall > .reveal:hover {
  border-color: rgba(201,163,90,0.42);
  box-shadow: 0 26px 54px -32px rgba(36,31,24,.42);
  transform: translateY(-3px);
}

/* The media area inside the card. The lift now belongs to the card, so the
   tile's own transform is removed — two things lifting by different amounts
   at the same time is what made the old hover feel loose. */
.wall .piece {
  border-radius: 0;
  border: 0;
  background: var(--stone-150);
}
.wall .piece:hover { transform: none; box-shadow: none; }

/* The slot fills the card's width and takes the shape the piece asked for.
   `aspect-ratio` comes inline from the renderer; nothing here overrides it. */
.wall .piece__img {
  width: 100%;
  background: var(--stone-150);
}

/* Fill the frame. Safe because the frame is the owner's choice and defaults to
   the photograph's own shape — where those agree, and by default they do,
   `cover` removes nothing. A piece that must be shown whole regardless opts
   out with data-fit="whole" below. */
.wall .piece__img img,
.wall .piece__img picture,
.wall .piece__img video {
  width: 100% !important;
  height: 100% !important;
  object-fit: contain !important;
  object-position: center !important;
}

/* The escape hatch. For a piece where a frame has been chosen deliberately and
   the whole of the work still has to survive inside it. */
.wall .piece__img[data-fit="whole"] img,
.wall .piece__img[data-fit="whole"] picture,
.wall .piece__img[data-fit="whole"] video,
.piece__img[data-fit="whole"] img,
.piece__img[data-fit="whole"] picture,
.piece__img[data-fit="whole"] video {
  object-fit: contain !important;
}

/* Piece slots outside the walls — the homepage strips — now carry the piece's
   own ratio as well, so the picture has to fill it rather than be laid out at
   its natural height inside a box that already has one. Same rule as the wall,
   stated for the slots that are not in one. */
.piece__img img,
.piece__img picture,
.piece__img video {
  width: 100% !important;
  height: 100% !important;
  object-fit: contain !important;
  object-position: center !important;
}

/* ---- gallery walls: the caption body ----------------------------------- */
.wall .piece__below {
  /* A minimum, not a height. The grid row already stretches every card in it
     to the tallest, so this is only reserving a consistent amount of air
     under the short captions — it is not what aligns them, and it must never
     be allowed to cut one short. */
  min-height: 78px;
  height: auto;
  /* Inside the card now, so it needs real padding rather than none. */
  padding: 15px 16px 17px !important;
  overflow: visible;
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* Takes the slack in a card whose picture is shorter than its neighbour's,
     which is what keeps the labels on one line across a row of mixed frames. */
  flex: 1 1 auto;
}
.wall .piece__below .piece__t {
  /* The clamp came off with the fixed height. It existed to stop a long title
     pushing past a box that no longer has a ceiling, and while it was there a
     three-line title lost its last line with no sign it had been truncated. */
  overflow: visible;
  margin-bottom: 0 !important;
}
/* The material line wraps rather than being trimmed. `gap` spaces the two
   halves when they sit side by side and when they fall onto two rows. */
.wall .piece__below .piece__m {
  flex-wrap: wrap;
  overflow: visible;
}
@media (max-width: 600px) {
  .wall .piece__below { min-height: 66px; padding-top: 10px !important; }
}

/* ---- homepage Selected work ------------------------------------------
   The sentence of description comes out of the tile. It exists on two of the
   five, so leaving it in means either two tall cards among three short ones,
   or two lines of reserved emptiness under the three that have nothing to
   put there. A gallery label is the name and the material; the sentence
   belongs where there is room to read it, not under a thumbnail. */
.works .work__cap .work__s { display: none !important; }
.works .work__cap {
  /* Same correction as the walls above: reserve the space, never enforce it. */
  min-height: 104px;
  height: auto;
  overflow: visible;
  padding: 14px 0 0 !important;
}
.works .work__cap .work__t {
  overflow: visible;
}
@media (max-width: 700px) {
  .works .work__cap { min-height: 92px; }
}

/* ---- the Collections rail --------------------------------------------
   `align-items: start` was letting each card find its own height. In a strip
   that scrolls sideways the bottom edge is as visible as the top, so the
   cards stretch to a common height and their media takes the same 3:4 the
   walls use — these are the same photographs, at 0.67 to 0.94. */
.rail { align-items: stretch !important; }
.rail .card__media,
.rail .card__media[style] {
  aspect-ratio: 3 / 4 !important;
  height: auto !important;
  max-height: none !important;
}
.rail .card { display: flex; flex-direction: column; }
.rail .card__body { flex: 1; }

/* ---- portfolio project cards ------------------------------------------
   Their three photographs are 1.09, 0.87 and 0.94 — near enough to square
   that a square slot costs about 9% and reads as deliberate, where 3:4 would
   have taken 27% off the widest of them. */
.pcard__media,
.pcard__media[style] {
  aspect-ratio: 1 / 1 !important;
  height: auto !important;
  max-height: none !important;
}
.pcard { display: flex; flex-direction: column; }
.pcard__body { flex: 1; }


/* ==========================================================================
   THE HEADER ON A PHONE — a finished bar, not a haze
   --------------------------------------------------------------------------
   The scrim above is a desktop fix and belongs only there. On a laptop the
   reel runs unscrimmed down the right of the hero, straight under the links,
   which is where the 1.53:1 measurement came from. On a phone the reel is a
   band across the top and it already carries a scrim of its own — 62% sand
   at its top edge, from .hero__film::after. That alone puts the wordmark at
   5.91:1 against the darkest frame and 8.00:1 against an average one, both
   past the 4.5:1 floor and the second past AAA.

   Laying a second 96% scrim over the first bought nothing and cost the look:
   132px of the reel behind a fade, which reads exactly as what it is — a
   translucent haze across the top of the page.

   The bar itself is finished properly instead. Once stuck it is fully opaque
   rather than 98%, because at 98% a busy photograph scrolling underneath
   shows through as a faint smear rather than as a bar; and the hard hairline
   under it is replaced with a soft shadow, so the edge is a shadow the page
   casts rather than a drawn line. That is the difference between a strip
   laid over the page and a bar the page is under.
   ========================================================================== */
@media (max-width: 1200px) {
  .nav::before { display: none; }
  .nav--stuck {
    background: var(--sand) !important;
    border-bottom-color: transparent !important;
    box-shadow: 0 10px 30px -22px rgba(20, 17, 12, 0.55);
  }
}


/* ==========================================================================
   THE PHONE HERO AND HEADER, FINISHED
   --------------------------------------------------------------------------
   Measured on a 390x844 phone: the reel is a band from 0 to 439px, the header
   covers its top 77px, the eyebrow sits at 128px and the headline runs from
   161 to 383. The scrim over the reel went 62% at the top, down to 34% at
   132px, back up to 55% at 272px.

   That is upside down. The heaviest wash, 62%, lands where the header is
   already covering the film and nobody can see it; the lightest, 34%, lands
   exactly where the eyebrow and the headline are. So the page spent its
   contrast where it was not needed and ran out where it was — and because
   the curve goes dark, light, dark again, the eye reads a distinct hazy band
   across the top of the reel rather than a gradient. That band is the
   "transparency" at the top of the page.

   The curve below is monotonic and matched to the content: almost clear
   through the strip just under the header, so the carving reads sharply the
   moment the header ends, then rising steadily so every line of text sits on
   more ground than the line above it, and closing into sand at the bottom.

   The header itself is now solid on a phone rather than transparent-then-
   solid. Two complaints go with it: nothing shows through it at the top, and
   nothing appears part-way down the page, because it was never absent. Its
   bottom edge is a short fade rather than a drawn hairline, so the bar melts
   into the page instead of sitting on it like a label.
   ========================================================================== */
@media (max-width: 1200px) {
  /* ---- the bar is frosted glass, not paint ---------------------------
     The reel should be visible through the header, and the wordmark and the
     menu must still read against it. Those two only coexist because of the
     blur.

     Without a blur the worst case is the darkest PIXEL the film can put under
     a letter — measured at 0.018 luminance, which no sensible amount of tint
     survives. A blur removes the variance: the backdrop becomes the local
     average, so the worst case becomes the darkest passage rather than the
     darkest pixel, and a modest tint is then enough.

     Measured against --ink at 52% sand over a blurred backdrop:
        average passage  6.9:1
        dark passage     5.3:1
     both clear of the 4.5:1 floor for this size, with just under half the
     film showing through. At 30% tint the dark passages fall to 3.2:1 and the
     menu starts disappearing into the carving, which is where this began.

     The filter is on ::before, NOT on .nav, and that is load-bearing.
     backdrop-filter promotes an element to a containing block for
     position:fixed descendants — the same trap will-change set earlier — and
     the phone menu panel is position:fixed, inset:0, nested inside this
     header. On .nav it would size the menu to the 77px bar again. A
     pseudo-element has no descendants, so it cannot do that. */
  .nav { background: transparent; }
  .nav::before,
  .nav--stuck::before {
    display: block;
    opacity: 1;
    inset: 0;
    height: auto;
    /* THE ALPHA HAS TO DO THIS ON ITS OWN, and it was being asked to do half.

       0.52 with an 18px backdrop blur reads as a frosted bar on a device that
       renders backdrop-filter well. Android Chrome frequently does not — it
       drops it under memory pressure, and over a long scrolling page it is one
       of the first things it stops paying for — and what is left is a 52%
       wash. The page then reads straight through the wordmark: in the
       screenshots, "TECHNIQUE / Pietra dura inlay and repeating carved fields"
       is legible THROUGH the word STONEGRAPH.

       A fixed bar is opaque or it is not a bar. The blur stays, because where
       it works it is better than a flat panel — but it is now the enhancement
       and the alpha is the floor. */
    /* The 18px blur is gone with the 8px one above, and for the same reason:
       this scrim is the full width of a sticky bar, so it was the second
       per-frame backdrop pass on the same element. The note above argued the
       alpha had to hold on its own because Android drops the blur under
       memory pressure — it now holds on its own everywhere, at the opacity
       the stuck bar uses. */
    background: rgba(233, 227, 213, 0.97);
  }
  /* Stuck, it firms up the rest of the way: by then the bar is passing over
     the photographs themselves, which it has no business competing with. */
  .nav--stuck::before {
    background: rgba(233, 227, 213, 0.97);
  }
  .nav--stuck {
    background: transparent !important;
    border-bottom-color: transparent !important;
    box-shadow: none;
  }
  /* The soft bottom edge moves to ::after, so the bar ends in a fade rather
     than a drawn line. */
  .nav::after {
    content: "";
    position: absolute;
    inset: 100% 0 auto 0;
    height: 16px;
    /* The fade below the bar, not the bar itself. It starts from the bar's own
       strength so there is no visible step where one ends and the other
       begins. */
    background: linear-gradient(180deg, rgba(233, 227, 213, 0.72) 0%, rgba(233, 227, 213, 0) 100%);
    pointer-events: none;
    z-index: -1;
  }
  .nav--stuck::after {
    background: linear-gradient(180deg, rgba(233, 227, 213, 0.82) 0%, rgba(233, 227, 213, 0) 100%);
  }
  /* Behind the open menu both are pointless — the panel is solid sand. */
  .nav--open::before,
  .nav--open::after { opacity: 0; }
}

@media (max-width: 760px) {
  .hero__film::after {
    background:
      linear-gradient(180deg,
        rgba(233, 227, 213, 0.08) 0%,
        rgba(233, 227, 213, 0.08) 16%,
        rgba(233, 227, 213, 0.42) 27%,
        rgba(233, 227, 213, 0.60) 46%,
        rgba(233, 227, 213, 0.80) 70%,
        rgba(233, 227, 213, 0.96) 92%,
        rgba(233, 227, 213, 1) 100%);
  }
}

/* ==========================================================================
   THE PEACOCK IN THE ENQUIRY PANEL
   --------------------------------------------------------------------------
   Same fault as the mandala, in the one place left that still had it. The
   drawing is centred in .panel__main and the copy is given z-index 2 on top
   of it, so on a laptop a 468px peacock sits with its lower half printed
   under the heading and the paragraph — half a drawing, and a busy ground
   under text that has to be read.

   Measured: .panel__main is 487 x 776 on a laptop and every word in it lives
   between y 479 and 734. The top 479px is empty. So the peacock moves up into
   the space that was already there, at a size that clears the copy by about
   50px, and can be shown at proper strength for once because nothing is
   sitting on it.

   On a phone the same box is 360 x 300 and the copy runs from y 7 to 270 —
   there is no empty space at all, and any drawing placed there is by
   definition underneath something. It is not shown.
   ========================================================================== */
@media (min-width: 821px) {
  .panel__art {
    top: 0;
    transform: none;
    right: 3%;
    width: min(86%, 418px);
    opacity: 0.6;
  }
}
@media (max-width: 820px) {
  .panel__art { display: none; }
}


/* ==========================================================================
   THE FOOTER'S LEGAL LINE — stated once, after everything that reaches it
   --------------------------------------------------------------------------
   `<p class="mono">` at the foot of the page is caught by three separate rules
   that bump mono type at phone widths — .mono in this file at 720px and 400px,
   and .foot__bottom p in site.css at 760px. Every one of them is right for the
   page's own labels, where 11px letter-spaced uppercase is genuinely hard to
   read, and wrong for a forty-character copyright line that only has to fit.

   At 0.82rem and 0.13em it wrapped onto two lines on a 375px screen. Stated
   here, at the end of the last stylesheet the page loads, it needs no
   !important and no specificity war: it is simply the last word.
   ========================================================================== */
@media (max-width: 760px) {
  .foot__bottom p,
  .foot__bottom p.mono,
  .foot__legal a,
  .foot__legal span {
    font-size: 10.5px;
    letter-spacing: 0.05em;
    line-height: 1.5;
  }
}


/* ==========================================================================
   THE MOUNT
   --------------------------------------------------------------------------
   What the site draws around an uploaded picture, so a photograph added from
   the panel arrives looking like part of the site rather than pasted onto it.
   Four treatments, chosen per asset; the ids are the ones in
   src/lib/frames.ts and nowhere else.

   THE RATIO SITS ON THE PICTURE, NOT ON THE BOARD, and that is the whole of
   the geometry. The first version put it on the outer element together with
   `box-sizing: content-box`, reasoning that the ratio describes the picture
   and so the board should sit outside it. It does describe the picture — but
   content-box also means `width: 100%` resolves against the container BEFORE
   the padding is added, so a mount inside a 335px column measured 357px and
   hung 22px past its own figure. Measured on a 375px phone, the board ran to
   x=377: two pixels off the screen.

   Inverting it costs nothing and cannot overflow. The board is a plain
   border-box element the width of its column; the picture inside it carries
   the ratio. The board is still outside the ratio, which was the point, and
   the arithmetic now runs the right way round.

   Every colour is a token off the ramp at the top of site.css. Nothing here
   invents a value, which is the only way a mount stays in the palette after
   the palette moves.
   ========================================================================== */

.sg-frame {
  position: relative;
  display: block;
  box-sizing: border-box;
  width: 100%;
  max-width: 100%;
  isolation: isolate;
}
/* The picture. The renderer writes `aspect-ratio` here as an inline style,
   because the ratio is per-card data — a class per ratio would mean this
   stylesheet carried a copy of the closed list too, and two copies of a list
   is one copy too many. */
.sg-frame__inner {
  position: relative;
  width: 100%;
  overflow: hidden;
}
.sg-frame__media {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
  object-position: center;
  display: block;
}
/* "As shot" carries no ratio, so the picture sets its own height and there is
   nothing for an absolutely positioned child to fill. */
.sg-frame__inner:not([style*="aspect-ratio"]) .sg-frame__media {
  position: static;
  height: auto;
}

/* ---- 1. the museum mount ----
   A board, a deep margin, and a hairline of gold set inside the margin rather
   than drawn on the picture's own edge. The margin is the whole effect: a
   frame with no air in it is a border, and a border is what every template
   already does. The rule sits a few pixels off the picture so the eye reads
   board, then rule, then work — the order a real mount is cut in. */
.sg-frame--mount {
  background: var(--stone-050);
  padding: clamp(12px, 2.2vw, 22px);
  border: 1px solid var(--stone-150);
  box-shadow: 0 1px 0 rgba(255, 255, 255, .6) inset;
}
.sg-frame--mount .sg-frame__inner {
  outline: 1px solid rgba(194, 170, 126, .55);
  outline-offset: 3px;
}

/* ---- 2. the dark plate ----
   The near-black ground the studio benches are drawn on, so a photograph
   dropped into a dark band belongs to it. Same geometry; the board is basalt
   and the rule is warmer against it. */
.sg-frame--plate {
  background:
    radial-gradient(ellipse 70% 70% at 50% 40%, rgba(194,170,126,.10), transparent 70%),
    linear-gradient(160deg, var(--stone-800) 0%, var(--stone-900) 60%, var(--stone-950) 100%);
  padding: clamp(12px, 2.2vw, 22px);
  border: 1px solid rgba(194, 170, 126, .18);
}
.sg-frame--plate .sg-frame__inner {
  outline: 1px solid rgba(194, 170, 126, .40);
  outline-offset: 3px;
}

/* ---- 3. the hairline ---- full bleed, one rule, no margin. */
.sg-frame--edge {
  border: 1px solid var(--stone-200);
  background: var(--stone-050);
}

/* ---- 4. none ---- */
.sg-frame--none { border: 0; background: none; padding: 0; }

/* The gold rule is drawn 3px outside the picture. On a phone, where the margin
   is 12px rather than 22, that would put it almost on the board's own border,
   so it is pulled in. The margin itself holds at 12px rather than following
   the viewport down: a mount needs its air more than it needs to be
   proportional, and below about ten pixels it stops reading as a mount. */
@media (max-width: 620px) {
  .sg-frame--mount .sg-frame__inner,
  .sg-frame--plate .sg-frame__inner { outline-offset: 2px; }
}

/* A film in a mount keeps the clipping and never its own controls. */
.sg-frame video::-webkit-media-controls { display: none !important; }


/* ==========================================================================
   SELECTED WORK — the studio page's band
   ========================================================================== */
.stu-works {
  display: grid;
  gap: clamp(28px, 4.5vw, 56px);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
}
.stu-work { display: flex; flex-direction: column; gap: 16px; }
.stu-work__c { margin: 0 0 8px; }
.stu-work__t {
  font-family: var(--f-display, Georgia, serif);
  font-weight: 300;
  font-size: clamp(1.25rem, 2.4vw, 1.7rem);
  line-height: 1.2;
  letter-spacing: -.01em;
  margin: 0;
}
.stu-work__b {
  margin: 10px 0 0;
  font-size: .95rem; font-weight: 300; line-height: 1.7;
  color: var(--ink-mute, var(--stone-500));
}
.stu-work__m {
  margin: 12px 0 0;
  font-family: var(--f-mono, ui-monospace, monospace);
  font-size: .68rem; letter-spacing: .14em; text-transform: uppercase;
  /* Was rgba(194,170,126,.9) — the dark-ground gold on the studio page's cream
     Work band, where it measured 1.47:1. --stu-gold resolves to the light-ground
     value inside .stu and to the pale one inside the hero, so the same line
     reads correctly wherever a Work card is placed. The literal fallback is for
     the one case the variable is not in scope. */
  color: var(--stu-gold, var(--accent, #6F5A3C));
}

/* ==========================================================================
   LATEST WORK — the home page's single card
   --------------------------------------------------------------------------
   Beside the "Start here" band rather than in a section of its own. A second
   full-width band for one card is how a home page starts to scroll forever.
   ========================================================================== */
.latest { background: var(--stone-050); }
.latest__in {
  display: grid;
  gap: clamp(26px, 4vw, 64px);
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 820px) {
  .latest__in { grid-template-columns: 1.05fr .95fr; }
}
.latest__m {
  margin: 16px 0 26px;
  font-family: var(--f-mono, ui-monospace, monospace);
  font-size: .7rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--stone-500);
}
.latest__txt .display { margin: 10px 0 14px; }


/* ==========================================================================
   THE SINGLE MOUNTED PLATE
   --------------------------------------------------------------------------
   One piece of work, under the header, on the pages that are otherwise all
   grid. Collections and Portfolio are walls — every photograph on them is a
   tile sized by the grid — so neither page had anywhere a single image was
   allowed to be the subject, and a mount is mostly margin: at tile size it is
   only a border.

   Sized against the reading measure rather than the full band. A mount 1200px
   wide has a 22px margin, which is a rule near an edge; at 780px the same
   margin reads as a mount. The frame is the subject here, so it is given the
   width the frame was drawn for.
   ========================================================================== */
.plate-one { padding-block: clamp(38px, 6vw, 86px); }
.plate-one__fig {
  margin: 0 auto;
  max-width: min(780px, 100%);
}
.plate-one__cap {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 14px;
  margin-top: 16px;
  font-family: var(--f-mono, ui-monospace, monospace);
  font-size: .66rem;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.plate-one__k { color: var(--accent, #8A6E3C); }
.plate-one__t {
  font-family: var(--f-display, Georgia, serif);
  font-size: 1.02rem;
  letter-spacing: .01em;
  text-transform: none;
}
.plate-one__m { color: var(--stone-500); }

/* On a phone the caption stacks. Three items on one line at 11px is three
   fragments, not a caption. */
@media (max-width: 620px) {
  .plate-one__cap { flex-direction: column; gap: 5px; }
  .plate-one__t { font-size: 1.12rem; }
}

/* ========================================================================== 
   CONTENT MEDIA — one permanent, no-crop contract
   --------------------------------------------------------------------------
   Photographs of finished work are evidence, not decorative backgrounds.
   Every content image is therefore fitted wholly inside its own box, even if
   a legacy card ratio or an admin-selected ratio differs from the file. The
   cinematic hero reel is intentionally absent: it is a background layer and
   continues to fill its band. This block is last so older layout experiments
   cannot silently put a cover crop back on artwork.
   ========================================================================== */
.piece__img img,
.wall .piece__img img,
.card__media img,
.work__media img,
.pcard__media img,
.feature__media img,
.frame > img,
.post__cover img,
.sg-frame__media {
  max-width: 100% !important;
  object-fit: contain !important;
  object-position: center !important;
}

/* Scaling media inside an overflow-clipped box is itself a crop. Cards still
   answer hover/touch by lifting the whole card, but the photograph never grows
   past its own frame. */
.card:hover .card__media .plate,
.card:active .card__media .plate,
.work:hover .work__media .plate,
.work:active .work__media .plate,
.works .work:hover .work__media .plate,
.works .work:active .work__media .plate,
.pcard:hover .pcard__media .plate,
.pcard:hover .pcard__media img,
.pcard:active .pcard__media .plate,
.pcard:active .pcard__media img,
.wall .piece:hover .piece__img img,
.wall .piece:active .piece__img img {
  transform: none !important;
}

/* A fixed 18px backdrop blur has to be recomposited against every photograph
   that passes under the phone header. On coarse touch devices the same
   contrast is achieved with an opaque sandstone ground, so scrolling stays
   smooth and neither image nor text can ghost through the wordmark. */
@media (max-width: 1200px) and (pointer: coarse) {
  .nav::before,
  .nav--stuck::before {
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    background: rgba(233, 227, 213, 0.975) !important;
  }
}

/* The hero has exactly three short lineage figures. Collapsing that triptych
   to two columns on a tablet manufactured an empty fourth cell; three columns
   remain comfortably readable at this width and preserve the rule-of-thirds
   composition used on phone and desktop. */
@media (min-width: 621px) and (max-width: 1100px) {
  .specs { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .specs__item:nth-child(2) { border-right: 1px solid var(--rule); }
  .specs__item:nth-child(3) {
    border-top: 0;
    border-right: 0;
  }
}

/* ========================================================================== 
   JOURNAL — desktop editorial spread
   --------------------------------------------------------------------------
   A cover used to live in the middle track of a deliberately narrow post.
   That was balanced for text-only entries, but a portrait cover became an
   isolated vertical strip with unused page on both sides. Above 900px the
   journal now uses the site's full 1,400px frame. A media-bearing post keeps
   its folio and metadata rails, while its main track divides cover and copy
   by the golden ratio (1.618 : 1). This gives every photograph a clear edge
   and every headline a related reading column instead of floating beside it.

   The existing <=900px post rules remain the phone/tablet contract: no
   selector below applies there, so the compact layout is unchanged. */
@media (min-width: 901px) {
  /* .journal-list also carries .wrap--narrow in the shipped HTML. The
     journal is a long-form editorial spread, so on a laptop it needs the
     same frame as the rest of the site; the old narrow cap isolated a cover
     in the middle of a very wide page. */
  .wrap.journal-list {
    width: 100%;
    max-width: var(--wrap);
  }

  .journal-list .post.post--with-media {
    grid-template-columns:
      clamp(42px, 4.2vw, 62px)
      minmax(0, 1fr)
      clamp(150px, 13vw, 190px);
    column-gap: clamp(22px, 2.5vw, 38px);
  }

  .journal-list .post.post--with-media > :nth-child(2) {
    display: grid;
    grid-template-columns: minmax(0, 1.38fr) minmax(260px, 1fr);
    grid-template-rows: auto 1fr;
    column-gap: clamp(24px, 3vw, 46px);
    align-items: start;
  }

  .journal-list .post.post--with-media .post__cover {
    grid-column: 1;
    grid-row: 1 / span 2;
    width: 100%;
    margin: 0;
  }

  .journal-list .post.post--with-media .post__t {
    grid-column: 2;
    grid-row: 1;
    margin-bottom: 14px;
    text-wrap: balance;
  }

  .journal-list .post.post--with-media .post__x {
    grid-column: 2;
    grid-row: 2;
    max-width: 42ch;
    text-wrap: pretty;
  }
}

/* ========================================================================== 
   COLLECTIONS + PORTFOLIO — laptop-safe media scale
   --------------------------------------------------------------------------
   A project section without a purpose-built media slot takes the renderer's
   fallback path. That used to make a portrait upload `width:100%` of the full
   1,400px wrap, producing a photograph taller than the laptop viewport. This
   is a gallery mount, not a crop: either dimension may reach the limit, never
   both exceed it, and the whole file remains visible in the centre.

   Collection tiles get the same height guard only on short desktop screens.
   Their grid and captions keep their existing width; a very tall photograph
   gains quiet stone margins inside its mount instead of running beyond a full
   screen. Phone/tablet composition is deliberately untouched. */
.portfolio-project-media {
  display: grid;
  place-items: center;
  width: 100%;
  max-height: min(72svh, 720px);
  margin: 0 auto clamp(28px, 4vw, 52px);
  overflow: hidden;
}
.portfolio-project-media > .work__media--media {
  display: grid;
  place-items: center;
  width: 100%;
  height: auto !important;
  max-height: inherit;
  background: transparent !important;
}
.portfolio-project-media img,
.portfolio-project-media video {
  display: block;
  width: auto !important;
  height: auto !important;
  max-width: 100%;
  max-height: min(72svh, 720px);
  margin-inline: auto;
  object-fit: contain !important;
  object-position: center !important;
}

@media (min-width: 1001px) and (max-height: 900px) {
  .wall .piece__img,
  .wall .piece__img[style] {
    max-height: min(72svh, 680px) !important;
  }
}

@media (max-width: 700px) {
  .portfolio-project-media {
    max-height: none;
  }
  .portfolio-project-media img,
  .portfolio-project-media video {
    width: 100% !important;
    max-height: none;
  }
}

/* At laptop widths the cover and copy share the row, but the available
   reading measure is not wide enough for two columns on a small tablet. Keep
   the established one-column tablet/phone layout there rather than allowing
   a narrow headline ribbon or horizontal overflow. */
@media (min-width: 901px) and (max-width: 1100px) {
  .journal-list .post.post--with-media {
    grid-template-columns: clamp(40px, 7vw, 60px) minmax(0, 1fr) clamp(130px, 16vw, 170px);
    column-gap: clamp(18px, 2.5vw, 28px);
  }

  .journal-list .post.post--with-media > :nth-child(2) {
    display: block;
  }

  .journal-list .post.post--with-media .post__cover {
    margin-bottom: clamp(14px, 2vw, 20px);
  }

  .journal-list .post.post--with-media .post__t,
  .journal-list .post.post--with-media .post__x {
    max-width: 62ch;
  }
}

/* ========================================================================== 
   iPHONE AUTOPLAY — THE SAME MP4, WITHOUT THE MEDIA-SESSION GATE
   --------------------------------------------------------------------------
   Low Power Mode makes WebKit reject even a silent, muted, inline <video> and
   paint a native play control. It is an OS policy: another play() call cannot
   override it. Safari also decodes H.264 MP4 as an animated image. On a coarse
   touch WebKit device we use that path, so the exact reel starts and loops on
   first paint without controls or a tap. Other engines stay on <video>.
   ========================================================================== */
.hero__ios-reel { display: none !important; }

@supports (-webkit-touch-callout: none) {
  @media (hover: none) and (pointer: coarse) {
    .hero__film.hero__film--ios-reel > video {
      display: none !important;
    }

    .hero__film.hero__film--ios-reel > picture.hero__ios-reel {
      position: absolute;
      inset: 0;
      z-index: 0;
      display: block !important;
      width: 100%;
      height: 100%;
      overflow: hidden;
      pointer-events: none;
      opacity: 0;
      transition: opacity .42s cubic-bezier(.22,.61,.36,1);
      animation: hero-ios-reel-failsafe .42s cubic-bezier(.22,.61,.36,1) .45s forwards;
    }

    .hero__film.hero__film--ios-reel.is-film-ready > picture.hero__ios-reel {
      opacity: 1;
      animation: none;
    }

    .hero__film.hero__film--ios-reel > picture.hero__ios-reel > img {
      display: block;
      width: 100%;
      height: 100%;
      max-width: none;
      object-fit: cover;
      object-position: var(--stu-focal, var(--focal, center));
      transform: none;
      backface-visibility: visible;
    }

    @media (max-width: 759px) {
      .hero__film.hero__film--ios-reel > picture.hero__ios-reel > img {
        object-position: var(--stu-focal, var(--focal-m, 50% 38%));
      }
    }
  }
}

@keyframes hero-ios-reel-failsafe {
  to { opacity: 1; }
}
