/* ============================================================
   MAISON MADERA
   ------------------------------------------------------------
   1.  Tokens
   2.  Reset + base
   3.  Utilities
   4.  Header
   5.  Hero
   6.  Epitome (scroll image feature)
   7.  Modes (expanding panels)
   8.  Logo wall
   9.  Featured projects
   10. All work (cursor collage)
   11. CTA typewriter
   12. Footer
   13. Responsive
   14. Reduced motion
   ============================================================ */

/* ---------- 1. Tokens ---------- */
:root {
  --mm-black:      #000;
  --mm-off:        #0a0a0a;
  --mm-white:      #fff;
  --mm-muted:      rgba(255, 255, 255, .55);
  --mm-faint:      rgba(255, 255, 255, .16);
  --mm-crest:      #5e1518;
  --mm-gold:       #c9a227;
  --mm-gold-soft:  rgba(201, 162, 39, .22);

  /* Swap this pair when the licensed brand faces arrive. */
  --mm-sans: "Inter", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  --mm-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* Section heading and band copy sizes. Named because the text bands need
     both values to line the copy's cap line up with the heading's — see
     the cap-line note on .epitome__copy. */
  --mm-title-size:     clamp(1.175rem, 3.49vw, 2.855rem);
  --mm-band-copy-size: clamp(.85rem, 1.05vw, 1.02rem);

  /* Display serif for the big headings. The intended face is Ogg (Sharp
     Type) — commercially licensed, so it is not in the repo.

     Playfair Display stands in for it — the closest free match after
     comparing candidates against the Ogg specimen. It carries the high
     contrast and stem weight of Ogg's roman, its serifs are bracketed
     rather than flat, and its ampersand is calligraphic like Ogg's.
     Bodoni was too mechanical (flat unbracketed serifs, hairlines too
     fine); Cormorant had the right Renaissance bones but read far too
     light and delicate beside the specimen.

     Ogg is listed first so it takes over the moment the @font-face block
     above is filled in. Nothing else needs changing. */
  --mm-serif: "Ogg", "Playfair Display", "Didot", Georgia, serif;

  /* Separate stand-in for the italic cut. Playfair's italic is too upright
     and restrained next to Ogg's, which is swashy and calligraphic with
     very high contrast; Bodoni Moda's italic carries that flow much better.
     Two families is a compromise the real font removes — Ogg is first in
     both stacks, so licensing it collapses these back into one face. */
  --mm-serif-italic: "Ogg", "Bodoni Moda", "Didot", Georgia, serif;

  --mm-gut:   clamp(1.25rem, 4vw, 3.5rem);

  /* The clear space above and below every text band — heading, paragraph, or
     both. One value everywhere so the sections breathe alike; the four bands
     used to run 99/55, 66/44, 165/83 and 44/16 at this width, which read as
     four different rhythms rather than one. */
  /* The Projects line above the grid, measured: about 48px at desktop since
     the titles came down, less on a phone. Only the grid's height budget
     uses it — it is an allowance for a line of type, not a size imposed on
     one — and it has to follow the title size, or the grid stops short of
     filling the screen. */
  --mm-workgrid-head: 3.05rem;

  --mm-band-space: clamp(4rem, 9vw, 8rem);

  /* Clearance under the fixed header, for sections that start at the top of
     the viewport rather than arriving from below. Built from the header's
     own height — the 56px seal plus its 1.35rem above and below, see
     .site-header — plus a band of air, so a heading never sits under the
     seal. The old value fell short of the header on a phone and put THE BOH
     12px below it. */
  --mm-head-clear: calc(56px + 2.7rem + clamp(1.5rem, 5vw, 1.75rem));

  /* How long the about section holds still while its collage runs. Read as
     scroll distance, not height: it is what the page travels between the
     copy pinning and letting go, and initAboutScroll() paces the reels to
     cover roughly one full turn of photographs in it. */
  --mm-about-hold: 140vh;

  --mm-ease:  cubic-bezier(.22, .61, .36, 1);
  /* Its mirror, for things leaving rather than arriving. An ease-out played
     backwards decelerates into the exit, which reads as hesitation. */
  --mm-ease-in: cubic-bezier(.64, 0, .78, .39);
  --mm-slow:  .75s;
}

/* ---------- 1b. Licensed display face ----------
   Ogg (Sharp Type, https://www.sharptype.co/typefaces/ogg) is the intended
   heading face. It needs a paid webfont licence, so the files are not in
   this repo. To switch it on:

     1. Buy the webfont licence and download the kit.
     2. Drop Ogg-Medium.woff2 (and .woff) into assets/fonts/.
     3. Uncomment the block below, and remove Playfair Display and Bodoni Moda from the
        Google Fonts <link> in index.html.

   Both --mm-serif and --mm-serif-italic ask for "Ogg" first, so nothing else
   has to change. Only the medium roman is used today; add faces here if the
   design later calls for the italic or other weights.

@font-face {
  font-family: "Ogg";
  src: url("../assets/fonts/Ogg-Medium.woff2") format("woff2"),
       url("../assets/fonts/Ogg-Medium.woff")  format("woff");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
*/

/* ---------- 2. Reset + base ---------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Also set inline in each page's head, so it is black before this file
     arrives; repeated here so the rule lives with the rest of the page. */
  background: var(--mm-black);
  color-scheme: dark;
}

/* Page to page, a cross-fade instead of a cut. The browser keeps the old
   page on screen until the new one is ready to draw, then blends one into
   the other — no white frame, no half-built page flashing up. Only between
   pages on this site; browsers that do not support it simply navigate.

   The header is given its own name, so it is treated as the same object on
   both pages and holds still while the page beneath it changes, rather than
   fading out and back in with everything else. */
@view-transition { navigation: auto; }

.site-header { view-transition-name: site-header; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .38s;
  animation-timing-function: var(--mm-ease);
}

/* Reduced motion: a plain navigation, no fade. */
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

body {
  margin: 0;
  /* The hero curtain changes the height of the first section as the page
     scrolls. Scroll anchoring exists to absorb exactly that — it would
     nudge the scroll position back to keep the content below still, which
     feeds into the next frame's height and sets the two oscillating
     against each other. The curtain is the intended movement, so anchoring
     has to stay out of it. */
  overflow-anchor: none;
  background: var(--mm-black);
  color: var(--mm-white);
  font-family: var(--mm-sans);
  font-weight: 300;
  line-height: 1.5;
  /* clip, not hidden: hidden would make body a scroll container, which
     breaks position:sticky for any descendant. clip does not. */
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
}

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

h1, h2, h3, p, figure, ul { margin: 0; }
ul { padding: 0; list-style: none; }

a { color: inherit; text-decoration: none; }

button {
  margin: 0; padding: 0;
  font: inherit; color: inherit;
  background: none; border: 0; cursor: pointer;
}

:focus-visible {
  outline: 1px solid var(--mm-white);
  outline-offset: 4px;
}

/* The flash a touch browser paints over whatever you tap — a blue or grey
   box that lands on top of the artwork and takes no notice of the design.
   It inherits, so the root turns it off everywhere. Nothing is lost: the
   rule above is what actually marks a focused control, and it still fires
   for anyone on a keyboard. */
:root { -webkit-tap-highlight-color: transparent; }

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

.skip-link {
  position: absolute;
  top: -100px; left: var(--mm-gut);
  z-index: 999;
  padding: .75rem 1.25rem;
  background: var(--mm-white); color: var(--mm-black);
  font-size: .75rem; text-transform: uppercase; letter-spacing: .12em;
  transition: top .2s var(--mm-ease);
}
.skip-link:focus { top: 1rem; }

/* Shared oversized section heading. Display serif, roman — a high-contrast
   face needs a touch more line-height and none of the negative tracking a
   grotesque wanted. */
.section-title {
  font-family: var(--mm-serif);
  /* 75% of the original clamp(1.75rem, 5.2vw, 4.25rem), then brought down
     about four points more at desktop size (51px to 45.6 at the cap) — each
     time all three terms scaled together, by the same factor, so the curve
     keeps its shape and a phone's titles shrink in proportion rather than by
     the same four points, which would have taken them from 21px to 16. */
  font-size: var(--mm-title-size);
  /* 500 = the "Medium" the specimen labels its roman. Playfair is a
     heavier design than the previous stand-in, so this reads sturdier
     than 700 did there. */
  font-weight: 500;
  font-style: normal;
  line-height: 1.06;
  letter-spacing: .005em;
  text-transform: uppercase;
}

/* Italic flourish inside a heading — 600 to echo the specimen, which shows
   its italic as a bold. The right-side padding stops the slanted final
   letter from crowding the upright word after it. */
.section-title .is-italic {
  font-family: var(--mm-serif-italic);
  font-style: italic;
  font-weight: 600;
  /* Bodoni's italic runs a touch smaller on the cap height than Playfair's
     roman, so nudge it up to sit on the same optical line. */
  font-size: 1.04em;
  padding-right: .08em;
}

/* ---------- 4. Header ---------- */
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  /* 1.35rem above and below a 56px seal keeps the bar at the same height it
     had with the 64px seal before — see .crest. */
  padding: 1.35rem var(--mm-gut);
  /* Transparent at rest so the hero runs to the top of the screen, and the
     colour is what fades in. Named explicitly rather than left to the
     initial value: a shorthand `background` on the stuck state would have
     nothing to transition from otherwise. */
  background-color: transparent;
  transition: background-color .35s var(--mm-ease);
}
/* Opaque, not a wash. At 72% with a blur behind it the nav still had page
   showing through, and a heading passing underneath read straight across
   the words — which is the whole thing this is here to stop. Solid black
   ends it outright, and the blur goes with it: there is nothing left to
   see through, so it was only work. */
.site-header.is-stuck {
  background-color: var(--mm-black);
}

/* Flipped while the header sits over a light section, otherwise the white
   nav would be invisible. Toggled in initHeader() off [data-light-section].
   Dormant right now — every section is black — but kept wired so a light
   section only has to carry the attribute. The crest is a silver wax seal with
   a dark rim, so it reads on either and needs no treatment. */
.site-header.is-on-light .site-nav a { color: var(--mm-black); }
.site-header.is-on-light.is-stuck {
  background-color: var(--mm-white);
}

/* 56px, not 64. The silver seal file is trimmed to its edges, where the
   earlier seals carried a transparent border; at 64px it read larger than
   them and crowded the top of the screen. 56px puts it back at their size,
   about 22px down. */
.crest { display: block; width: 56px; }
.crest img {
  width: 100%;
  height: auto;
  transition: transform .5s var(--mm-ease);
}
.crest:hover img { transform: rotate(-6deg) scale(1.04); }

.site-nav {
  display: flex;
  gap: clamp(1.25rem, 3vw, 2.75rem);
  font-size: .7rem;
  text-transform: uppercase;
  letter-spacing: .18em;
}
.site-nav a {
  position: relative;
  padding-bottom: 2px;
  color: var(--mm-white);
}
.site-nav a::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform .45s var(--mm-ease);
}
.site-nav a:hover::after { transform: scaleX(1); transform-origin: left; }

/* ---------- the menu ----------
   Two lines at every width, not only on a phone. The inline links came off
   the desktop bar as well, so the header is a crest at one end and this
   control at the other whatever the screen. */

/* A hairline each, the same weight as the rule that runs under a nav link
   on hover, so it belongs to the same drawing as the rest of the header
   rather than being an icon dropped into it. */
.nav-toggle {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--mm-white);
  cursor: pointer;
}
.nav-toggle span {
  display: block;
  width: 100%;
  height: 1px;
  background: currentColor;
  transition: transform .35s var(--mm-ease);
}
.site-header.is-on-light .nav-toggle { color: var(--mm-black); }

