/* ============================================================
   Naughty Society — One-Page Site
   Faithful implementation of v4 (badland-style direction).
   ============================================================ */

@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap");

/* ============================================================
   Self-hosted fonts. Neue Haas Grotesk Display Pro (3 weights,
   no italic — for the hero wordmark and the foot bigword only).
   Satoshi (full family + italics — used everywhere else).
   ============================================================ */

@font-face {
  font-family: "Neue Haas Grotesk Display";
  src: url("./fonts/NHaasGroteskDSPro-45Lt.otf") format("opentype");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Neue Haas Grotesk Display";
  src: url("./fonts/NHaasGroteskDSPro-75Bd.otf") format("opentype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Neue Haas Grotesk Display";
  src: url("./fonts/NHaasGroteskDSPro-95Blk.otf") format("opentype");
  font-weight: 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Satoshi";
  src: url("./fonts/Satoshi-Light.otf") format("opentype");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("./fonts/Satoshi-LightItalic.otf") format("opentype");
  font-weight: 300;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("./fonts/Satoshi-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("./fonts/Satoshi-Italic.otf") format("opentype");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("./fonts/Satoshi-Medium.otf") format("opentype");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("./fonts/Satoshi-MediumItalic.otf") format("opentype");
  font-weight: 500;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("./fonts/Satoshi-Bold.otf") format("opentype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("./fonts/Satoshi-BoldItalic.otf") format("opentype");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("./fonts/Satoshi-Black.otf") format("opentype");
  font-weight: 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("./fonts/Satoshi-BlackItalic.otf") format("opentype");
  font-weight: 900;
  font-style: italic;
  font-display: swap;
}

:root {
  --bg:     oklch(99%  0.005 85);
  --fg:     oklch(8%   0.012 70);
  --mute:   oklch(62%  0.006 70);
  /* Darker mute for ≤11px mono captions — passes WCAG AA (~5.2:1). */
  --mute-strong: oklch(46% 0.009 70);
  --hair:   color-mix(in oklch, var(--fg) 12%, transparent);
  --fill:   oklch(95%  0.005 85);
  --fill-2: oklch(91%  0.006 85);
  --fill-3: oklch(20%  0.012 70);

  --accent: oklch(46% 0.16 28);

  /* Inverse-surface tokens — for type sitting on the dark hero/footer.
     Same warm hue, just sampled from the bright end of the lightness ramp. */
  --fg-inverse:        oklch(99% 0.005 85);
  --mute-inverse:      oklch(82% 0.006 85);
  --mute-inverse-soft: oklch(72% 0.006 85);
  --mute-inverse-weak: oklch(60% 0.006 85);
  --hair-inverse:      color-mix(in oklch, var(--fg-inverse) 18%, transparent);

  --font-hero: "Neue Haas Grotesk Display", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --font-body: "Satoshi", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --pad-x: clamp(28px, 5vw, 56px);
  --pad-y: clamp(80px, 12vw, 180px);

  --ease-out-quart: cubic-bezier(0.22, 1, 0.36, 1);
}

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  /* Anchors land below the fixed nav, not under it. */
  scroll-padding-top: clamp(64px, 8vh, 88px);
}

html, body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-feature-settings: "kern" 1, "liga" 1;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

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

::selection {
  background: var(--fg);
  color: var(--bg);
  text-shadow: none;
}

/* ============================================================
   TOP NAV — fixed, mix-blend-difference for legibility everywhere
   ============================================================ */
.nav {
  position: fixed; top: 0; left: 0; right: 0;
  z-index: 60;
  padding: 22px var(--pad-x);
  display: flex; align-items: center; justify-content: space-between;
  color: var(--fg-inverse);
  mix-blend-mode: difference;
  pointer-events: none;
}
.nav .brand,
.nav .links { pointer-events: auto; }

.nav .brand {
  font-family: var(--font-body);
  font-weight: 700;
  text-transform: uppercase;
  font-size: 14px;
  letter-spacing: 0.04em;
  /* A wordmark never breaks mid-name. Without these the flex row
     squeezes the brand before the links and "Naughty / Society" wraps
     onto two lines, spilling out of the nav onto the page beneath. */
  white-space: nowrap;
  flex-shrink: 0;
  /* Padding extends the hit area without shifting the visual baseline. */
  padding: 10px 4px;
  margin: -10px -4px;
}

.nav .links {
  display: flex; gap: 36px;
  font-family: var(--font-mono);
  font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
}
.nav .links a {
  opacity: 0.85;
  transition: opacity 180ms var(--ease-out-quart);
  /* Hit area extends upward into the nav's existing padding without
     shifting the visual baseline (negative margin cancels the padding,
     underline stays 2px under the text). */
  padding: 12px 4px 2px;
  margin: -12px -4px 0;
  border-bottom: 1px solid transparent;
}
.nav .links a:hover { opacity: 1; }
.nav .links a:focus-visible {
  outline: none;
  opacity: 1;
  border-bottom-color: currentColor;
}
.nav .links a[aria-current="true"] {
  opacity: 1;
  border-bottom-color: currentColor;
}

/* ============================================================
   QUIET SECTION HEADER — small mono row
   ============================================================ */
.hdr {
  padding: 0 var(--pad-x);
  margin-bottom: clamp(28px, 3vw, 44px);
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 32px;
  font-family: var(--font-mono);
  font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--mute-strong);
}
.hdr .label { color: var(--fg); }
.hdr .label .num {
  color: var(--accent);
  margin-right: 16px;
  font-feature-settings: "tnum";
}

/* ============================================================
   HERO — full-bleed reel, wordmark bottom-left
   ============================================================ */
.hero {
  height: 100vh;
  height: 100dvh;
  min-height: 720px;
  background: var(--fg);
  color: oklch(87% 0.005 85);
  position: relative;
  overflow: hidden;
}

.hero .reel {
  position: absolute; inset: 0;
  background: radial-gradient(120% 80% at 30% 30%, oklch(20% 0.012 70) 0%, oklch(10% 0.012 70) 55%, oklch(5% 0.01 70) 100%);
  /* Stacking context for the reel-video / noise-overlay layering below. */
  isolation: isolate;
}
/* Playlist video — fills the reel, cropped to cover. Stays under the
   noise overlay (z-index: 1 on ::before) so the film grain dances over
   the picture rather than under it. */
.hero .reel-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
}
.hero .reel::before {
  content: ""; position: absolute; inset: -10%;
  z-index: 1;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='320' height='320'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='2' seed='9' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 0.6 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  background-size: 320px 320px;
  opacity: 0.18;
  mix-blend-mode: screen;
  animation: heroDrift 23s linear infinite;
}
@keyframes heroDrift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-12%, -16%, 0); }
}