/* Open: they cross. Half the gap each, so the two meet in the middle of
   where they were rather than at one of them. */
.site-header.is-nav-open .nav-toggle span:first-child {
  transform: translateY(3.5px) rotate(45deg);
}
.site-header.is-nav-open .nav-toggle span:last-child {
  transform: translateY(-3.5px) rotate(-45deg);
}

/* The menu itself: a panel under the bar rather than a full screen. Three
   items do not need the whole page, and leaving what is under it in view
   keeps the sense of where you are.

   Out of the flow, so the header stays a crest at one end and two lines at
   the other. visibility, not just opacity, so the links cannot be tabbed
   into while the panel is shut. */
.site-nav {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  flex-direction: column;
  gap: 1.4rem;
  padding: 1.5rem var(--mm-gut) 2rem;
  background: var(--mm-black);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition: opacity .35s var(--mm-ease),
              transform .35s var(--mm-ease),
              visibility .35s;
}
.site-header.is-nav-open .site-nav {
  opacity: 1;
  visibility: visible;
  transform: none;
}

/* Wider than a phone there is no panel. The links open as a row inside the
   bar, immediately to the left of the two lines, and shoot out from them:
   each starts a little to the right, over where the lines are, and slides
   left into place while it fades up — the one nearest the lines first, the
   farthest last, so the row reads as coming out of the control rather than
   appearing beside it.

   The bar itself is left alone: no black band, since nothing hangs below
   it. Closing pulls all three back at once, and visibility waits for that
   to finish before it hides them, so they cannot be tabbed into while shut
   and do not vanish mid-slide. */
@media (min-width: 561px) {
  .site-nav {
    top: 50%;
    left: auto;
    right: calc(var(--mm-gut) + 26px + clamp(1.25rem, 2.5vw, 2.25rem));
    flex-direction: row;
    align-items: center;
    gap: clamp(1.25rem, 3vw, 2.75rem);
    padding: 0;
    background: none;
    opacity: 1;
    transform: translateY(-50%);
    transition: visibility 0s linear .45s;
  }
  .site-header.is-nav-open .site-nav {
    transform: translateY(-50%);
    transition: visibility 0s;
  }

  .site-nav a {
    opacity: 0;
    transform: translateX(32px);
    transition: opacity .3s var(--mm-ease), transform .45s var(--mm-ease);
  }
  .site-header.is-nav-open .site-nav a {
    opacity: 1;
    transform: none;
  }
  /* Nearest the lines first. */
  .site-header.is-nav-open .site-nav a:nth-child(3) { transition-delay: 0s; }
  .site-header.is-nav-open .site-nav a:nth-child(2) { transition-delay: .07s; }
  .site-header.is-nav-open .site-nav a:nth-child(1) { transition-delay: .14s; }
}

/* On a phone the panel hangs below the bar, and the bar is transparent until
   the page scrolls — which would leave the panel hanging off something you
   can see straight through, so open is its own reason to be solid. And
   there is no cursor for the hover underline to answer to. */
@media (max-width: 560px) {
  .site-header.is-nav-open { background-color: var(--mm-black); }
  .site-nav a::after { content: none; }

  /* The links hang from the right, under the control that opened them,
     rather than starting at the far side of the screen from it. The panel
     shares the bar's gutter, so their right edge lines up with the cross.

     And they come out from that side: each starts a little to the right and
     slides in as it fades up, top to bottom in quick succession — the same
     gesture as the desktop row shooting out from the lines, turned on its
     side. The panel itself only fades, so the movement belongs to the
     links. Closing pulls them back together. */
  .site-nav {
    align-items: flex-end;
    transform: none;
  }
  .site-nav a {
    opacity: 0;
    transform: translateX(24px);
    transition: opacity .3s var(--mm-ease), transform .42s var(--mm-ease);
  }
  .site-header.is-nav-open .site-nav a {
    opacity: 1;
    transform: none;
  }
  .site-header.is-nav-open .site-nav a:nth-child(1) { transition-delay: .02s; }
  .site-header.is-nav-open .site-nav a:nth-child(2) { transition-delay: .08s; }
  .site-header.is-nav-open .site-nav a:nth-child(3) { transition-delay: .14s; }
}

/* ---------- 5. Hero ---------- */
/* Stops short of the fold so a band of black shows under the still — enough
   to say the page carries on, not enough to bring the next section's
   heading into view. The strip is a clamped length rather than a share of
   the viewport so it stays the same visual thickness on any screen; as a
   percentage it would grow with height and, on a tall window, overrun the
   epitome band's top padding and pull the heading up into the fold. */
.hero {
  /* The panel below is sized from this too, so the two only ever differ by
     what initHeroCurtain() takes off the section's own height.

     The floor is inside the variable rather than a min-height on the
     section: min-height would also apply to the height the curtain writes
     each frame, so the wind-up would stall partway and the rest of the
     hero would scroll away normally. */
  --mm-hero-full: max(calc(100svh - clamp(2.5rem, 5vw, 4.5rem)), 340px);

  position: relative;
  height: var(--mm-hero-full);
  overflow: hidden;
  background: var(--mm-black);
}

/* Holds its full height while the section around it shortens, and is
   anchored to the top, so the section's bottom edge travelling upward
   trims the still from below — a blind winding onto a roller rather than
   a picture being squashed. */
.hero__panel {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: var(--mm-hero-full);
}

/* Full bleed above the fold, and now a clean plate: the wordmark is its own
   layer, so the frame can be cropped however the box wants without cutting
   into type. */
/* <picture> is inline by default, which would leave the still sitting on a
   text baseline inside the panel rather than filling it. */
.hero__frame {
  display: block;
  width: 100%;
  height: 100%;
}

/* No grade. There was one here — saturation to 45% — because the wide plate
   was shot gold, at 31 points of red over blue. The frame it corrected has
   been replaced by one already cool at 9, and running the old correction
   over it took it to 4 and near grey. A filter that fixes a photograph is
   worth keeping only while that photograph is the one in the file. */
.hero__media {
  /* block, or the video sits on a text baseline and leaves a sliver under
     it the way the old inline <picture> would have. */
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center;
}

/* Top and bottom gradients for the nav and the fold, over a flat 20% shade
   across the whole film. The flat layer is for the wordmark: the montage
   has bright clips — a white studio, daylight crowds — where white type
   over them fell under 3:1 for about four seconds of every loop. Measured
   across the film, 20% brings that to half a second; 30% clears it
   entirely but flattens the footage more than it needs. Approved by the
   client 2026-10-20. The wordmark sits above this layer (z-index on
   .hero__wordmark), so only the film is darkened, never the mark. */
.hero__scrim {
  position: absolute; inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom, rgba(0,0,0,.55) 0%, transparent 28%),
    linear-gradient(to top,    rgba(0,0,0,.45) 0%, transparent 26%),
    rgba(0,0,0,.2);
}

/* The mark, over the still. Sized to the panel rather than to the type it
   used to be, and capped so it never runs to the gutters — 62% of a wide
   panel, more of a narrow one, because a phone has less width to give away.

   Placed by initHeroCurtain(), which sets --mm-hero-mark to the middle of
   whatever is still showing as the section winds up. The 50% here is only
   what it looks like before the script runs. */
.hero__wordmark {
  position: absolute;
  left: 50%;
  top: var(--mm-hero-mark, 50%);
  transform: translate(-50%, -50%);
  width: min(62%, 760px);
  height: auto;
  pointer-events: none;
  /* Above .hero__scrim, which comes after it in the markup — the shade is
     for the film, and over the mark it would turn the white grey. */
  z-index: 1;
}

/* ---------- 6. Epitome ---------- */
/* Text band on top, one full-bleed image row beneath it. Height comes from
   the content — the row animates on its own, so there is nothing to hold
   the page for and no reason to pin the section to the viewport.

   Black ground like the rest of the page: the section reads as a continuous
   run from the hero above into MODES below, with the image row as the only
   edge, rather than as a light block cut into the middle. */
.epitome {
  position: relative;
  background: var(--mm-black);
  color: var(--mm-white);
}

.epitome__panel {
  overflow: hidden;
}

/* --- text band (copy left, heading right) --- */
/* Shared by the epitome, MODES and logo wall bands. They are the same
   treatment, so they are declared together rather than three times — a
   change to one is meant to land on all of them. Only the padding differs,
   set per section below.

   The heading comes first in the DOM and is placed into column 2, so the
   reading order stays heading-then-copy while the eye reads copy-then-
   heading across the band. Both blocks hang from the top: the first line of
   copy starts level with the cap line of the heading. */
.epitome__text,
.modes__head,
.logos__head {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: clamp(1.5rem, 5vw, 5rem);
  align-items: start;
}

.epitome__title,
.modes__title,
.logos__title {
  grid-area: 1 / 2;
  text-align: right;
  margin: 0;
}

/* The copy starts on the heading's cap line, not level with its box.
   align-items: start only lines the two boxes up; what sits between a box
   top and the first visible glyph is half-leading plus the gap from the
   font's ascender down to its cap height, and those differ — the heading
   is a display serif at line-height 1.06, the copy is Inter at 1.7. So the
   copy is nudged by the difference.

   Both terms are proportional to their own font size, and the two sizes
   run on different clamps, so the correction is not a constant: it swings
   from about 1px at 900px wide to about 3px at full size. Hence the calc
   rather than a fixed value. The coefficients are measured font metrics —
   .216em for Playfair at 1.06, .478em for Inter at 1.7 — and must be
   re-measured if either face or line-height changes, Ogg included. */
.epitome__copy,
.modes__copy,
.logos__copy {
  grid-area: 1 / 1;
  font-size: var(--mm-band-copy-size);
  line-height: 1.7;
  color: var(--mm-muted);
  max-width: 44ch;
  margin: calc(.216 * var(--mm-title-size) - .478 * var(--mm-band-copy-size)) 0 0;
}

.epitome__text {
  padding: var(--mm-band-space) var(--mm-gut);
}

/* --- gallery (full bleed, below the text) --- */
/* A single row travelling rightwards, evenly spaced horizontally and
   scattered vertically. No tilt, no overlap, no size variation — tiles all
   share a height, and only the drop down the band and the photo's own
   width change from one to the next. The loop runs straight through: no
   pause on hover. position: relative anchors the cursor label. */
/* The scatter is gone, so the band is simply the tile plus its padding. */
.epitome__gallery {
  position: relative;
  padding-block: clamp(1.25rem, 2.5vw, 2.5rem);
  overflow: hidden;
}

/* max-content, not a percentage: the row must be as wide as its twenty-six-
   plus-twenty-six tiles so that 50% of it is exactly one copy. Spacing via
   margin-right rather than `gap` — `gap` puts n-1 gaps in the row, leaving
   half a gap of drift at the seam, while margin-right gives n and lands
   exactly.

   center, not flex-start: every tile shares a height, so they sit on one
   line. This used to be flex-start to leave room for a vertical scatter
   that no longer exists.

   The duration here is a fallback. initEpitomeRow() measures the row and
   retimes it to a fixed speed, because the tiles now keep their own
   proportions: an upright photograph and a wide one are different distances
   to travel, so the figure cannot be written down and left. It was wrong
   three times when it was — 102s, then 157s, then 272s, each right for one
   arrangement and silently wrong for the next. */
.gallery__row {
  display: flex;
  align-items: center;
  width: max-content;
  will-change: transform;
  animation: mm-marquee-right 272s linear infinite;
}

/* No gap: the tiles butt against each other, which is what the about page's
   row does. That row looks the way it does partly by accident — it asks for
   var(--mm-tile-gap), which is not in scope there, so the declaration is
   dropped and the margin lands at nought. Here it is deliberate.

   It also makes the seam exact for free. The old margin-right existed to
   put n gaps in the row rather than the n-1 that `gap` gives, so the loop
   would not drift half a gap at the join; with no gap at all there is
   nothing to drift. */
.gallery__tile {
  display: block;
  flex: 0 0 auto;
  margin: 0;
  overflow: hidden;
}


/* Starts a copy back and runs to zero, so the row travels rightwards. */
@keyframes mm-marquee-right {
  from { transform: translate3d(-50%, 0, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

/* One height for every tile, and its own width. The photographs keep the
   shape they were shot in — assets/epitome/ is generated at a fixed height,
   not cropped to a ratio — so the row runs upright, wide, upright, wide.
   aspect-ratio and cover used to hold every tile at 3:2, which is what made
   it a steady band of identical rectangles.

   The resting scale is 1.03, not 1. Blur samples beyond the image edge, and
   the tile clips at that edge, so a blurred tile at scale 1 fades out around
   its border instead of staying solid. Oversizing pushes the soft edge
   outside the clip. It has to sit on the resting state rather than only on
   :hover — animating the scale would make the tile lurch. */
.gallery__row img {
  display: block;
  height: clamp(190px, 22vw, 300px);
  width: auto;
  transform: scale(1.03);
  transition: filter .5s var(--mm-ease);
}

/* Hovered tile recedes so the label reads over it — the blur carries the
   focus shift, the dim gives the type something to sit on.

   Only where there is a real pointer to hover with. A phone has none, but
   it still applies :hover to whatever was last tapped and leaves it there,
   so a tap on the row left a photograph blurred and dimmed until the next
   tap somewhere else — the effect with none of the label it exists to
   serve, since the label is already switched off without a cursor. Gated on
   the device rather than the width, so a narrow desktop window keeps it. */
@media (hover: hover) {
  .gallery__tile:hover img {
    filter: blur(9px) brightness(.55);
  }
}

/* --- cursor label --- */
/* Translated from JS on every frame, so no transition on transform — the
   easing that makes it trail the cursor is done there. pointer-events:none
   keeps it from stealing the hover from the tile underneath it. */
.gallery__label {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  pointer-events: none;
  font-family: var(--mm-mono);
  font-size: .72rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--mm-white);
  opacity: 0;
  transition: opacity .3s var(--mm-ease);
}
.gallery__label.is-visible { opacity: 1; }

/* ---------- 7. Modes ---------- */
/* Bottom is zero: the run from the rail down to the logo wall's heading is
   that section's own top space, and doubling up here would make the one gap
   on the page that is twice every other. */
.modes { padding-block: var(--mm-band-space) 0; }

/* Grid, alignment and type come from the shared band rules above; the
   gutters and the drop to the rail are this section's own. */
.modes__head {
  padding-inline: var(--mm-gut);
  margin-bottom: var(--mm-band-space);
}

.modes__rail {
  /* Shared by .mode.is-open and .mode__card so the two stay in step. */
  --mm-mode-grow: 2.1;
  /* Stacked only: how long the card takes to cross in and the label to
     leave. One value because they are one movement — see the responsive
     block. The exit is quicker: nobody waits to watch something go. */
  --mm-mode-slide: .55s;
  --mm-mode-exit:  .34s;
  display: flex;
  height: clamp(340px, 46vw, 560px);
  overflow: hidden;
}

.mode {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  cursor: pointer;
  transition: flex-grow var(--mm-slow) var(--mm-ease);
}
/* Even thirds at rest; the hovered panel takes a bit more room — enough
   for the copy card to breathe without swamping the other two. */
.mode.is-open { flex-grow: var(--mm-mode-grow); }

.mode__img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  /* Per panel, because where the faces sit is a property of the photograph
     and not of the layout. Set --mode-pos on the panel to move the crop. */
  object-position: var(--mode-pos, 50% 50%);
  filter: grayscale(.35) brightness(.72);
  transform: scale(1.06);
  transition: filter var(--mm-slow) var(--mm-ease),
              transform var(--mm-slow) var(--mm-ease);
}
.mode.is-open .mode__img {
  filter: grayscale(0) brightness(.92);
  transform: scale(1);
}

/* Vertical label, visible while collapsed */
/* Not selectable: the panel is a control now, and a long press that lifts a
   text selection out of it instead of opening it is a dead end. The card's
   own heading carries the same word for anyone copying text. */
.mode__label {
  position: absolute;
  top: 1.75rem; right: 1.5rem;
  user-select: none;
  writing-mode: vertical-rl;
  font-size: clamp(.95rem, 1.7vw, 1.5rem);
  font-weight: 400;
  letter-spacing: .16em;
  text-transform: uppercase;
  transition: opacity .35s var(--mm-ease);
}
.mode.is-open .mode__label { opacity: 0; }

/* Copy card, slides in while expanded. Fills the whole panel — the hovered
   section turns solid white rather than leaving a strip of photo beside
   the card. Its neighbours keep their images. */
.mode__card {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  /* Width of the panel once expanded, not the panel's current width. The
     card appears at full size on frame one and the still-narrow panel just
     clips it, so the copy is laid out at its final measure immediately.
     Sizing to the live panel meant the text wrapped tight, then re-wrapped
     as the panel grew — a visible jump when moving between panels.
     grow / (grow + the two resting panels), nudged up so rounding and the
     scrollbar can never leave a sliver uncovered. */
  width: calc(100vw * var(--mm-mode-grow) / (var(--mm-mode-grow) + 2) + 12px);
  padding: clamp(1.5rem, 2.6vw, 2.75rem);
  display: flex;
  flex-direction: column;
  /* Centred when the copy fits, and the copy is longer than it was: on the
     smallest phones it fits with nothing to spare, and anyone reading with
     a larger text size runs it over the edge. Plain `center` would then
     spill it out of the top and the bottom alike, cutting off the heading
     as well as the last line. `safe center` falls back to starting at the
     top once it overflows, and overflow-y lets the card scroll rather than
     hide what is left. Neither changes anything while the copy fits.
     The plain declaration first is for browsers that predate `safe`. */
  justify-content: center;
  justify-content: safe center;
  overflow-y: auto;
  background: var(--mm-white);
  color: var(--mm-black);
  opacity: 0;
  /* Closing only. Opening is instant — see below. */
  transition: opacity .3s var(--mm-ease);
}

/* The white lands immediately on hover, then the panel widens underneath
   it. Fading the card in meant watching the photo through it for half a
   second, which read as lag. Only the exit is animated. */
.mode.is-open .mode__card {
  opacity: 1;
  transition: none;
}

.mode__heading {
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  margin-bottom: .85rem;
}
.mode__copy {
  font-size: clamp(.8rem, .95vw, .95rem);
  line-height: 1.75;
  color: rgba(0, 0, 0, .72);
  max-width: 34ch;
}
/* A card can run to two paragraphs (Luxury does). The reset zeroes every
   paragraph's margin, so without this the second would butt straight
   against the first with no break between them. */
.mode__copy + .mode__copy { margin-top: .9em; }

/* ---------- 8. Logo wall ---------- */
.logos {
  position: relative;
  padding: var(--mm-band-space) var(--mm-gut);
}

/* No padding-inline of its own — the section already carries the gutter,
   and adding it here would inset the band past the marks below it. */
.logos__head {
  margin-bottom: var(--mm-band-space);
}

.logos__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem) clamp(1rem, 3vw, 3rem);
}

.logos__cell { display: grid; place-items: center; }
/* Nine cells are in the markup but only three across shows the ninth: nine
   makes a full 3 x 3 there, where eight left a gap in the last row. Four
   across and two across stay at eight, which they divide evenly.
   initLogoSwap() returns a hidden cell's logo to the rotation queue, so the
   partner in it still comes round on a desktop or a phone. */
.logos__cell:nth-child(9) { display: none; }

/* No hover state by design — a lift or a brightening would read as
   "clickable", and these are not links. The only response is the drift
   away from the cursor written by initLogoWall(). The transition is what
   makes it glide rather than snap. */
.logos__item {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  height: clamp(56px, 7vw, 92px);
  opacity: .82;
  will-change: transform;
  transition: transform .55s var(--mm-ease);
}

/* Absolutely positioned rather than height:100%. The item's implicit grid
   row is auto-sized, so a percentage height on the image would depend on a
   row that depends on the image — a cyclic reference Chrome resolves to
   auto, letting tall marks (Spotify, Lexus, Jordan) blow out the row. */
/* The swap animates the img, never the .logos__item — initLogoWall() writes
   the drift straight onto the item's transform every frame, so anything
   transforming the same element would be overwritten mid-jump. Two layers:
   the item drifts, the mark inside it jumps. */
.logos__item img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
  /* Breathing room inside every cell. Logos are trimmed to their own edges
     when the site builds (lib/trim.js), so without this they run right to
     the cell's edge — and on a phone, two across, neighbours ended up 22px
     apart. Top and bottom are a length, not a percentage, because a
     percentage padding is measured against the cell's width even
     vertically, and on a cell this wide and short it would eat most of
     the height. */
  padding: clamp(.4rem, .8vw, .7rem) 9%;
  transition: opacity .42s var(--mm-ease), transform .42s var(--mm-ease);
}

/* Out of the top, in from the bottom, so the wall reads as marks moving
   through the slots rather than crossfading in place. */
.logos__item img.is-leaving  { opacity: 0; transform: translateY(-42%); }
.logos__item img.is-entering { opacity: 0; transform: translateY(42%); }

/* Optical sizing for marks that sit tight to their own edges. A solid
   silhouette like the swoosh reads far heavier than a wordmark drawn to
   the same box, so it needs to be given less of the box.

   Padding rather than transform: the content box is what object-fit fits
   the mark into, so shrinking it scales the mark down, and transform is
   already spoken for by the jump. Percentage padding resolves against the
   slot's *width* on all four sides, which is what keeps the reduction even.

   This travels with the mark, not the slot — the rotation carries the img's
   classes across, so add the class in the markup beside the mark itself. */
/* Solid shapes (the Nike swoosh) carry more visual weight than a wordmark
   of the same size, so they sit further in. Has to stay larger than the
   inset every logo gets above — it replaces it rather than adding to it,
   and at its old 3.5% it would now have given them less room, not more. */
.logos__item img.is-tight { padding: clamp(.7rem, 1.8vw, 1.35rem) 15%; }

/* Logos supplied on a near-black plate rather than transparency.
   screen drops #000 out; the contrast bump first crushes the plate
   (Netflix ships on #101010) to true black so nothing shows through.
   Remove this class once a transparent file replaces the asset. */
.logos__item img.is-knockout {
  mix-blend-mode: screen;
  filter: contrast(1.3);
}

/* Stand-in for brands still awaiting a clean transparent file. */
.logos__wordmark {
  font-size: clamp(1.15rem, 2.4vw, 2rem);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--mm-white);
}

/* ---------- 9. Featured projects ---------- */
.features { display: block; }

.feature {
  position: relative;
  display: block;
  height: clamp(300px, 44vw, 620px);
  overflow: hidden;
}

.feature__img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 1.1s var(--mm-ease);
}
.feature:hover .feature__img { transform: scale(1.04); }

.feature::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,.6) 0%, transparent 42%);
  pointer-events: none;
}

.feature__meta {
  position: absolute;
  left: var(--mm-gut); bottom: 1.75rem;
  z-index: 1;
}
.feature__name {
  font-size: .78rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
}
.feature__loc {
  font-size: .7rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--mm-muted);
}

/* ---------- 10. All work ---------- */

/* Bottom is zero for the same reason as MODES: the CTA below carries its
   own top space. */
.allwork { padding-block: var(--mm-band-space) 0; }