.hero::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.55) 0%,
    transparent 22%,
    transparent 65%,
    rgba(0, 0, 0, 0.7) 100%
  );
  pointer-events: none;
}

.hero .corner {
  position: absolute;
  font-family: var(--font-mono);
  font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--mute-inverse);
  z-index: 3;
}
.hero .corner.tl { top: 72px; left: var(--pad-x); }
.hero .corner.tr { top: 72px; right: var(--pad-x); text-align: right; }
.hero .corner.bl { bottom: 28px; left: var(--pad-x); }

.hero .play {
  position: absolute;
  right: var(--pad-x);
  top: 50%; transform: translateY(-50%);
  z-index: 3;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  color: color-mix(in oklch, var(--fg-inverse) 90%, transparent);
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: 0.26em; text-transform: uppercase;
  cursor: pointer;
  background: none; border: 0;
}
.hero .play .ring {
  width: 68px; height: 68px;
  border: 1px solid color-mix(in oklch, var(--fg-inverse) 70%, transparent);
  border-radius: 999px;
  display: grid; place-items: center;
  position: relative;
  transition: transform 160ms var(--ease-out-quart);
}
.hero .play:focus-visible .ring { transform: scale(1.06); }
.hero .play:active .ring { transform: scale(0.97); }
@media (hover: hover) and (pointer: fine) {
  .hero .play:hover .ring { transform: scale(1.06); }
}
.hero .play .ring::before {
  content: "";
  border-style: solid;
  border-width: 9px 0 9px 14px;
  border-color: transparent transparent transparent var(--fg-inverse);
  margin-left: 3px;
  /* Anticipates the press — triangle leans into the motion. */
  transition: transform 220ms var(--ease-out-quart);
}
.hero .play:focus-visible .ring::before { transform: translateX(1.5px); }
@media (hover: hover) and (pointer: fine) {
  .hero .play:hover .ring::before { transform: translateX(1.5px); }
}

.hero .wordmark {
  position: absolute;
  left: var(--pad-x);
  bottom: 96px;
  font-family: var(--font-hero);
  font-style: normal;
  font-weight: 900;
  font-size: clamp(72px, 14vw, 220px);
  line-height: 0.85;
  letter-spacing: -0.045em;
  color: var(--fg-inverse);
  z-index: 3;
  pointer-events: none;
  max-width: calc(100% - var(--pad-x) * 2);
  margin: 0;
}
.hero .wordmark .l2 { display: block; }

.hero .sub {
  position: absolute;
  left: var(--pad-x);
  bottom: 58px;
  z-index: 3;
  margin: 0;
  font-family: var(--font-body);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(14px, 1.05vw, 17px);
  line-height: 1.4;
  color: color-mix(in oklch, var(--fg-inverse) 82%, transparent);
  max-width: 44ch;
  text-wrap: balance;
}

/* ============================================================
   CLIENTS — marquee directly under the hero
   ============================================================ */
.clients-band { padding: clamp(36px, 5vw, 56px) 0; }

.marquee {
  overflow: hidden;
  border-top: 1px solid var(--fg);
  border-bottom: 1px solid var(--fg);
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 6%, #000 94%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0%, #000 6%, #000 94%, transparent 100%);
  cursor: ew-resize;
  isolation: isolate;
}

/* Base height is a CAP-HEIGHT target, not a box size: every logo file is
   trimmed to its ink, so this height is the height of the letterforms
   themselves. Optical size then only varies with weight, which the tiers
   below correct for.

   --logo-scale multiplies every tier at once. The tiers below are an
   optical balance against each other, so a breakpoint must move them
   together or the balance breaks; set --logo-scale on .marquee rather
   than re-tuning individual marks. Defaults to 1. */
.marquee .item .logo {
  height: calc(clamp(20px, 2.4vw, 32px) * var(--logo-scale, 1));
  width: auto;
  max-width: calc(clamp(200px, 25vw, 400px) * var(--logo-scale, 1));
  object-fit: contain;
  display: block;
  filter: grayscale(1) contrast(1.05);
  mix-blend-mode: multiply;
  user-select: none;
  -webkit-user-drag: none;
}

/* Weight correction. Two marks at the same cap height do not read at the
   same size — a heavy geometric sans carries far more ink than a light
   serif, so it must sit lower to feel equal. Tiers are derived from each
   mark's measured ink density against the set's median (0.25). */