.allwork__head {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: clamp(1rem, 4vw, 3rem);
  padding-inline: var(--mm-gut);
  color: var(--mm-white);
  text-decoration: none;
}
/* The chevron lifts on hover; the heading stays put. Moving the type would
   shift the whole band's baseline for the sake of a rollover. */
.allwork__head:hover .allwork__chevron { transform: translateY(-3px); }

.allwork__chevron {
  width: 26px;
  color: var(--mm-white);
  transition: transform .35s var(--mm-ease);
}
.allwork__chevron svg { width: 100%; height: auto; }

/* Open space for the trail. Was ~780px of dedicated black; the trail now
   runs across the whole field, so this only needs to be enough to read as
   a pause between the projects and the CTA — roughly a quarter of what it
   was. */
/* A grid edge to edge and full bleed. No gap and no radius: the photographs
   meet, which is what makes it read as one block rather than a set of cards.

   Four wide photographs, two by two, on anything wider than a phone: each
   is half the width of the screen, so the block is as large as it can be
   while still sitting inside one screen with its heading. Only landscape
   photographs are dealt into it — initWorkGrid() takes them from
   data-images-wide — so nobody's head is cropped off to fit a wide frame.
   The count and the column count are locked together; change one and
   change the other. */
.workgrid {
  /* The space between the heading and the grid, tighter than the band
     space the rest of the page uses, so the photographs get the height.
     Opened up by half from clamp(1.5rem, 3vw, 2.75rem), which sat the
     photographs too close under the heading. Still well inside the band
     space (clamp(4rem, 9vw, 8rem)). The grid's height subtracts this, so a
     larger lead shrinks the photographs a little rather than pushing them
     below the fold. A phone uses the band space here instead — see the
     560px rule below. */
  --workgrid-lead: clamp(2.25rem, 4.5vw, 4rem);

  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-template-rows: repeat(2, 1fr);
  gap: 0;
  margin-top: var(--workgrid-lead);

  /* The band has to sit inside one screen: the section's space above, the
     heading, the lead, and the grid. The grid takes what is left — the
     first term.

     The second term is two rows at three by two: each photograph is half
     the width, so its height is a third of it, and two rows are 66.67vw.
     min() takes the smaller, so on a normal desktop the screen height
     decides and the photographs come out a little wider than three by two
     — 640 by 296 at 1280 by 800 — and cover crops them into it.

     svh rather than vh so a phone's collapsing address bar does not make
     the promise false at the moment the page loads. */
  height: min(
    calc(100svh - var(--mm-workgrid-head) - var(--mm-band-space) - var(--workgrid-lead)),
    66.67vw
  );
}

/* On a phone five across made 75px squares — a strip of thumbnails. Six
   instead, two across and three down, so each is half the screen wide and
   the grid fills the screen under the heading rather than a third of it.

   Same budget as above, only the second term changes: two across makes each
   square half the width, so three rows of them are 150vw. On a 375 by 812
   phone that is squares of 187 and a grid of 562 — with the heading and
   the fixed bar, the whole screen. On a shorter phone the rows flatten a
   little rather than the grid running past the bottom.

   Six is what initWorkGrid() builds at this width; the two have to agree. */
@media (max-width: 560px) {
  .workgrid {
    margin-top: var(--mm-band-space);
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: repeat(3, 1fr);
    height: min(
      calc(100svh - var(--mm-workgrid-head) - 2 * var(--mm-band-space)),
      150vw
    );
  }
}

/* No hover state anywhere in here, on purpose. The block is one link, so
   there is nothing to distinguish cell by cell, and a lift or a dim would
   suggest each square went somewhere of its own. */
/* No aspect-ratio: the rows are 1fr of a height the grid is given, so the
   cell takes its height from the band's budget rather than from its own
   width. min-height: 0 lets a row actually shrink to it — a grid item's
   automatic minimum is its content, and the image inside would otherwise
   hold the row open. */
.workgrid__cell {
  position: relative;
  overflow: hidden;
  min-height: 0;
}

/* Cross-faded rather than replaced outright: the outgoing photograph is
   still there under the incoming one for the length of the fade, so a cell
   never flashes the page's black between two images.

   The scale is what makes it pop rather than dissolve — it arrives a touch
   large and settles. */
.workgrid__slide {
  position: absolute;
  inset: 0;
  transition: opacity .48s var(--mm-ease), transform .48s var(--mm-ease);
}
.workgrid__slide.is-leaving  { opacity: 0; transform: scale(1.04); }
.workgrid__slide.is-entering { opacity: 0; transform: scale(1.06); }

.workgrid__slide img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* What the section is without JavaScript: a plain grid of the same
   photographs, no turnover. Visible by default and removed by
   initWorkGrid() once the real grid is standing, rather than hidden by
   default — hidden by default it was unreachable, which is what it had
   quietly become. It sits below the fold and its images are lazy, so
   nothing is fetched in the moment before it goes. */
.allwork__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 2px;
  margin-top: clamp(1rem, 3vw, 2rem);
}
.allwork__grid img {
  width: 100%;
  aspect-ratio: 1 / 1;   /* square, like the cluster tiles */
  object-fit: cover;
}

/* ---------- 9b. About ---------- */

/* Bottom is zero, as on MODES: the run down to the All Work heading is that
   section's own top space, and doubling up here made the one gap on the
   site that is twice every other. */
.about {
  padding: var(--mm-head-clear) var(--mm-gut) 0;
}

/* Copy left, heading and photographs right — and the copy column is the
   narrower of the two, so the collage takes the greater part of the screen.
   The copy is capped at 44ch anyway, so the width it gives up is width it
   was not using; all the split really decides is how big the photographs
   get. */
.about__body {
  display: grid;
  grid-template-columns: 0.8fr 1.2fr;
  column-gap: clamp(2rem, 6vw, 6rem);
  align-items: start;
}

/* Heading alone on the first row; copy and photographs share the second.
   That row is what starts the first line of copy exactly level with the top
   of the first photograph — they are neighbours in the same row rather than
   two blocks that happen to begin near each other. Placement is by
   grid-area, so the heading can still come first in the DOM. */
/* No cap-line nudge here, unlike the bands on the home page. The copy is
   lining up with the top edge of a photograph now, not with the first glyph
   of a heading, and an edge has no half-leading to correct for. */
.about__copy    { grid-area: 2 / 1; }
.about__collage { grid-area: 2 / 2; }

/* The hold. .about__hold is pure scroll runway with nothing drawn in it, and
   .about__body is pinned across it: the copy stays on screen and the collage
   runs its photographs through beside it, and the section lets go just as
   All Work arrives.

   The runway has to be a sibling inside the section rather than padding on
   it. A sticky box is bounded by its containing block's CONTENT box, so a
   runway made of padding leaves it nowhere to travel and it never sticks at
   all.

   Both halves pin together. Pinning only the copy would leave the collage
   sliding up out from under it, which is the opposite of the intent: the
   photographs are meant to move inside their windows, not the windows.

   is-held is set by initAboutScroll(), so a reduced-motion or no-JS visitor
   gets neither the runway nor the pin — they get every photograph at
   natural height, which is the honest fallback for a section whose whole
   point is the movement. */
.about__hold {
  height: 0;
}
.about.is-held .about__hold {
  height: var(--mm-about-hold);
}
.about.is-held .about__body {
  position: sticky;
  top: var(--mm-head-clear);
}

/* Ranged right like every other section title. Its bottom margin is the only
   thing setting the air between the heading and the row under it, and it
   applies to the copy and the photographs alike — they start together. */
.about__title {
  grid-area: 1 / 2;
  text-align: right;
  margin: 0 0 clamp(2.5rem, 6vw, 4.5rem);
}

/* Two independent stacks rather than a six-cell grid. Every photograph keeps
   the shape it was shot in — portraits stay upright, landscapes stay wide —
   which means neighbours are never the same height, and a grid row would
   have forced the shorter of each pair to leave a hole under it. Stacks just
   let each column run at its own pace.

   One width for everything, so nothing is cropped and nothing is out of
   scale. The scatter is the mix of shapes plus the right column starting
   lower; there is no per-tile nudging left to keep in sync. */
.about__collage {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(1rem, 2.6vw, 2.75rem);
  align-items: start;
}

/* Each column is its own window. initAboutScroll() sets the height to the
   bottom edge of the second photograph, so the collage stops level with the
   deck and the workout store instead of running on past the copy beside it
   and leaving a hole above All Work. Until it does, the column is its own
   height and every photograph shows — which is the reduced-motion state
   and the no-JS state both. */
.about__col {
  overflow: hidden;
}

/* The strip that actually moves. The gap lives here rather than on the
   column so the clone the script appends inherits it, which is what makes
   the loop seamless. */
.about__reel {
  display: flex;
  flex-direction: column;
  gap: clamp(1rem, 2.6vw, 2.75rem);
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .about__reel { transform: none !important; }
}

/* Bare photographs, not prints. The polaroid treatment belongs to the
   cluster further down the page; using it twice would flatten the
   difference between the two — and it is square, which is why these are
   not. */
.about-tile {
  margin: 0;
}
.about-tile img {
  display: block;
  width: 100%;
  height: auto;
}

/* The running row between the paragraphs, on phones only — built by
   initAboutReel(), which is also where its speed is set. Wide, it is never
   in the page at all. */
.about-reel { display: none; }

.about__copy p {
  margin: 0 0 1.4em;
  max-width: 44ch;
  font-size: var(--mm-band-copy-size);
  line-height: 1.7;
  color: var(--mm-muted);
}
.about__copy p:last-child { margin-bottom: 0; }

/* ---------- 10a. Past work ---------- */

/* Sits under the fixed header, so it carries that height as padding rather
   than starting at the top of the viewport and disappearing behind it. */

/* Two columns; a wide card takes both. Hairline gaps rather than none, so
   two photographs of similar tone still read as two. */
/* The cards run three regular, one wide, and again. That rhythm is written
   into the markup because it is what reads going down a phone, where every
   card is a row of its own.

   Two columns cannot tile it: three regular cards is a row and a half, so
   the wide one that follows has nowhere to sit and drops to the next row,
   leaving the odd cell empty. `dense` lets the card after it fall back into
   that gap — the order shifts, but only on the wide layout, and only enough
   to close the hole. */
.work-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-flow: row dense;
  gap: 2px;

  /* Clears the fixed header. The filter bar used to carry this, and taking
     the bar out left the first row of photographs running underneath the
     nav — the mosaic is the first thing on this page now, so the clearance
     belongs to it. */
  padding-top: var(--mm-head-clear);
}

.work-card {
  position: relative;
  display: block;
  overflow: hidden;
  aspect-ratio: 4 / 3;
  color: var(--mm-white);
  text-decoration: none;
}
.work-card.is-wide {
  grid-column: 1 / -1;
  aspect-ratio: 5 / 2;
}

.work-card__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 1.1s var(--mm-ease);
}
.work-card:hover .work-card__img { transform: scale(1.04); }

/* Gradient rather than a flat panel: the caption needs a ground on photos
   that happen to be pale down there, without boxing it on the ones that
   are not. */
.work-card__meta {
  position: absolute;
  left: var(--mm-gut);
  right: var(--mm-gut);
  bottom: clamp(1rem, 2.5vw, 1.75rem);
}
.work-card::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 42%;
  pointer-events: none;
  background: linear-gradient(to top, rgba(0, 0, 0, .6), transparent);
}
.work-card__meta { z-index: 1; }

.work-card__name {
  margin: 0;
  font-size: .78rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
}
.work-card__loc {
  margin: .35rem 0 0;
  font-size: .7rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--mm-muted);
}

/* A grid cell like the cards, so it reflows with them when the filter runs
   rather than sitting outside the mosaic. */