/* Heavy — dense bold wordmarks (density 0.40-0.51), set ~0.87x base. */
.marquee .item .logo[src*="Rabanne"]          { height: calc(clamp(17px, 2.1vw, 28px) * var(--logo-scale, 1)); }
.marquee .item .logo[src*="b028ce8f"]         { height: calc(clamp(17px, 2.1vw, 28px) * var(--logo-scale, 1)); }     /* Klarna */
.marquee .item .logo[src*="Nudient"]          { height: calc(clamp(17px, 2.1vw, 28px) * var(--logo-scale, 1)); }
.marquee .item .logo[src*="Byredo"]           { height: calc(clamp(17px, 2.1vw, 28px) * var(--logo-scale, 1)); }

/* Semi-heavy (0.31-0.34), ~0.94x base. */
.marquee .item .logo[src*="Sjostrand"]        { height: calc(clamp(19px, 2.25vw, 30px) * var(--logo-scale, 1)); }

/* Compact marks — the narrowest in the set. Cap height alone leaves them
   short on footprint next to the wide wordmarks, so they sit taller. */
.marquee .item .logo[src*="Jean_Paul"]        { height: calc(clamp(21px, 2.55vw, 34px) * var(--logo-scale, 1)); }    /* 2.6:1, narrowest mark here */

/* A shade under base — these two carried slightly more presence than the
   rest of the mid-weight group. */
.marquee .item .logo[src*="Transparent"]      { height: calc(clamp(19px, 2.25vw, 30px) * var(--logo-scale, 1)); }
.marquee .item .logo[src*="Giorgio_Armani"]   { height: calc(clamp(19px, 2.25vw, 30px) * var(--logo-scale, 1)); }

/* Light and sparse marks need to sit taller to hold their own. */
.marquee .item .logo[src*="Devialet"]         { height: calc(clamp(22px, 2.7vw, 36px) * var(--logo-scale, 1)); }     /* fine tear symbol + light caps */
.marquee .item .logo[src*="logo-2D-black"]    { height: calc(clamp(24px, 2.85vw, 38px) * var(--logo-scale, 1)); }    /* VinFast — outline V + small text */
.marquee .item .logo[src*="Fresh"]            { height: calc(clamp(24px, 2.85vw, 38px) * var(--logo-scale, 1)); }    /* lowercase italic — small x-height */
.marquee .item .logo[src*="Baume"]            { height: calc(clamp(26px, 3.15vw, 42px) * var(--logo-scale, 1)); }    /* stacked lockup: Φ + name + tagline */

/* Ultra-wide wordmarks. Matching the others' cap height would make these
   two to four times wider than everything else, so they are bound by
   max-width instead and allowed to sit shorter — the usual compromise. */
.marquee .item .logo[src*="Carolina-Herrera"] { height: calc(clamp(16px, 1.95vw, 26px) * var(--logo-scale, 1)); }    /* 15.7:1 */
.marquee .item .logo[src*="dries-van-noten"]  { height: calc(clamp(16px, 1.95vw, 26px) * var(--logo-scale, 1)); }    /* 15.5:1 */
.marquee .item .logo[src*="LArtisan"] {                                                 /* 21:1 — the extreme */
  height: calc(clamp(14px, 1.65vw, 22px) * var(--logo-scale, 1));
  max-width: calc(clamp(240px, 29vw, 470px) * var(--logo-scale, 1));
}

.marquee .track {
  display: flex; align-items: center;
  width: max-content;
  animation: scroll 60s linear infinite;
}
.marquee .item {
  padding: 28px clamp(28px, 3.6vw, 56px);
  line-height: 1;
  color: var(--fg);
  white-space: nowrap;
  display: flex; align-items: center; gap: clamp(56px, 7vw, 100px);
}
.marquee .item .sep {
  display: block;
  width: 1px;
  height: calc(clamp(20px, 2.4vw, 32px) * var(--logo-scale, 1));
  background: var(--fg);
  flex-shrink: 0;
}
@keyframes scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* ============================================================
   WORK — badland-style roster, 2-col staggered
   ============================================================ */
.work {
  padding-top: clamp(60px, 7vw, 96px);
  padding-bottom: var(--pad-y);
}
/* MUSEUM WALL — 12-column asymmetric bento.
   Per-tile spans + aspect ratios + vertical offsets create the
   gallery rhythm. See .tile:nth-child(N) rules below for the layout. */
.roster {
  padding: 0 var(--pad-x);
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: clamp(20px, 2.4vw, 44px);
  row-gap: clamp(48px, 6vw, 100px);
}

.tile {
  display: flex; flex-direction: column; gap: 18px;
  cursor: pointer;
}
.tile .frame {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 5;
  background-color: var(--fill);
  background-image: var(--img, none);
  background-size: cover;
  background-position: center;
  overflow: hidden;
  filter: contrast(1.06) brightness(0.94);
  -webkit-mask-image: radial-gradient(125% 135% at 50% 45%, #000 72%, transparent 100%);
          mask-image: radial-gradient(125% 135% at 50% 45%, #000 72%, transparent 100%);
  transition: filter 480ms var(--ease-out-quart);
}
.tile.tone .frame { background-color: var(--fill-2); }
.tile.dark .frame { background-color: var(--fill-3); }
.tile.wide .frame { aspect-ratio: 16 / 10; }

/* Museum wall layout — each tile gets its own grid span, aspect ratio,
   and vertical offset. Source order matters: these win over .wide. */
.tile:nth-child(1)  { grid-column: span 8; }
.tile:nth-child(1) .frame { aspect-ratio: 4 / 5; }

.tile:nth-child(2)  { grid-column: span 4; margin-top: clamp(80px, 10vw, 180px); }
.tile:nth-child(2) .frame { aspect-ratio: 2 / 3; }

.tile:nth-child(3)  { grid-column: span 5; margin-top: clamp(40px, 5vw, 100px); }
.tile:nth-child(3) .frame { aspect-ratio: 3 / 4; }

/* Tile 4 — bleeds to the right viewport edge. Sharp on the right,
   directional mask fades softly on left/top/bottom only. */
.tile:nth-child(4)  {
  grid-column: span 7;
  margin-right: calc(-1 * var(--pad-x));
}
.tile:nth-child(4) .frame {
  aspect-ratio: 3 / 2;
  -webkit-mask-image: radial-gradient(150% 150% at 62% 50%, #000 68%, transparent 100%);
          mask-image: radial-gradient(150% 150% at 62% 50%, #000 68%, transparent 100%);
}

.tile:nth-child(5)  { grid-column: span 7; }
.tile:nth-child(5) .frame { aspect-ratio: 4 / 5; }

.tile:nth-child(6)  { grid-column: span 5; margin-top: clamp(60px, 7vw, 140px); }
.tile:nth-child(6) .frame { aspect-ratio: 16 / 10; }

.tile:nth-child(7)  { grid-column: 1 / -1; }
.tile:nth-child(7) .frame { aspect-ratio: 2 / 1; }

.tile:nth-child(8)  { grid-column: 4 / span 6; }
.tile:nth-child(8) .frame { aspect-ratio: 4 / 5; }

.tile:nth-child(9)  { grid-column: 3 / span 8; }
.tile:nth-child(9) .frame { aspect-ratio: 16 / 9; }

.tile .frame::before {
  content: "";
  position: absolute; inset: 0;
  background:
    linear-gradient(180deg,
      color-mix(in oklch, var(--fg) 8%, transparent) 0%,
      transparent 35%,
      color-mix(in oklch, var(--fg) 38%, transparent) 100%);
  pointer-events: none;
  z-index: 1;
}

.tile .frame .overlay {
  position: absolute; inset: 0;
  display: flex; align-items: flex-end; justify-content: flex-start;
  padding: 28px;
  color: oklch(99% 0.005 85);
  opacity: 0;
  transition: opacity 320ms var(--ease-out-quart);
  background: linear-gradient(to top, color-mix(in oklch, var(--fg) 55%, transparent) 0%, transparent 60%);
  z-index: 2;
}


/* Reel tile — 11 absolutely-positioned .slide layers cycling fast
   (one every 0.3s, full 3.3s cycle), step-end timing so each image
   hard-cuts to the next like a film reel. Pauses on hover so the
   reader can study a frame. The .frame's inherited background-image
   is suppressed; slides own the imagery. */
.tile.reel .frame {
  background-image: none;
}
.tile.reel .slide {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  animation: editorialReel 3.3s step-end infinite;
}
.tile.reel .slide:nth-child(1)  { animation-delay: 0s; }
.tile.reel .slide:nth-child(2)  { animation-delay: 0.3s; }
.tile.reel .slide:nth-child(3)  { animation-delay: 0.6s; }
.tile.reel .slide:nth-child(4)  { animation-delay: 0.9s; }
.tile.reel .slide:nth-child(5)  { animation-delay: 1.2s; }
.tile.reel .slide:nth-child(6)  { animation-delay: 1.5s; }
.tile.reel .slide:nth-child(7)  { animation-delay: 1.8s; }
.tile.reel .slide:nth-child(8)  { animation-delay: 2.1s; }
.tile.reel .slide:nth-child(9)  { animation-delay: 2.4s; }
.tile.reel .slide:nth-child(10) { animation-delay: 2.7s; }
.tile.reel .slide:nth-child(11) { animation-delay: 3.0s; }

/* Each slide gets one 11th of the cycle (~9.09%) before snapping off. */
@keyframes editorialReel {
  0%      { opacity: 1; }
  9.09%   { opacity: 0; }
}

@media (hover: hover) and (pointer: fine) {
  .tile.reel:hover .slide { animation-play-state: paused; }
}
@media (prefers-reduced-motion: reduce) {
  .tile.reel .slide { animation: none; }
  .tile.reel .slide:nth-child(1) { opacity: 1; }
}

/* Video tile — a <video> element absolutely-positioned over the
   frame instead of a background-image. Autoplay + loop + muted at
   the markup level; CSS just makes it cover the frame like an image
   would. */
.tile.video .frame {
  background-image: none;
}
.tile.video .frame > video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
}

/* Project tile — playable video WITH sound. Starts paused; the
   centered play/pause button toggles playback. The button mirrors
   the hero "Play reel" styling exactly (68px ring, mono label) just
   anchored at the tile centre instead of right-of-viewport. */
.tile.project .frame {
  background-image: none;
  background-color: #000;
  cursor: pointer;
}
.tile.project .frame > .project-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
}
.tile.project .play {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 3;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  color: color-mix(in oklch, var(--fg-inverse) 90%, transparent);
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: 0.26em; text-transform: uppercase;
  cursor: pointer;
  background: none; border: 0;
  padding: 0;
  opacity: 1;
  transition: opacity 80ms linear;
}
/* While playing, hide the button — it only re-appears on hover or
   keyboard focus so it doesn't sit on top of the picture. Touch users
   can still pause by tapping the frame (the frame's click handler
   toggles playback regardless of button visibility). */
.tile.project.is-playing .play { opacity: 0; }
@media (hover: hover) and (pointer: fine) {
  .tile.project.is-playing:hover .play { opacity: 1; }
}
/* :focus-visible (not :focus-within) — only keyboard focus shows the
   button. Mouse-click focus is suppressed, so the button properly
   hides immediately after click-to-play. */
.tile.project.is-playing .play:focus-visible { opacity: 1; }
.tile.project .play .ring {
  width: 68px; height: 68px;
  border: 1px solid color-mix(in oklch, var(--fg-inverse) 70%, transparent);
  border-radius: 999px;
  display: grid; place-items: center;
  position: relative;
  transition: transform 160ms var(--ease-out-quart);
}
.tile.project .play:focus-visible .ring { transform: scale(1.06); }
.tile.project .play:active .ring { transform: scale(0.97); }
@media (hover: hover) and (pointer: fine) {
  .tile.project:hover .play .ring { transform: scale(1.06); }
}
.tile.project .play .ring::before {
  content: "";
  border-style: solid;
  border-width: 9px 0 9px 14px;
  border-color: transparent transparent transparent var(--fg-inverse);
  margin-left: 3px;
  transition: transform 220ms var(--ease-out-quart),
              border-color 160ms var(--ease-out-quart),
              border-width 160ms var(--ease-out-quart);
}
@media (hover: hover) and (pointer: fine) {
  .tile.project:hover .play .ring::before { transform: translateX(1.5px); }
}
/* When playing, swap the triangle glyph for two pause bars. Drawn
   with the same ::before pseudo by mutating its border styles — no
   extra DOM. Reuses the ring's transition for a smooth icon swap. */
.tile.project.is-playing .play .ring::before {
  border-style: double;
  border-width: 0 0 0 14px;
  height: 18px;
  margin-left: 0;
  transform: translateX(0);
}

/* Slideshow tile — 4 absolutely-positioned .slide layers stacked
   inside .frame, each running the same fade keyframe with a 5s
   offset so they crossfade A → B → C → D → A on a 20s loop. No JS,
   no library; pure CSS animation. The base .frame background-image
   is suppressed since the slides own the imagery. */
.tile.slideshow .frame {
  background-image: none;
  aspect-ratio: 1 / 1;
}
.tile.slideshow .slide {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  animation: tileSlideshow 1.5s ease-in-out infinite;
}
/* Slide 1 starts at a negative offset so it's already at full
   opacity on first paint — no initial dark frame. Other slides keep
   the same 0.5s relative spacing. */
.tile.slideshow .slide:nth-child(1) { animation-delay: -0.15s; }
.tile.slideshow .slide:nth-child(2) { animation-delay: 0.35s; }
.tile.slideshow .slide:nth-child(3) { animation-delay: 0.85s; }

/* Sequential crossfade — the NEW slide fades in over the OLD while
   the OLD stays at opacity 1. By the time the OLD slide starts
   fading out (43%), the new one is already on top at opacity 1 —
   so the old slide drops behind a fully opaque image and the dark
   frame background never peeks through. Visually it still reads as
   a soft image-to-image transition because the new slide fades in
   smoothly on top of the previous one. */
@keyframes tileSlideshow {
  0%   { opacity: 0; }
  10%  { opacity: 1; }   /* fade in over ~150ms */
  43%  { opacity: 1; }   /* hold visible — next slide finishes its fade-in here */
  53%  { opacity: 0; }   /* fade out behind the next slide (now at 1) */
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .tile.slideshow .slide { animation: none; }
  .tile.slideshow .slide:nth-child(1) { opacity: 1; }
}

/* Summer-campaign variant — this tile bleeds to the right viewport
   edge on a 3:2 frame, so it keeps its own ratio rather than the
   slideshow's default square. */
.tile.slideshow.summer .frame { aspect-ratio: 3 / 2; }

/* Press-to-advance variant — no timeline at all. The slide on screen
   holds until the reader presses the tile, then the next one crossfades
   in over it (see the module in index.html). Opacity is driven purely
   by .is-active; the higher specificity here also overrides the base
   slideshow's animation and its reduced-motion first-slide pin. */
.tile.slideshow.press .slide {
  animation: none;
  opacity: 0;
  transition: opacity 160ms ease-in-out;
}
.tile.slideshow.press .slide.is-active { opacity: 1; }

/* These two tiles are the only ones that respond to a press, so they
   reveal the overlay on hover — the counter is what tells the reader
   there is more than one frame here. Gated behind a real hover-capable
   pointer; touch gets its feedback from the image changing. */
@media (hover: hover) and (pointer: fine) {
  .tile.press:hover .frame .overlay { opacity: 1; }
}

/* The counter is the overlay's only content, parked bottom-right. The
   inherited scrim was sized to carry a full project title across the
   frame; with just the counter left it would only dim the photograph,
   so it retreats to a short fade under the corner it serves. */
.tile.press .frame .overlay {
  justify-content: flex-end;
  background: linear-gradient(to top,
    color-mix(in oklch, var(--fg) 40%, transparent) 0%,
    transparent 20%);
}
.tile.press .frame .overlay .o-index {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.22em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  align-self: flex-end;
  opacity: 0.75;
  /* Carries the counter over a bright frame where the short scrim
     alone can't guarantee contrast. */
  text-shadow: 0 1px 4px color-mix(in oklch, var(--fg) 60%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .tile.slideshow.press .slide { transition: none; }
}

/* Value-prop variant — JS-driven mixed reel in a 4:5 frame:
   still → video → video → still (module in index.html). Stills hold
   a few seconds; each video starts from frame 0 as its slide fades
   in and hands over the moment it ends. Transitions run at the same
   quickfade speed as the base slideshow's crossfade. */
.tile.slideshow.vp .frame {
  aspect-ratio: 4 / 5;
}
.tile.slideshow.vp .slide {
  animation: none;
  transition: opacity 160ms ease-in-out;   /* keep in sync with FADE_MS in the JS module */
}
.tile.slideshow.vp .slide.is-active { opacity: 1; }
/* Pre-init / no-JS fallback — pin the first still. */
.tile.slideshow.vp:not(.is-js) .slide:nth-child(1) { opacity: 1; }
.tile.slideshow.vp .slide > video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
@media (prefers-reduced-motion: reduce) {
  .tile.slideshow.vp .slide { transition: none; }
}
.tile:focus-visible .frame .overlay { opacity: 1; }

.tile .frame .overlay .o-name {
  font-family: var(--font-body);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(20px, 2vw, 32px);
  line-height: 1;
  letter-spacing: -0.01em;
}

.tile .meta {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 24px;
  padding-right: 4px;
}
/* Grid items default to min-width: auto, so without this the caption's
   own width becomes a hard floor under the tile — and since .roster
   tracks are 1fr, that floor pushes the whole page wider than the
   viewport rather than letting the text reflow. */
.tile .meta > * { min-width: 0; }
.tile .meta .brand {
  font-family: var(--font-body);
  font-weight: 900;
  text-transform: uppercase;
  font-size: clamp(28px, 3.2vw, 48px);
  line-height: 0.95;
  letter-spacing: -0.005em;
  color: var(--fg);
}
/* Wrappable on purpose: the `auto` track still takes max-content
   wherever there's room, so roomy tiles keep the one-line-per-row
   look and only genuinely cramped ones reflow. */
.tile .meta .sub {
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--mute-strong);
  text-align: right;
  line-height: 1.6;
}
.tile .meta .sub b { color: var(--fg); font-weight: 500; display: block; }

/* ============================================================
   PRACTICE — editorial list
   ============================================================ */
.services {
  padding-top: clamp(80px, 10vw, 160px);
  padding-bottom: var(--pad-y);
}
.svc-list { padding: 0 var(--pad-x); }

.svc-row {
  display: grid;
  grid-template-columns: 64px minmax(0, 5fr) minmax(0, 4fr);
  align-items: start;
  gap: clamp(24px, 4vw, 80px);
  padding: clamp(36px, 4vw, 60px) 0;
  border-top: 1px solid var(--fg);
}
.svc-row:last-child { border-bottom: 1px solid var(--fg); }

.svc-row .n {
  font-family: var(--font-mono);
  font-size: 11px; letter-spacing: 0.22em;
  color: var(--accent);
  padding-top: 8px;
  font-feature-settings: "tnum";
}
.svc-row .t {
  font-family: var(--font-body);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(36px, 4vw, 64px);
  line-height: 0.95;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.svc-row .d {
  font-family: var(--font-body);
  font-size: 15px; line-height: 1.6;
  max-width: 44ch;
  padding-top: 10px;
}
.svc-row .d .tags {
  display: flex; flex-wrap: wrap; gap: 8px 18px;
  margin-top: 18px;
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--mute-strong);
}

/* ============================================================
   ABOUT
   ============================================================ */
.about {
  padding-top: clamp(80px, 10vw, 160px);
  padding-bottom: var(--pad-y);
}
.about-grid {
  padding: 0 var(--pad-x);
  display: grid;
  grid-template-columns: 1fr 1.2fr;
  gap: clamp(40px, 6vw, 120px);
  align-items: start;
}
.about-statement {
  font-family: var(--font-body);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(36px, 4.2vw, 64px);
  line-height: 1.0;
  letter-spacing: -0.02em;
  text-wrap: balance;
  margin: 0;
}
.about-statement em {
  font-style: italic;
  color: var(--mute);
}
.about-body {
  font-family: var(--font-body);
  font-size: 16px; line-height: 1.65;
  max-width: 60ch;
}
.about-body p { margin: 0 0 20px; }
.about-body p:last-child { margin: 0; }

.about-meta {
  margin-top: clamp(40px, 5vw, 80px);
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 32px;
  border-top: 1px solid var(--hair);
  padding-top: 22px;
  font-family: var(--font-mono);
  font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--mute-strong);
}
.about-meta b { color: var(--fg); font-weight: 500; display: block; margin-bottom: 4px; }

/* ============================================================
   FOOTER
   ============================================================ */
.foot {
  background: var(--fg);
  color: var(--bg);
  padding: var(--pad-y) var(--pad-x) 40px;
}
.foot a {
  color: inherit;
  transition: color 220ms var(--ease-out-quart);
}
.foot a:hover { color: var(--accent); }

.foot .top {
  display: grid;
  grid-template-columns: 1.6fr 1fr 1fr 1fr;
  gap: clamp(32px, 4vw, 80px);
  padding-bottom: clamp(60px, 8vw, 120px);
  border-bottom: 1px solid var(--hair-inverse);
}
.foot h4 {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--mute-inverse-soft);
  margin: 0 0 14px;
}
.foot p { margin: 0 0 6px; font-size: 16px; line-height: 1.5; }