.work-note {
  /* The whole last row, so it always comes after the final project. As a
     half-width cell the grid's dense packing pulled it up into any gap
     left before a wide tile — it was found sitting above the last two
     projects. A full-width cell can only ever land at the end. */
  grid-column: 1 / -1;
  align-self: start;
  margin: 0;
  padding: clamp(1.5rem, 4vw, 3rem);
  /* Wider than a reading column: this is one closing sentence at the end of
     a mosaic, not a paragraph to settle into, and at 46ch it was breaking
     into far more lines than the space around it needed. */
  max-width: 64ch;
  font-size: var(--mm-band-copy-size);
  line-height: 1.7;
  color: var(--mm-muted);
}

/* ---------- 10b. Project page ---------- */

/* Full-bleed carousel, in two shapes.

   On a phone the slides are stacked and cross faded — one photograph at a
   time, cropped to fill. At that width a wide image is already broader than
   the screen, so there is no room for an edge to show and nothing to gain
   from a strip.

   Wider than that they become a strip at true proportions: one height for
   all of them and each as wide as its own shape makes it, so an upright
   photograph is narrow and a wide one is wide. Showing a slide means
   bringing it to the middle rather than fading it up, which leaves the
   edges of its neighbours in view on both sides — the point of the whole
   arrangement. Nothing is cropped: the box is the image's own ratio.

   This used to say a strip was not worth laying every image out at once.
   Four images is not a cost worth a worse picture, and the epitome row on
   the home page lays out fifty-two. */
.phero {
  position: relative;
  /* On a phone this is one height for every step, set by initProjectHero()
     from the tallest photograph in the set — so an upright one stands at
     its own full shape, and the two wide ones stack inside the same height
     rather than each being a thin band. The fallback is what the strip
     uses. Still transitioned: the height changes when the screen turns. */
  height: var(--mm-phero-h, clamp(420px, 78svh, 900px));
  transition: height .6s var(--mm-ease);
  overflow: hidden;
  background: var(--mm-black);
}

.phero__frame { position: absolute; inset: 0; }

.phero__slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity .7s var(--mm-ease);
}
.phero__slide.is-current { opacity: 1; }

/* A pair of wide photographs sharing the frame, one over the other, so the
   two of them together stand as tall as a single upright one does. Half
   each, touching in the middle, and the height comes from the offsets
   rather than from a percentage — which is what height: auto is for here.

   Half of the frame each, stated as a percentage rather than left to auto.
   An absolutely positioned image with height: auto takes its intrinsic
   height and ignores the offsets it was given, which left each one at its
   own 3:2 height and a band of black between them.

   They are cropped a little at the sides as a result: half a frame shaped
   like an upright photograph is wider than it is tall, but not as wide as
   three by two. That is the cost of the two of them measuring the same as
   one upright, which is the point of the arrangement.

   The 50% here is the fallback. initProjectHero() sets each one's share
   inline from the two photographs' own shapes (pairSplit()), and sizes the
   frame so a pair fits at its true proportions when the set has no upright
   photograph to set the height (phoneHeight()). */
.phero__slide.is-pair-top,
.phero__slide.is-pair-bottom { opacity: 1; height: 50%; }
.phero__slide.is-pair-top    { top: 0; bottom: auto; }
.phero__slide.is-pair-bottom { top: 50%; bottom: auto; }

/* The strip. Same breakpoint the rest of the sheet turns at.

   The frame stops being a full-width box and becomes exactly as wide as
   what is in it, anchored to the left edge and moved by initProjectHero().
   right: auto matters — inset: 0 above pins both sides, and a box pinned
   both sides cannot size to its contents.

   Every slide keeps its height and takes its width from its own ratio,
   which is what width: auto on an image with a definite height means. The
   box therefore already is the image's shape, so object-fit has nothing
   left to decide. */
@media (min-width: 900px) {
  .phero__frame {
    right: auto;
    display: flex;
    align-items: stretch;
    width: max-content;
    transition: transform .85s var(--mm-ease);
  }

  .phero__slide {
    position: static;
    inset: auto;
    flex: 0 0 auto;
    width: auto;
    height: 100%;
    object-fit: fill;
    opacity: 1;
    transition: none;
  }
}

/* Darkens top and bottom only, where the nav and the caption sit — the
   middle of the photograph is left alone. */
.phero__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom, rgba(0,0,0,.5) 0%, transparent 22%),
    linear-gradient(to top,    rgba(0,0,0,.55) 0%, transparent 30%);
}

.phero__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: clamp(40px, 4vw, 56px);
  height: clamp(40px, 4vw, 56px);
  display: grid;
  place-items: center;
  padding: 0;
  color: var(--mm-white);
  background: none;
  border: 0;
  cursor: pointer;
  opacity: .8;
  transition: opacity .3s var(--mm-ease), transform .3s var(--mm-ease);
}
.phero__arrow:hover { opacity: 1; }
.phero__arrow.is-prev { left: clamp(.5rem, 1.5vw, 1.5rem); }
.phero__arrow.is-next { right: clamp(.5rem, 1.5vw, 1.5rem); }
.phero__arrow.is-prev:hover { transform: translateY(-50%) translateX(-3px); }
.phero__arrow.is-next:hover { transform: translateY(-50%) translateX(3px); }
.phero__arrow svg { width: 60%; height: 60%; }

/* Name, then place and mode together on the line beneath it. The mode used
   to be its own box pinned to the far corner; it now reads as part of the
   caption, which is the same thing every project card says everywhere else
   on the site. */
.phero__meta {
  position: absolute;
  left: var(--mm-gut);
  bottom: clamp(1.5rem, 4vw, 3rem);
  z-index: 2;
  margin: 0;
}

.phero__name {
  font-size: clamp(.85rem, 1.2vw, 1.05rem);
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  margin: 0;
}
.phero__loc {
  margin: .4rem 0 0;
  font-size: .74rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--mm-muted);
}

/* The mode, on the end of the place, wherever a project is captioned — the
   project hero, the work cards, the featured blocks. The rule is here once
   rather than three times because the three captions say the same thing and
   should not be able to drift apart.

   The bar is generated content so that it is a mark on the page rather than
   a word in the caption. There is a real space before the span in the
   markup, though, so the two values are still separated for anything
   reading the text rather than looking at it — without it the caption is
   announced as "Los AngelesSocial". The left margin is the smaller of the
   two to allow for that space. */
.meta-mode::before {
  content: "|";
  margin: 0 .5em 0 .3em;
  opacity: .5;
}

/* --- sizzle reel --- */

/* Sits between the photography and the write-up. Held in rather than run
   full-bleed: the carousel above it already reaches both edges, and a
   second full-width picture directly under it would read as one continuous
   block rather than two things. The black margin is what separates them. */
/* Bottom is zero, as on MODES and All Work: the run down to the write-up is
   that section's own top space. Carrying it on both sides stacked two
   measures into one gap and left the reel marooned above the text. */
.pvideo {
  padding: var(--mm-band-space) var(--mm-gut) 0;
}

.pvideo__frame {
  position: relative;
  max-width: 1180px;
  margin-inline: auto;
  aspect-ratio: 16 / 9;
  background: #000;
  overflow: hidden;
}

/* contain, not cover: a sizzle reel is cut to its own frame and cropping it
   to the box would lose the edges of shots someone composed. Anything the
   ratio does not fill is black, which is the page anyway. */
.pvideo__frame video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
}

/* The button is the whole frame rather than the ring inside it, so the reel
   starts wherever it is clicked. The ring is only what marks the middle. */
.pvideo__play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgba(0, 0, 0, .3);
  color: var(--mm-white);
  cursor: pointer;
  transition: opacity .45s var(--mm-ease), background .45s var(--mm-ease);
}
.pvideo__play:hover { background: rgba(0, 0, 0, .16); }

/* Out of the way from the moment the reel starts until it ends. Not while
   it is merely playing: the overlay covers the whole frame, so bringing it
   back on pause put it straight over the control bar — and scrubbing pauses
   in most browsers, so the bar disappeared under the hand that was dragging
   it. pointer-events matters as much as opacity here; an invisible overlay
   still swallows the clicks meant for the controls. */
.pvideo.is-started .pvideo__play {
  opacity: 0;
  pointer-events: none;
}

.pvideo__ring {
  display: grid;
  place-items: center;
  width: clamp(58px, 6.5vw, 92px);
  aspect-ratio: 1;
  border: 1px solid rgba(255, 255, 255, .85);
  border-radius: 50%;
  background: rgba(0, 0, 0, .2);
  transition: transform .45s var(--mm-ease), background .45s var(--mm-ease);
}
.pvideo__play:hover .pvideo__ring {
  transform: scale(1.07);
  background: rgba(255, 255, 255, .12);
}

/* Nudged right by a little under a tenth of its width: a triangle centred
   on its bounding box reads as sitting left of centre in a circle, because
   its mass is all down the flat edge. */
.pvideo__ring svg {
  width: 32%;
  margin-left: 7%;
}

/* Full screen. The frame, not the video, is what goes full screen (so the
   close button can sit over it — see initProjectVideo()); the browser
   stretches it to the screen, and these drop the inline frame's own shape
   so the film is letterboxed to the screen instead of to 16:9. */
.pvideo.is-full .pvideo__frame {
  max-width: none;
  aspect-ratio: auto;
}
/* Fills the screen when the film and the screen are nearly the same shape,
   rather than leaving black bands — set by fit() in initProjectVideo(). */
.pvideo.is-fill .pvideo__frame video { object-fit: cover; }

/* The way out of full screen: top right, where people look for it, and
   always visible — the native control bar hides itself when the mouse
   stops, which is how its own exit icon went unnoticed.

   A bare X, no circle (the client's call, 2026-10-22). The mark is small,
   but the button around it stays 44px so it is still an easy target. With
   no dark wash behind it, a soft shadow keeps the white lines readable over
   bright footage. */
.pvideo__close {
  position: absolute;
  top: clamp(.75rem, 2vw, 1.5rem);
  right: clamp(.75rem, 2vw, 1.5rem);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 44px;
  aspect-ratio: 1;
  background: none;
  color: var(--mm-white);
  cursor: pointer;
  filter: drop-shadow(0 0 3px rgba(0, 0, 0, .65));
  transition: opacity .3s var(--mm-ease), transform .3s var(--mm-ease);
}
.pvideo__close:hover { opacity: .75; transform: scale(1.08); }
.pvideo__close:focus-visible { outline: 2px solid var(--mm-white); outline-offset: 2px; }
.pvideo__close[hidden] { display: none; }
.pvideo__close svg { width: 22px; }

/* --- write-up --- */
/* Bottom is zero for the same reason it is on .pvideo above: All Work
   follows on every project page and brings its own top space. */
.pintro {
  padding: var(--mm-band-space) var(--mm-gut) 0;
}
.pintro__copy {
  margin: 0;
  max-width: 62ch;
  font-size: var(--mm-band-copy-size);
  line-height: 1.7;
  color: var(--mm-muted);
}
/* A paragraph break in the Studio is a new <p>, and the reset at the top
   of the sheet takes every paragraph's margin away — so two paragraphs ran
   together as one block. The same gap the About page uses. */
.pintro__copy p + p { margin-top: 1.4em; }

/* The way on to the next project. Sits in the space under the write-up
   that was empty, at the same left edge as the copy above it.

   inline-flex, not flex: the target should be the tile, the words and the
   arrow, not the whole width of the page. A link that reaches the far side
   of a wide screen is a trip hazard for anyone dragging to select text. */
.pnext {
  display: inline-flex;
  align-items: center;
  gap: clamp(.9rem, 2vw, 1.4rem);
  margin-top: var(--mm-band-space);
  color: var(--mm-muted);
  text-decoration: none;
  transition: color .3s var(--mm-ease);
}

/* Square and small. It says which project is next without being a second
   photograph on a page that is already made of them — cropped square from
   that project's own lead image. */
.pnext__tile {
  display: block;
  width: clamp(64px, 8vw, 92px);
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
}

/* ---------- more projects (Fandom page) ----------
   Built from the site's existing pieces rather than a look of its own: the
   serif section title every band uses, sitting where the Projects title
   sits, then the homepage's featured cards — photograph, darkening, the
   project in tracked capitals and place and mode beneath — three to a row
   and edge to edge. Only the arrangement is new here; the cards take their
   type, label position, gradient and hover from .feature. */
.pmore { padding-top: var(--mm-band-space); }

.pmore__head {
  display: flex;
  justify-content: flex-end;
  padding-inline: var(--mm-gut);
  margin-bottom: var(--mm-band-space);
}
.pmore__head .section-title { margin: 0; }

.pmore__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0;
}
/* Three across rather than one, so shorter than a homepage card but still
   taller than wide at desktop sizes. */
.pmore__grid .feature { height: clamp(320px, 38vw, 600px); }

/* Holds the row and its arrows, so the arrows stay put while the row
   scrolls under them. The arrows are for the phone row only — above that
   all three projects are on the screen at once. */
.pmore__rail { position: relative; }
.pmore__arrow { display: none; }

/* A phone: the title moves to the left like every other band's, and the
   three projects become a row you swipe through rather than a stack — one
   tall card at a time with the edge of the next showing at the right, so
   it is clear there is more. It stops at the third; nothing loops.

   Each card is most of the screen wide and four by five, and snaps to the
   left gutter so a swipe always lands on a whole project. The row scrolls
   inside itself — the page never scrolls sideways — and the scrollbar is
   hidden, since the peeking card is the cue. */
@media (max-width: 560px) {
  .pmore__head { justify-content: flex-start; }
  .pmore__grid {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: 84%;
    /* No gap: the photographs meet, as they do everywhere else on the site.
       A gap showed as a black bar between them against the page. */
    gap: 0;
    padding-inline: var(--mm-gut);
    scroll-padding-inline: var(--mm-gut);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
  }
  .pmore__grid::-webkit-scrollbar { display: none; }
  .pmore__grid .feature {
    height: auto;
    aspect-ratio: 4 / 5;
    scroll-snap-align: start;
  }

  /* The project carousel's arrows: white chevrons straight on the
     photographs, one at each edge of the screen and centred on the card
     height. A soft shadow keeps them legible on a light picture, which the
     carousel's darker frames never needed. At either end of the row the
     arrow that has nowhere to go fades away. */
  .pmore__arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    width: 40px;
    height: 40px;
    display: grid;
    place-items: center;
    padding: 0;
    color: var(--mm-white);
    background: none;
    border: 0;
    cursor: pointer;
    opacity: .85;
    filter: drop-shadow(0 1px 3px rgba(0, 0, 0, .45));
    transition: opacity .3s var(--mm-ease);
  }
  .pmore__arrow.is-prev { left: .25rem; }
  .pmore__arrow.is-next { right: .25rem; }
  .pmore__arrow svg { width: 60%; height: 60%; }
  .pmore__arrow:disabled { opacity: 0; pointer-events: none; }
}

/* The picture element around the tile: a flex item that stays the tile's
   size on a phone and grows on a desktop. */
.pnext__pic { display: block; flex: 0 0 auto; }

/* The words and arrow as one piece, so they can travel together. Same gap
   inside as the row has outside, so the resting layout is unchanged. */
.pnext__slide {
  display: flex;
  align-items: center;
  gap: inherit;
  flex: none;
  position: relative;
  z-index: 1;
}

/* Peekaboo. The row starts as black with only "Next project" showing,
   sitting where the picture will be. Then the words slide right to their
   place, and the picture is uncovered left to right behind them.

   Two movements that have to agree. The black over the picture shrinks
   toward its right edge, so its left edge travels the picture's width; the
   words travel the picture's width plus the gap. Same duration, same
   easing, so the words' left edge is always just ahead of the black's —
   they read as dragging it off the picture. The distance is measured by
   initNextReveal() and handed over as --mm-peek-shift, since the picture is
   300-odd wide on a desktop and 64 on a phone.

   Both classes come from initNextReveal(). With no script, or reduced
   motion, there is no cover and nothing is moved.

   The transitions live on the revealed state only. Put on the waiting
   state, they animated the words into their starting position the moment
   the page loaded — a slide left before the slide right — which on a phone,
   where the row can already be on screen, was a visible double movement.
   This way the start is applied instantly and only the reveal moves. */
.pnext.is-peek .pnext__pic { position: relative; overflow: hidden; }
.pnext.is-peek .pnext__pic::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--mm-black);
  transform-origin: right center;
}
.pnext.is-peek .pnext__slide {
  transform: translateX(calc(-1 * var(--mm-peek-shift, 0px)));
}
.pnext.is-revealed .pnext__pic::after {
  transform: scaleX(0);
  transition: transform 1s var(--mm-ease) .1s;
}
.pnext.is-revealed .pnext__slide {
  transform: none;
  transition: transform 1s var(--mm-ease) .1s;
}

/* Desktop: the row runs exactly the width of the write-up above it. The
   write-up is capped at 62ch of its own type, and ch is measured from the
   font, so the row takes the same font size and the same 62ch — the words
   and arrow inside are sized in rem and px, so nothing in them changes. The
   picture takes whatever the words and arrow leave, at the tile's height,
   and cover crops the wide source into it. */
@media (min-width: 900px) {
  .pnext {
    display: flex;
    font-size: var(--mm-band-copy-size);
    max-width: 62ch;
  }
  .pnext__pic { flex: 1 1 auto; min-width: 0; }
  /* The words and the arrow keep their own size; only the picture gives.
     Left to shrink like the picture, the label was squeezed until it broke
     onto two lines. */
  .pnext__slide { flex: none; }
  .pnext__label { flex: none; white-space: nowrap; }
  .pnext__arrow { flex: none; }
  .pnext__pic .pnext__tile {
    width: 100%;
    height: clamp(64px, 8vw, 92px);
    aspect-ratio: auto;
  }
}

/* The label and, beneath it, the next project's name. */
.pnext__text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* The project the row leads to, set like place and mode on the featured
   cards — smaller, tracked capitals, muted — and held muted when the row is
   hovered, so "Next project" always reads first. */
.pnext__title {
  margin-top: .35rem;
  font-size: .7rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--mm-muted);
}

/* White, like a featured card's project name, so the next project's title
   beneath it reads as the lighter line — the same pairing as name over
   place and mode. */
.pnext__label {
  font-size: .72rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--mm-white);
}

.pnext__arrow {
  display: block;
  width: 22px;
  transition: transform .35s var(--mm-ease);
}
.pnext__arrow svg { display: block; width: 100%; height: auto; }

.pnext:hover { color: var(--mm-white); }
.pnext:hover .pnext__arrow { transform: translateX(5px); }

/* The arrow is the only thing that moves, and it is decoration. Held still
   for anyone who has asked for less of that. */
@media (prefers-reduced-motion: reduce) {
  .pnext__arrow { transition: none; }
  .pnext:hover .pnext__arrow { transform: none; }
}

/* ---------- 11. CTA typewriter ---------- */
/* The line sits level with the middle of the napkin rather than on its
   foot. No cap-line correction here, unlike the section bands: centring
   works off the line box, whose leading is split evenly above and below, so
   the type's optical middle already lands on the box's middle. */
.cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  padding: clamp(2.25rem, 7vw, 5.5rem) var(--mm-gut) clamp(2.5rem, 6vw, 4rem);
}

/* Monospace, per the Section.png design export — the typewriter line is
   set in a mono face, not the body sans. */
/* 300 is the lightest JetBrains Mono weight loaded — see the Google Fonts
   link in index.html before going lighter. The typed span used to restate
   the weight; it is a child of this line and inherits, so one declaration
   is easier to keep honest than two. */
.cta__line {
  font-family: var(--mm-mono);
  font-size: clamp(.98rem, 2.45vw, 2rem);
  font-weight: 300;
  letter-spacing: -.01em;
  line-height: 1.15;
}

.cta__phone {
  position: relative;      /* anchors the receiver layer */
  flex: 0 0 auto;
  width: clamp(110px, 13vw, 190px);
  transition: transform .5s var(--mm-ease);
}
/* The lift is the button's; the ring is the photograph's inside it. Two
   layers because a transform animation would overwrite a transform set by
   :hover — one element cannot both ease upward and shake at once. The old
   static -3deg lean went with it: the ring supplies the rotation now, and
   holding a tilt underneath it just fought the shake. */
.cta__phone:hover { transform: translateY(-6px); }

/* The photograph is shot on black, so it sits on the page without a cutout. */
.cta__phone img { display: block; width: 100%; height: auto; }

/* The base sets the button's height; the receiver lies exactly over it.
   Both are the full photograph's canvas, so inset: 0 is all the alignment
   they need. Only this layer moves — the dome, dial and fringe underneath
   stay put, which is the whole point of splitting the image.

   The pivot is the horn's own middle, so it shivers in the cradle rather
   than swinging one end like a lever. */
.phone__receiver {
  position: absolute;
  inset: 0;
  transform-origin: 50% 22%;
}

.cta__phone:hover .phone__receiver {
  animation: mm-ring 1.6s var(--mm-ease) infinite;
}

/* A burst, then a rest, the way a phone actually rings — the shake is done
   by 45% and the rest of the cycle is still. Rotation is anchored at the
   foot so the handset swings and the base stays put, which reads as the
   telephone shaking rather than the picture sliding about. Amplitude stays
   under 4 degrees: any more and the fringe starts to look like it is being
   waved. */
@keyframes mm-ring {
  0%             { transform: rotate(0deg)    translateX(0); }
  4%             { transform: rotate(-3.4deg) translateX(-2px); }
  10%            { transform: rotate(3deg)    translateX(2px); }
  16%            { transform: rotate(-2.6deg) translateX(-1.6px); }
  22%            { transform: rotate(2.1deg)  translateX(1.4px); }
  28%            { transform: rotate(-1.5deg) translateX(-1px); }
  34%            { transform: rotate(1deg)    translateX(.6px); }
  40%            { transform: rotate(-.5deg)  translateX(0); }
  45%, 100%      { transform: rotate(0deg)    translateX(0); }
}

/* --- napkin variant of the contact button --- */

/* The work and project pages hand you the number on a cocktail napkin
   instead. The background is already cut out of the file, so it lands on the
   black without a plate behind it — the same trick the telephone plays by
   having been shot on black. Everything else about it is the telephone's:
   same slot in the row, same [data-contact-trigger], same pop-out. */
.cta__napkin {
  position: relative;      /* anchors the kiss layer */
  flex: 0 0 auto;
  width: clamp(150px, 18vw, 260px);
  transition: transform .5s var(--mm-ease);
}
.cta__napkin img { display: block; width: 100%; height: auto; }

/* Picked up off the table: it lifts and straightens at the same time. */
.cta__napkin:hover,
.cta__napkin:focus-visible {
  transform: translateY(-8px) rotate(1.7deg);
}

/* The kiss is not on the napkin until you reach for it. It is a separate
   file lying exactly over the blank one — both are the same photograph's
   canvas, so inset: 0 is all the alignment they need — and it fades in on
   the hover the lift is already responding to.

   Fading rather than pressing on: the print is a photograph now, and
   scaling a photograph up from nothing reads as a sticker landing rather
   than as lipstick meeting paper. */
.napkin__kiss {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity .4s var(--mm-ease);
}
.cta__napkin:hover .napkin__kiss,
.cta__napkin:focus-visible .napkin__kiss { opacity: 1; }