.foot .lede {
  font-family: var(--font-body);
  font-weight: 400;
  font-style: italic;
  font-size: clamp(28px, 3vw, 44px);
  line-height: 1.0;
  letter-spacing: -0.02em;
  max-width: 520px;
  text-wrap: balance;
}
.foot .bigword {
  font-family: var(--font-hero);
  font-style: normal;
  font-weight: 900;
  font-size: clamp(80px, 22vw, 320px);
  line-height: 0.82;
  letter-spacing: -0.045em;
  margin: clamp(60px, 8vw, 120px) 0 0;
}
.foot .colophon {
  margin-top: 32px;
  display: flex; justify-content: space-between;
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--mute-inverse-weak);
}
.foot .no-spam {
  opacity: 0.5;
  font-size: 12px;
  letter-spacing: 0.20em;
  text-transform: uppercase;
}

/* ============================================================
   SEAM — dithered noise transition between the white page and the
   black footer. Pure binary pixels (ink and paper, no gray). Mouse Y
   over the seam pulls the transition toward the cursor.
   ============================================================ */
.seam {
  position: relative;
  width: 100%;
  height: clamp(120px, 14vh, 200px);
  background: var(--bg);
  overflow: hidden;
  cursor: ns-resize;
  isolation: isolate;
}

.seam-canvas {
  display: block;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

@media (prefers-reduced-motion: reduce) {
  .hero .reel::before,
  .marquee .track { animation: none !important; }
  .seam { cursor: default; }
}

/* ============================================================
   Responsive
   ============================================================ */

/* LAPTOP — 13"/14" MacBook class (1440x900, 1470x956, 1512x982).
   Every ramp in this sheet is clamp(min, Nvw, max), and at ~1440px wide
   the vw term has already reached or nearly reached its maximum. So a
   laptop gets display type and imagery sized for a 1920+ desktop — but
   on 900-980px of height instead of 1080+. These screens are wide and
   short, and it is the height that runs out first.

   Lower ceilings here rather than flatter vw ramps, so ultrawide
   desktops keep the sizes they were tuned for. Bounded below at 901px
   so the mobile blocks that follow own everything under 900. */
@media (min-width: 901px) and (max-width: 1600px) {
  /* Display type. The hero wordmark is two lines at 0.85 line-height —
     at 14vw/1440 that is ~200px a line and eats half the viewport. */
  .hero .wordmark   { font-size: clamp(64px, 11vw, 168px); }
  .foot .bigword    { font-size: clamp(72px, 17vw, 250px); }
  .svc-row .t       { font-size: clamp(32px, 3.2vw, 50px); }
  .about-statement  { font-size: clamp(32px, 3.4vw, 52px); }
  .foot .lede       { font-size: clamp(26px, 2.6vw, 38px); }

  /* Client marks, scaled as a set — see --logo-scale above. */
  .marquee { --logo-scale: 0.8; }

  /* The museum wall's portrait tiles are the widest spans, so they are
     also the tallest: tile 1 at span 8 computes to ~1090px tall on a
     1440 viewport, taller than the whole screen. Squaring the two 4/5
     tiles and easing tile 3 brings the tallest under one screen without
     touching the spans, so the row composition is unchanged. */
  .tile:nth-child(1) .frame,
  .tile:nth-child(5) .frame { aspect-ratio: 1 / 1; }
  .tile:nth-child(3) .frame { aspect-ratio: 4 / 5; }

  /* Vertical offsets were set against desktop tile heights; shorter
     tiles need less stagger or the wall drifts apart. */
  .tile:nth-child(2) { margin-top: clamp(60px, 7vw, 120px); }
  .tile:nth-child(6) { margin-top: clamp(48px, 5vw, 96px); }

  .work, .services, .about { padding-top: clamp(56px, 6vw, 104px); }
}

@media (max-width: 900px) {
  .roster {
    grid-template-columns: 1fr;
    row-gap: clamp(40px, 8vw, 80px);
  }
  .roster > .tile { grid-column: 1 / -1; margin-top: 0; margin-left: 0; margin-right: 0; }
  .roster > .tile .frame {
    -webkit-mask-image: radial-gradient(125% 135% at 50% 45%, #000 72%, transparent 100%);
            mask-image: radial-gradient(125% 135% at 50% 45%, #000 72%, transparent 100%);
  }
  .svc-row { grid-template-columns: 1fr; gap: 18px; }
  .about-grid { grid-template-columns: 1fr; }
  .about-meta { grid-template-columns: 1fr 1fr; }
  .foot .top { grid-template-columns: 1fr 1fr; }
  .nav .links { gap: 18px; font-size: 10px; }
  .hero .play { right: 24px; }
  .hero .corner.tr { display: none; }
}
@media (max-width: 560px) {
  .about-meta { grid-template-columns: 1fr; }
  .foot .top { grid-template-columns: 1fr; }
  .nav .brand { font-size: 12px; }
  .nav .links { gap: 14px; }

  /* Section headers stack instead of squeezing the label and the meta
     onto one row — at this width the meta wrapped under the number and
     left the label hanging. */
  .hdr {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }

  /* Tile captions sit under the brand rather than beside it. A display
     brand and a right-aligned caption on one baseline row is elegant at
     tile widths of ~500px+ and cramped at ~270px. */
  .tile .meta {
    grid-template-columns: 1fr;
    gap: 10px;
  }
  .tile .meta .sub { text-align: left; }

  /* Marquee marks read one-per-screen at phone width; tighten the
     rhythm so two or three houses are in view at once. */
  .marquee .item { padding: 22px 20px; gap: 32px; }
  .marquee .item .logo { max-width: 150px; }
}

/* Phone widths can't hold the wordmark and four links on one row
   without collapsing the gap between them (7px at 390) or dropping the
   links below a legible size. The nav carries navigation only here —
   the hero and footer wordmarks keep the brand present. */
@media (max-width: 460px) {
  .nav { justify-content: flex-end; }
  .nav .brand { display: none; }
}

/* Touch targets — the nav's links measure 30px tall, under the 44px
   minimum. Extend the tap area with a pseudo-element instead of padding
   so the focus/aria-current underline stays 2px under the text. */
@media (pointer: coarse) {
  .nav .brand,
  .nav .links a { position: relative; }
  .nav .brand::after,
  .nav .links a::after {
    content: "";
    position: absolute;
    left: 0; right: 0;
    top: -8px; bottom: -8px;
  }
}

/* ============================================================
   POLISH — tabular figures, press feedback, scroll-reveal
   ============================================================ */

.hdr,
.hero .corner,
.tile .meta .sub,
.foot .colophon,
.foot h4 {
  font-variant-numeric: tabular-nums;
}

.tile,
.foot a {
  transition: transform 160ms var(--ease-out-quart);
}
.tile:active,
.foot a:active {
  transform: scale(0.985);
}
.nav .links a:active { transform: translateY(1px); }

.reveal {
  opacity: 0;
  transform: translateY(12px);
  transition:
    opacity 700ms var(--ease-out-quart),
    transform 700ms var(--ease-out-quart);
  transition-delay: calc(var(--reveal-i, 0) * 60ms);
}
.reveal.is-in {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .reveal,
  .reveal.is-in {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ============================================================
   Site-wide film grain — fixed, pointer-events-none, 0.04 opacity.
   Editorial-luxury "paper" feel without weight or motion.
   ============================================================ */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 200;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='1.05' numOctaves='1' seed='4' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.9 0'/></filter><rect width='100%25' height='100%25' filter='url(%23g)'/></svg>");
  background-size: 240px 240px;
  opacity: 0.045;
  mix-blend-mode: multiply;
}

/* Body type breath — slightly looser line-height and tracking on long copy.
   `text-wrap: pretty` controls widows/orphans on supporting browsers; older
   browsers ignore the property and fall back to default wrapping. */
.about-body,
.svc-row .d {
  line-height: 1.68;
  letter-spacing: 0.002em;
  text-wrap: pretty;
}
.foot p {
  text-wrap: pretty;
}

/* ============================================================
   Lenis smooth-scroll overrides (when Lenis is active the
   html element gets `.lenis.lenis-smooth` and we hand control
   to the library; without Lenis, native smooth scroll stands).
   ============================================================ */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }
.lenis.lenis-smooth iframe { pointer-events: none; }

/* ============================================================
   LOADER — first-paint overlay. Perlin/fBm noise field wrapping
   the wordmark; dissolves to reveal the page on document.fonts.ready.
   Sits ABOVE page content (z-index 150) but BELOW the fixed film
   grain (z-index 200), so the grain texture unifies loader and page.
   Skipped on subsequent visits within the same session.
   ============================================================ */
#loader {
  position: fixed;
  inset: 0;
  z-index: 150;
  background: var(--bg);
  overflow: hidden;
  opacity: 1;
  transform: scale(1);
  transform-origin: 50% 50%;
  filter: blur(0);
  /* Full-motion dismiss: 450ms opacity fade + a 1 → 1.06 dilation +
     a small terminal blur, choreographed with the hero's 1.02 → 1.0
     scale-settle below. The ink-field expands AND softens as it
     dissolves into the hero — a print-style negative-to-positive
     flip at the wordmark's anchor. */
  transition: opacity 450ms cubic-bezier(0.77, 0, 0.175, 1),
              transform 450ms cubic-bezier(0.77, 0, 0.175, 1),
              filter 450ms cubic-bezier(0.77, 0, 0.175, 1);
}
#loader.is-dismissing {
  opacity: 0;
  transform: scale(1.06);
  filter: blur(2px);
  pointer-events: none;
}
html.loader-skip #loader { display: none; }

/* Full-bleed canvas — the single visible surface inside the loader.
   Each frame paints cream paper + noise pixels where a per-pixel
   threshold (mask × silhouette pull + distance × spread pull) crosses
   the fBm field. The wordmark is not a layer; it IS the noise.
   LOGICAL_W = 960 with denser feature frequency and one extra octave
   over the seam — the loader is the focal moment, so the grain is
   fine; the seam is decor. */
#loader .loader-noise {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

/* The <img> is a pixel-source ONLY — never visible itself. Stays in
   the DOM so the browser preloader fetches it and `logo.complete` /
   `logo.naturalWidth` are reliable when the script calls drawImage. */
#loader .loader-logo {
  position: absolute;
  display: block;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
  user-select: none;
}

/* Hero settles 1.02 → 1.0 in lockstep with the loader cross-fade —
   same 450ms duration, same easing — so they read as one coordinated
   gesture. Bottom-left origin keeps the wordmark anchored to the same
   point the loader logo lived, which is the spot the eye is watching
   when the negative-to-positive flip happens. */
.hero {
  transform-origin: 0% 100%;
  transition: transform 450ms cubic-bezier(0.77, 0, 0.175, 1);
}
html.loader-on .hero { transform: scale(1.02); }

@media (prefers-reduced-motion: reduce) {
  /* No animation tick; the loader holds a static cream+logo frame
     until the page is ready, then fades out over 200ms. Hero doesn't
     scale. */
  #loader {
    transition: opacity 200ms cubic-bezier(0.4, 0, 0.6, 1);
  }
  html.loader-on .hero { transform: none; }
  .hero { transition: none; }
}