/* --- project page variant --- */
/* Same row as the home page's — line left, napkin right. Only the
   breathing room differs: this one sits between the write-up and the
   footer, so it takes the band spacing the rest of the site uses. */
.cta--project { padding-block: var(--mm-band-space); }

.cta__caret {
  display: inline-block;
  width: .5ch;
  height: 1em;
  margin-left: .04em;
  background: var(--mm-white);
  vertical-align: -.12em;
  animation: mm-blink 1.05s steps(1, end) infinite;
}
@keyframes mm-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

/* ---------- 12. Footer ---------- */
.site-footer {
  padding: clamp(2rem, 5vw, 3.5rem) var(--mm-gut) 1.75rem;
  border-top: 1px solid var(--mm-faint);
}

.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: clamp(2rem, 6vw, 5rem);
}

/* The wordmark itself rather than the name set in letterspaced caps. Sized
   to 150px because the caps it replaces measured 135 wide — a drawn mark
   reads lighter than tracked-out type at the same width, so it takes a
   little more to hold the same weight in the corner. The bottom margin is
   unchanged, so the socials underneath do not move.

   White on alpha, so it sits on the footer's black without a plate. Same
   file as the hero, where it is drawn at 760 — this is the small end of its
   range, which is the direction that costs nothing. */
.site-footer__name {
  display: block;
  width: 150px;
  height: auto;
  margin-bottom: 1.5rem;
}

.socials { display: flex; gap: .85rem; }
.socials a {
  display: block;
  width: 20px;
  color: var(--mm-muted);
  transition: color .3s var(--mm-ease);
}
.socials a:hover { color: var(--mm-white); }
.socials svg { width: 100%; height: auto; }

.site-footer__menu {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(2rem, 6vw, 5.5rem);
}

/* One line of links, no headings over them. Work and Connect were headings
   on columns of one item and three — more label than list, and the
   links name themselves. Wraps rather than overflowing, so a narrow window
   breaks it into two lines instead of pushing the footer sideways. */
.menu-links {
  display: flex;
  flex-wrap: wrap;
  gap: .55rem clamp(1.5rem, 4vw, 3rem);
}
.menu-links a {
  font-size: .72rem;
  color: var(--mm-muted);
  transition: color .3s var(--mm-ease);
}
.menu-links a:hover { color: var(--mm-white); }

.site-footer__legal {
  margin-top: clamp(2rem, 6vw, 4rem);
  font-size: .62rem;
  letter-spacing: .1em;
  /* Not a token: this sits below --mm-muted and is the one footer colour
     the pop-out has to restate rather than inherit. */
  color: rgba(255, 255, 255, .35);
}

/* ---------- 12b. Contact pop-out ---------- */

/* A panel over the page, not a replacement for it. Whatever you were
   reading stays where it was on the left; only the backdrop comes between. */
.contact {
  position: fixed;
  inset: 0;
  z-index: 200;                /* over the fixed header, which is 100 */
}
.contact[hidden] { display: none; }

/* Dims enough to bring the panel forward and to say the page is not the
   live surface any more, but not so far that it stops being the page you
   came from. Doubles as the click-to-close target. */
.contact__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, .55);
  opacity: 0;
  transition: opacity .5s var(--mm-ease);
}

.contact__panel {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: clamp(340px, 42%, 620px);
  overflow: hidden;
  transform: translateX(100%);
  transition: transform .6s var(--mm-ease);
  will-change: transform;
}

/* `hidden` cannot be transitioned, so it only takes the panel out of the
   layout; is-open is what animates. initContact() sets hidden first and
   adds the class on the next frame, and on the way out drops the class and
   waits for the slide before hiding again. */
.contact.is-open .contact__backdrop { opacity: 1; }
.contact.is-open .contact__panel    { transform: translateX(0); }

/* Drifts rather than loops. The velvet is a photograph, not a tile, so a
   one-way pan would jump when it wrapped; running it out and back on
   `alternate` never reaches a seam. The inset is overscan — without it the
   drift would pull the picture's edge into view. */
/* The filter lives in the markup — see #mmSilk. Nothing here translates:
   the fabric ripples where it is rather than the picture panning across.

   Overscan, because displacement pulls pixels in from outside the element
   and there is nothing out there but transparency. Pushing the edges well
   past the panel keeps the torn margin off screen.

   The bottom runs on much further than the other sides, and the picture
   hangs from its top: the photograph's bottom-right corner is a deep shadow
   fold, and with the whole photograph fitted to a phone's tall panel that
   fold sat in the panel's corner and read as the picture running out. This
   keeps the panel's bottom edge on the lit folds above it, at every width.
   Only just: -40% and the top of the picture cleared it with room to spare
   but sat the folds too low in the panel, so this is halfway back. */
.contact__velvet {
  position: absolute;
  inset: -14% -14% -27%;
  background: url("../assets/contact/velvet.jpg") center 25% / cover no-repeat;
  filter: url("#mmSilk");
}

.contact__filter { position: absolute; width: 0; height: 0; }

/* Type on the velvet stays white — the light theme above it is for the page
   beside the panel, not for anything sitting on the fabric. */
.contact__inner {
  position: relative;          /* above the velvet */
  height: 100%;
  overflow-y: auto;
  padding: clamp(2.5rem, 5vw, 4.5rem) clamp(1.5rem, 3vw, 3rem);
  color: #fff;
}
.contact__close { color: #fff; }

.contact__lede {
  margin: 0 0 clamp(2rem, 5vw, 3.5rem);
  max-width: 34ch;
  font-size: clamp(.82rem, 1.05vw, 1rem);
  line-height: 1.5;
  letter-spacing: .02em;
  text-transform: uppercase;
}
/* The way round the form, set below it and quieter than the lede so it
   reads as the alternative rather than a second instruction. */
.contact__alt {
  margin: clamp(1.5rem, 3.5vw, 2.5rem) 0 0;
  max-width: 34ch;
  font-size: .72rem;
  line-height: 1.6;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .62);
}

.contact__lede a,
.contact__alt a {
  color: #fff;
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, .45);
}
.contact__lede a:hover,
.contact__alt a:hover { border-bottom-color: #fff; }

.contact__form { display: grid; gap: clamp(1.1rem, 2.4vw, 1.8rem); }

/* What the form says back. Empty and drawing nothing until there is
   something to report, so it takes no space in the layout while the form is
   being filled in. Set in the same mono treatment as the labels, since it
   belongs to the form rather than to the page. */
.contact__note:empty { display: none; }
.contact__note {
  margin: 0;
  max-width: 40ch;
  font-size: .72rem;
  line-height: 1.6;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .62);
}
/* The same soft red the invalid field borders use, rather than a second
   red of its own — see .contact__form.is-checked :invalid below. */
.contact__note.is-error { color: rgba(255, 130, 130, .9); }

/* Replaces the form once a message is away. Set like the lede rather than
   like a field: at that point it is the panel talking, not the form. */
.contact__sent {
  margin: 0;
  max-width: 34ch;
  font-size: clamp(1rem, 1.6vw, 1.25rem);
  line-height: 1.5;
}

.field { margin: 0; display: grid; gap: .55rem; }
.field label {
  font-size: .72rem;
  letter-spacing: .12em;
  text-transform: uppercase;
}
/* Dimmed so it marks the field without competing with the label. */
.field__req { color: rgba(255, 255, 255, .55); }

/* Only after a failed submit, so nothing is flagged red before the visitor
   has had a go at it. See the submit handler in initContact(). */
.contact__form.is-checked :invalid { border-color: rgba(255, 130, 130, .75); }

/* Smoked glass rather than a solid fill, so the velvet still moves behind
   the boxes instead of being blocked out by them. */
.field input,
.field textarea {
  width: 100%;
  padding: .85rem 1rem;
  font: inherit;
  font-size: .9rem;
  color: #fff;
  background: rgba(0, 0, 0, .38);
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: 2px;
  transition: background-color .3s var(--mm-ease), border-color .3s var(--mm-ease);
}
.field textarea { resize: vertical; min-height: 6.5rem; }
.field input::placeholder,
.field textarea::placeholder {
  color: rgba(255, 255, 255, .5);
  letter-spacing: .12em;
  text-transform: uppercase;
  font-size: .72rem;
}
.field input:focus,
.field textarea:focus {
  outline: none;
  background: rgba(0, 0, 0, .52);
  border-color: rgba(255, 255, 255, .5);
}

/* Ghost, so it reads as part of the smoked-glass set rather than a solid
   slab dropped on the fabric; it fills on hover. */
.contact__submit {
  justify-self: start;
  margin-top: clamp(.5rem, 1.5vw, 1rem);
  padding: .9rem 2.4rem;
  font-family: var(--mm-mono);
  font-size: .72rem;
  font-weight: 400;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: #fff;
  background: rgba(0, 0, 0, .38);
  border: 1px solid rgba(255, 255, 255, .34);
  border-radius: 2px;
  cursor: pointer;
  transition: background-color .3s var(--mm-ease),
              border-color .3s var(--mm-ease),
              color .3s var(--mm-ease);
}
.contact__submit:hover {
  color: #0b0b0b;
  background: #fff;
  border-color: #fff;
}

.contact__close {
  position: absolute;
  top: clamp(1rem, 2vw, 1.75rem);
  right: clamp(1rem, 2vw, 1.75rem);
  z-index: 2;
  width: 40px; height: 40px;
  display: grid;
  place-items: center;
  padding: 0;
  background: rgba(0, 0, 0, .35);
  border: 1px solid rgba(255, 255, 255, .18);
  border-radius: 50%;
  cursor: pointer;
  transition: background-color .3s var(--mm-ease);
}
.contact__close:hover { background: rgba(0, 0, 0, .6); }
.contact__close svg { width: 18px; height: 18px; }

/* ---------- 13. Responsive ---------- */
@media (max-width: 900px) {
  /* Too narrow to read a heading and a paragraph side by side: both bands
     stack to heading, then copy, ranged left. */
  .epitome__text,
  .modes__head,
  .logos__head {
    grid-template-columns: 1fr;
    row-gap: clamp(1.25rem, 4vw, 2rem);
  }
  /* Out of the second column, but still ranged right — see the phone
     breakpoint, which is where the alignment actually gives way. */
  .epitome__title,
  .modes__title,
  .logos__title {
    grid-area: auto;
  }
  .epitome__copy,
  .modes__copy,
  .logos__copy {
    grid-area: auto;
    max-width: 46ch;
    /* Stacked, so there is no heading beside it to share a cap line with —
       the row-gap sets the spacing instead. */
    margin-top: 0;
  }

  /* One card to a row, and the shapes alternate down the page: horizontal,
     upright, horizontal, upright. Two-up, a 4:3 card is a thumbnail by the
     time the page is this narrow — the name and the location sit over the
     picture, so the picture has to be big enough to carry them.

     The 5:2 band that carries the wide cards on the desktop grid does not
     survive here: a fifth of a phone's height is too little of a photograph
     to be worth stopping at. is-wide is overruled and every card takes one
     of the two shapes by its position instead — which is why the order in
     the markup matters, and why the two photographs shot in portrait sit on
     even slots.

     Selectors are two deep to outrank .work-card.is-wide, which sets the
     aspect a few hundred lines above this. */
  .work-grid .work-card {
    grid-column: 1 / -1;
    aspect-ratio: 4 / 3;
    width: auto;
    max-height: none;
  }

  /* The upright one. Capped rather than left to the ratio: 3:4 of a
     full-width card runs past the screen on the larger phones, and a card
     you cannot see the whole of stops reading as one card. The width has to
     be stated alongside the cap, or the ratio is held by taking the width
     in instead of the height and the card pulls away from the edges of a
     mosaic that runs edge to edge everywhere else. */
  .work-grid .work-card:nth-of-type(even) {
    aspect-ratio: 3 / 4;
    width: 100%;
    max-height: 76vh;
  }

  /* The closing note drops below the mosaic rather than sharing a row with
     the last card. It is the last cell in the flow, so on a wide grid it
     lands in whatever column is left over and reads as a caption to the
     photograph beside it; in half a phone's width it was breaking three
     words to a line. Full width, on its own row, it goes back to being what
     it is — the line the section ends on. */
  .work-note {
    grid-column: 1 / -1;
    max-width: 46ch;
  }

  /* Four across rather than six — six at this width gives cells barely
     100px wide. Twelve divides by four, so the last row stays full. */

  /* The about page stacks like every other section: heading, copy, then the
     photographs. Two columns on a phone left the copy in a gutter narrow
     enough to break three words to a line. */
  .about__body {
    grid-template-columns: 1fr;
    row-gap: clamp(1.5rem, 5vw, 2.5rem);
  }
  .about__title {
    grid-area: auto;
    margin-bottom: 0;
  }
  .about__copy    { grid-area: auto; }
  .about__collage { grid-area: auto; }

  /* The photographs run past between the paragraphs instead of sitting in a
     block under them. Read the opening, watch the work go by, read the
     rest — which is the shape the page already had wide, where the pictures
     move beside the copy rather than after it.

     Full bleed: the section carries the gutter, so the row backs out of it
     and runs edge to edge. Everything inside keeps a fixed HEIGHT and takes
     whatever width its own proportions ask for, which is what puts upright
     and wide photographs in the same row. */
  /* A grid item's min-width is auto, so it swells to whatever its contents
     ask for — and this row asks for its whole length, which was stretching
     the copy column to five thousand pixels and the paragraphs with it.
     Zero lets the column be the width the grid says it is. */
  .about__copy { min-width: 0; }

  .about-reel {
    display: block;
    overflow: hidden;
    /* Measured from the viewport rather than by backing out of the gutter:
       the gutter is a clamp, and the row has to be exactly the screen
       whatever that clamp lands on. */
    width: 100vw;
    margin: clamp(1.75rem, 6vw, 3rem) calc(50% - 50vw);
  }
  .about-reel__row {
    display: flex;
    align-items: center;
    width: max-content;
    will-change: transform;
    animation: mm-marquee-right 60s linear infinite;   /* retimed in JS */
  }
  /* Touching, like the home page's row. This asked for --mm-tile-gap, which
     is declared on .epitome__gallery and so out of scope here — the
     declaration was being dropped and the margin landing at nought anyway.
     Saying nought is the same result, honestly. */
  .about-reel figure {
    flex: 0 0 auto;
    margin: 0;
  }
  .about-reel img {
    display: block;
    height: clamp(190px, 52vw, 300px);
    width: auto;
  }

  /* The row carries the same thirteen photographs, so the collage would be
     saying it twice. */
  .about__collage { display: none; }

  /* No pin and no runway. The hold only makes sense while the copy sits
     beside the photographs — stacked, there is nothing to hold on screen,
     and the runway would be a screen of black to scroll past. The columns
     run at their natural height and every photograph is simply there. */
  .about.is-held { padding-bottom: 0; }
  .about.is-held .about__hold { height: 0; }
  .about.is-held .about__body { position: static; }
  /* Back to cover on a phone. contain was here because the wordmark was
     baked into the frame and a cover crop cut its first and last letters
     off; with the mark on its own layer there is nothing in the photograph
     left to protect, so it can fill the box like everywhere else. The
     upright crop is served by <picture>. */
  .hero__wordmark { width: min(78%, 420px); }


  /* Panels stack, and the photograph is the control: there is no hover here
     to open one with, so nothing is open until it is tapped. */
  .modes__rail { display: block; height: auto; }
  /* Edge to edge, with nothing between them. The hairline that used to
     separate the panels read as a gap in the page rather than as a join. */
  .mode { height: 62vw; min-height: 300px; }

  /* The narrowest phones wrap the longer mode copy onto enough lines that
     300px holds it only by letting it run into the card's padding — at
     320px Entertainment touched both edges. A little more height there and
     nowhere else: 360px and up keep the 300px floor they look right at. */
  @media (max-width: 359px) {
    .mode { min-height: 350px; }
  }

  /* Stacked, the panel is a wide letterbox rather than a tall column, so a
     centred crop takes a band out of the middle of the frame and the heads
     go with it. --mode-pos-stacked is where each photograph wants to be cut
     in that shape; it falls back to the panel's own value and then to a
     bias towards the top, which is where faces are in most of them. */
  .mode__img {
    filter: grayscale(0) brightness(.85);
    transform: none;
    object-position: var(--mode-pos-stacked, var(--mode-pos, 50% 12%));
  }

  /* The label is the only thing saying a panel has more behind it, so it
     holds until the card arrives to take its place — and it leaves the way
     the card comes, sliding off the right edge as the white crosses from
     the left. The two are one movement, so they share a duration. */
  .mode__label {
    opacity: 1;
    transition: transform var(--mm-mode-exit) var(--mm-ease-in),
                opacity .3s var(--mm-ease);
  }
  .mode.is-open .mode__label {
    opacity: 0;
    transform: translateX(calc(100% + 1.5rem));
  }

  /* Stacked: the card is an overlay on each panel again, not a full column,
     and it slides in rather than appearing. It waits off the left edge, and
     the panel's own overflow is what hides it there — no opacity involved,
     so the white arrives solid instead of ghosting over the photograph. */
  /* This state is also the exit, and an exit is not an entrance played
     backwards. --mm-ease is an ease-out: run the other way it crawls over
     the last stretch, and the card looked like it snagged on the left edge
     before finally going. Leaving gets an ease-in, and less time. */
  .mode__card {
    opacity: 1;
    width: min(330px, 78%);
    height: auto;
    transform: translateX(-100%);
    transition: transform var(--mm-mode-exit) var(--mm-ease-in);
  }
  /* Restated because the wide rule opens instantly, on purpose — there, the
     white lands first and the panel widens under it. Here nothing is
     widening, so there is nothing to cover the swap. */
  .mode.is-open .mode__card {
    transform: translateX(0);
    transition: transform var(--mm-mode-slide) var(--mm-ease);
  }

  .logos__grid { grid-template-columns: repeat(3, 1fr); }
  .logos__cell:nth-child(9) { display: grid; }


  /* Too narrow to sit beside the page — the panel takes the screen. The
     backdrop is still behind it and still closes on a tap, but there is
     nothing of the page left to see past it. */
  .contact__panel { width: 100%; }
}

@media (max-width: 560px) {
  /* Three across on a phone: at four the cells came out under 90px, which
     is too small to see what a photograph is of. Twelve divides by three,
     so the grid stays a full rectangle. */

  .logos__grid { grid-template-columns: repeat(2, 1fr); }
  .logos__cell:nth-child(9) { display: none; }
  /* Two across, each logo has a far wider share of the screen than on a
     desktop, so the inset that looks right there reads large here. A
     little more room on a phone only. Solid shapes move in further again,
     so they keep sitting smaller than the wordmarks. */
  .logos__item img          { padding: .75rem 14%; }
  .logos__item img.is-tight { padding: 1rem 21%; }
  /* The closing object comes down here — see initFooterObject(), which moves
     it rather than the markup carrying a second one. It sits after Connect
     as a third column, and takes what width is left rather than a size of
     its own, so the two link columns keep theirs. Much smaller than it is
     beside the closing line: there it is the thing you look at, here it is
     the thing you reach for on the way out. */
  /* Two tracks: the links take what they need, the object takes the rest
     and sits at the end of it. The links are a single column now, so they
     are narrow enough to share the row — which is what the object was
     moved off for when they were briefly a line.

     flex-basis is about the row above: the menu is a flex item of
     .site-footer__inner, and left to shrink it would size to its contents
     rather than to the line. */
  .site-footer__menu {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 2rem;
    align-items: center;
    flex-basis: 100%;
  }

  /* One column, ranged left, under each other — the same left edge as the
     wordmark and the socials above them, so the whole corner of the footer
     hangs off one line rather than three. */
  .menu-links {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: .7rem;
  }
  .site-footer__menu .cta__phone,
  .site-footer__menu .cta__napkin {
    width: clamp(100px, 30vw, 140px);
    justify-self: end;
    align-self: center;
    /* Back to source order. The rule that lifts it above the closing line in
       the CTA sets order: -1, and grid honours order just as flex does — it
       was landing in the first column, to the left of the links. */
    order: 0;
  }

  /* Only here do the headings give up ranging right, and it is the width
     that decides it rather than the stacking. Ranging right works while
     there is enough line for the eye to travel: a heading at one end and
     the page's own margin at the other still reads as placed. On a phone
     the line is short enough that a right-ranged heading and the copy
     under it look like two unrelated columns, so everything comes back to
     one edge. */
  .epitome__title,
  .modes__title,
  .logos__title,
  .about__title { text-align: left; }
  .allwork__head { justify-content: flex-start; }

  /* Only here does the line give up its row. Above this the object stays
     beside it and level with it, which is the whole composition — it used
     to break at 900px, which is a desktop window, and dropped the object
     onto the left margin under the line.

     Once it does break, the object centres under the line rather than
     hanging off one edge. The line is ranged left and reaches most of the
     way across, so anything tucked into a corner beneath it reads as having
     come loose rather than as having been placed. */
  .cta { flex-direction: column; align-items: center; gap: 2.5rem; }

  /* The telephone and the napkin go above the line here. Side by side they
     read together whichever way round they are; stacked, the object is the
     thing you reach for and the line is what it leaves you with, so the
     line reads better underneath it.

     order rather than column-reverse: the heading still comes first in the
     DOM, where it belongs for anything reading the page rather than
     looking at it. */
  .cta__phone,
  .cta__napkin {
    align-self: center;
    order: -1;
  }

  /* The line starts on the gutter, which is the same edge MAISON MADERA
     starts on in the footer directly below it — the two are the only text
     on this stretch of the page, and reading down, a line that begins
     anywhere else looks like a mistake.

     No fixed width any more. It was there to stop the line creeping
     sideways as it typed while it was centred; ranging left, the text grows
     rightwards from a fixed edge and nothing moves anyway. The object above
     it stays centred. */
  .cta__line {
    align-self: flex-start;
  }
}

/* The cluster runs on touch too. It used to be swapped for a plain grid
   here, on the grounds that its one interaction was a hover — but the pile
   is the picture, and the hover only ever nudged it. What is left without a
   cursor is still the thing worth showing. The grid stays as the fallback
   for reduced motion and for no JS, which is what .allwork__grid is now
   for. */
@media (hover: none) {
  /* No hover on touch, so the logo lift never fires — nothing to disable. */

  /* The kiss is already on the napkin. There is no cursor to arrive and
     press it on, so gating it behind hover would mean it was never seen at
     all here — and it is half of what the napkin is. */
  .napkin__kiss {
    opacity: 1;
    transform: none;
  }
}

/* ---------- 14. Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  /* Stop the marquee rather than letting it snap to its end frame. */
  .gallery__row,
  .about-reel__row { animation: none; transform: none; }


  /* The grid itself is legible standing still — initWorkGrid() simply does
     not rotate it — so it stays, and the no-JS list stays hidden. */
  .cta__caret { animation: none; }
  /* The ripple is SMIL, which CSS cannot stop — dropping the filter is what
     stills it. The pop-out still opens and works. */
  .contact__velvet { filter: none; }
  /* The panel is simply there rather than sliding in. The global rule above
     already cuts the transition to nothing; this stops it starting off
     screen, which would otherwise leave it parked outside the viewport. */
  .contact__panel { transform: none; }
}
