/* ==========================================================================
   BRB — Interior Architecture & Design
   main.css

   Structure
   01. Tokens
   02. Reset & base
   03. Typography
   04. Layout
   05. Buttons & links
   06. Header / navigation
   07. Hero
   08. Reveal & motion utilities
   09. Components
   10. Forms
   11. Footer
   12. Reduced motion & print
   ========================================================================== */

/* ==========================================================================
   01. TOKENS
   ========================================================================== */

:root {
  /* --- Colour ------------------------------------------------------------
     Warm mineral palette: limestone, plaster, clay, charcoal.
     Never use raw hex in markup — always reference a token.                */
  --bone:        #f6f3ee;  /* page canvas — warm limestone                  */
  --bone-2:      #efeae2;  /* alternating section surface                   */
  --sand:        #e3dcd1;  /* cards, image placeholders                     */
  --sand-2:      #d6cdbf;  /* deeper sand, dividers on sand                 */

  --ink:         #1a1815;  /* primary text — warm near-black                */
  --ink-2:       #3a3630;  /* secondary headings                            */
  --muted:       #6e665b;  /* body-secondary — 5.4:1 on --bone (AA pass)    */
  --muted-quiet: #8c8478;  /* meta / captions only, never body copy         */

  --clay:        #a9714f;  /* brand accent — decorative & large text        */
  --clay-ink:    #8a5636;  /* accent for text/links — 4.9:1 on --bone       */
  --clay-soft:   #e8d9cd;  /* accent wash                                   */

  --noir:        #16140f;  /* dark sections                                 */
  --noir-2:      #211e18;  /* dark surface raised                           */
  --espresso:      #1b1611; /* hero ground — warm charcoal/espresso         */
  --espresso-lift: #241d17; /* barely-there centre lift behind the film     */
  --on-noir:     #f2eee7;  /* text on dark — 15:1                           */
  --on-noir-mut: #a8a094;  /* secondary on dark — 5.6:1 (AA pass)           */

  --line:        rgba(26, 24, 21, 0.13);
  --line-strong: rgba(26, 24, 21, 0.26);
  --line-noir:   rgba(242, 238, 231, 0.16);

  --focus:       #1a1815;
  --danger:      #9b3226;  /* 5.9:1 on --bone                               */
  --success:     #3f6b4a;  /* 5.1:1 on --bone                               */

  /* --- Type -------------------------------------------------------------- */
  --font-display: "Instrument Serif", "Iowan Old Style", "Palatino Linotype",
                  Georgia, "Times New Roman", serif;
  --font-sans:    "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
                  Roboto, Helvetica, Arial, sans-serif;

  /* Fluid scale — clamp(min, preferred, max). Base body never below 16px.  */
  --t-display-xl: clamp(3rem, 9.5vw, 8.5rem);
  --t-display-l:  clamp(2.5rem, 6.2vw, 5rem);
  --t-h2:         clamp(2rem, 4.2vw, 3.5rem);
  --t-h3:         clamp(1.375rem, 2.1vw, 1.875rem);
  --t-h4:         clamp(1.125rem, 1.4vw, 1.3125rem);
  --t-lead:       clamp(1.125rem, 1.55vw, 1.5rem);
  --t-body:       clamp(1rem, 0.97rem + 0.15vw, 1.0625rem);
  --t-small:      0.875rem;
  --t-eyebrow:    0.6875rem;

  --lh-display: 0.98;
  --lh-heading: 1.12;
  --lh-body:    1.65;

  /* --- Space — 4/8 rhythm, spacious end of the scale for editorial calm -- */
  --space-1:  0.25rem;
  --space-2:  0.5rem;
  --space-3:  0.75rem;
  --space-4:  1rem;
  --space-5:  1.5rem;
  --space-6:  2rem;
  --space-7:  3rem;
  --space-8:  4rem;
  --space-9:  6rem;
  --space-10: 8rem;

  --section-y:  clamp(5rem, 10vw, 10rem);
  --gutter:     clamp(1.25rem, 4vw, 4rem);
  --container:  1440px;
  --measure:    68ch;

  /* --- Form ------------------------------------------------------------- */
  --radius-sm: 2px;
  --radius:    3px;
  --radius-pill: 999px;

  --shadow-1: 0 1px 2px rgba(26, 24, 21, 0.04),
              0 8px 24px -12px rgba(26, 24, 21, 0.14);
  --shadow-2: 0 2px 4px rgba(26, 24, 21, 0.05),
              0 24px 60px -24px rgba(26, 24, 21, 0.22);

  /* --- Motion ------------------------------------------------------------
     One rhythm across the whole site. Enter is slower than exit.          */
  --ease-out:  cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in:   cubic-bezier(0.7, 0, 0.84, 0);
  --ease-soft: cubic-bezier(0.33, 1, 0.68, 1);

  --dur-fast:   180ms;
  --dur:        280ms;
  --dur-slow:   620ms;
  --dur-reveal: 900ms;

  /* --- Layers ------------------------------------------------------------ */
  --z-base:    0;
  --z-raised:  10;
  --z-sticky:  20;
  --z-header:  40;
  --z-overlay: 100;
  --z-toast:   1000;
}

/* ==========================================================================
   02. RESET & BASE
   ========================================================================== */

*,
*::before,
*::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchor targets clear the fixed header */
  scroll-padding-top: 6rem;
  /* Sits beneath the shader canvas, so the page still has its warm ground if
     WebGL is unavailable, blocked, or the context is lost. */
  background-color: var(--bone);
}

/* --- Animated ground -------------------------------------------------------
   Fixed, full-viewport, behind everything, and never interactive. `body` only
   becomes transparent once the shader has drawn its first frame, so a failure
   can never leave the page unpainted. */
.backdrop {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  display: block;
  pointer-events: none;
  user-select: none;
}
html.has-backdrop body { background-color: transparent; }

body {
  margin: 0;
  background: var(--bone);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  font-weight: 400;
  font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: clip;
}

body.is-locked { overflow: hidden; }

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

img {
  height: auto;
  /* Warm placeholder while the photo streams in — never a white flash */
  background-color: var(--sand);
}

/* --- Casual save-guard ----------------------------------------------------
   Stops a photograph being dragged to the desktop or highlighted. This is
   friction, not protection: the browser has to fetch the file to show it, so
   it is always reachable from the network panel or by its direct URL. The
   real control over reuse is resolution, not this. The right-click half of
   the guard lives in initImageGuard() in main.js, because a CSS rule cannot
   suppress a context menu.                                                  */
img,
video {
  -webkit-user-drag: none;
  -webkit-user-select: none;
  user-select: none;
}

h1, h2, h3, h4, h5, h6, p, figure, blockquote { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }

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

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

::selection {
  background: var(--clay);
  color: var(--bone);
}

/* Focus — visible, generous, never removed. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}
.on-noir :focus-visible,
.noir :focus-visible { outline-color: var(--on-noir); }

.skip-link {
  position: absolute;
  left: var(--gutter);
  top: -100px;
  z-index: var(--z-toast);
  padding: var(--space-3) var(--space-5);
  background: var(--ink);
  color: var(--bone);
  font-size: var(--t-small);
  letter-spacing: 0.04em;
  transition: top var(--dur) var(--ease-out);
}
.skip-link:focus { top: var(--space-4); }

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

/* ==========================================================================
   03. TYPOGRAPHY
   ========================================================================== */

.display,
.h1, .h2, .h3 {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: -0.015em;
  color: var(--ink);
  text-wrap: balance;
}

.display {
  font-size: var(--t-display-xl);
  line-height: var(--lh-display);
  letter-spacing: -0.028em;
}

.h1 {
  font-size: var(--t-display-l);
  line-height: 1.03;
  letter-spacing: -0.025em;
}

.h2 {
  font-size: var(--t-h2);
  line-height: var(--lh-heading);
}

.h3 {
  font-size: var(--t-h3);
  line-height: 1.2;
}

.h4 {
  font-family: var(--font-sans);
  font-size: var(--t-h4);
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: -0.01em;
}

.italic { font-style: italic; }

.lead {
  font-size: var(--t-lead);
  line-height: 1.5;
  color: var(--ink-2);
  letter-spacing: -0.008em;
  max-width: 30ch;
}

.body { max-width: var(--measure); color: var(--muted); }
.body + .body { margin-top: var(--space-5); }
.body strong { color: var(--ink); font-weight: 500; }

.eyebrow {
  display: inline-flex;
  /* flex-start, not center: when the label wraps to two or three lines on a
     narrow screen the rule must stay level with the first line. */
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--t-eyebrow);
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  font-family: var(--font-sans);
}
.eyebrow::before {
  content: "";
  flex: none;
  width: 1.75rem;
  height: 1px;
  margin-top: 0.82em;   /* centres the rule on the first line box */
  background: var(--clay);
  transform-origin: left center;
  transform: scaleX(var(--rule-scale, 1));
  transition: transform var(--dur-slow) var(--ease-out) 120ms;
}
.eyebrow--plain::before { display: none; }
.on-noir .eyebrow { color: var(--on-noir-mut); }

.meta {
  font-size: var(--t-small);
  color: var(--muted);
  letter-spacing: 0.01em;
}

.num {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1;
}

.text-muted { color: var(--muted); }
.text-clay  { color: var(--clay-ink); }

/* ==========================================================================
   04. LAYOUT
   ========================================================================== */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.container--narrow { max-width: 1080px; }
.container--text   { max-width: 780px; }

.section { padding-block: var(--section-y); }
.section--tight { padding-block: clamp(3.5rem, 7vw, 6.5rem); }
.section--flush-top { padding-top: 0; }

.surface-2 { background: var(--bone-2); }
.noir {
  background: var(--noir);
  color: var(--on-noir);
}
.noir .h1, .noir .h2, .noir .h3, .noir .display { color: var(--on-noir); }
.noir .body, .noir .meta, .noir .lead { color: var(--on-noir-mut); }

.grid { display: grid; gap: var(--space-6); }
.grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* Section header: label left, heading right — an editorial two-column rag  */
.section-head {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--space-6) var(--space-7);
  align-items: start;
  margin-bottom: clamp(2.5rem, 5vw, 4.5rem);
}
.section-head__aside { display: flex; flex-direction: column; gap: var(--space-5); }

/* Where the prose column runs long, the label rail would otherwise sit empty
   for its whole height. Letting the label track the text turns that gap into
   structure instead of a void — the same sticky treatment the process section
   already uses. Only above the breakpoint, where the two columns exist. */
@media (min-width: 901px) {
  .section-head--sticky .section-head__aside {
    position: sticky;
    top: 8rem;
    align-self: start;
  }
}
/* A section-head used purely for its editorial indent has no aside content.
   Once the grid stacks on small screens that empty track would still claim a
   row gap, so drop it out of flow entirely. */
.section-head__aside:empty { display: none; }
.section-head__body  { display: flex; flex-direction: column; gap: var(--space-5); }

.rule {
  height: 1px;
  background: var(--line);
  border: 0;
  margin: 0;
}
.noir .rule { background: var(--line-noir); }

.stack-4 > * + * { margin-top: var(--space-4); }
.stack-5 > * + * { margin-top: var(--space-5); }
.stack-6 > * + * { margin-top: var(--space-6); }

/* --- Media frame -----------------------------------------------------------
   Every photo sits in a frame with a reserved aspect ratio so nothing
   shifts while images load (CLS ≈ 0).                                      */
.frame {
  display: block;
  position: relative;
  overflow: hidden;
  background: var(--sand);
  aspect-ratio: var(--ratio, 4 / 3);
}
.frame > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* No blanket will-change here — promoting every image on the page costs
     real memory on low-end phones. It is set only where something actually
     animates continuously (the hero, the display-type masks). */
}
.frame--portrait { --ratio: 3 / 4; }
.frame--square   { --ratio: 1 / 1; }
.frame--wide     { --ratio: 16 / 10; }
.frame--pano     { --ratio: 21 / 9; }

/* A 21:9 band is ~110px tall on a phone — far too thin to read as a
   photograph. Let the wide crops stand up on small screens. */
@media (max-width: 700px) {
  .frame--pano { --ratio: 4 / 3; }
  .frame--wide { --ratio: 4 / 3; }
}

/* Graceful fallback if a photo fails to load: a tonal plaster panel with
   the project name, instead of a broken-image icon.                        */
.frame.is-failed > img { display: none; }
.frame.is-failed::after {
  content: attr(data-fallback);
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  padding: var(--space-6);
  text-align: center;
  font-family: var(--font-display);
  font-size: var(--t-h3);
  color: var(--muted-quiet);
  background:
    radial-gradient(120% 100% at 20% 0%, var(--sand) 0%, var(--sand-2) 70%);
}

@media (max-width: 900px) {
  .section-head { grid-template-columns: 1fr; }
  .grid--3, .grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
  .grid--2, .grid--3, .grid--4 { grid-template-columns: 1fr; }
}

/* ==========================================================================
   05. BUTTONS & LINKS
   ========================================================================== */

.btn {
  --btn-bg: var(--ink);
  --btn-fg: var(--bone);
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  /* min-height 48px — comfortable touch target on every device */
  min-height: 3rem;
  padding: 0.875rem 1.75rem;
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-size: var(--t-small);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border-radius: var(--radius-pill);
  overflow: hidden;
  isolation: isolate;
  transition: color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              opacity var(--dur) var(--ease-out);
  touch-action: manipulation;
}
/* Wash sweeps up from the bottom on hover — a state change, not decoration */
.btn::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--clay);
  transform: translateY(101%);
  transition: transform var(--dur-slow) var(--ease-out);
}
.btn:hover::before,
.btn:focus-visible::before { transform: translateY(0); }
.btn:hover { color: var(--bone); }
.btn:active { transform: scale(0.98); }

.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--ink);
  border: 1px solid var(--line-strong);
}
.btn--ghost:hover { color: var(--bone); border-color: var(--clay); }

.btn--light {
  --btn-bg: transparent;
  --btn-fg: var(--on-noir);
  border: 1px solid var(--line-noir);
}

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.45;
  pointer-events: none;
  cursor: not-allowed;
}

.btn__spinner {
  width: 1rem;
  height: 1rem;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Text link with a sweeping underline */
.link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--t-small);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding-block: var(--space-2);
}
.link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(1);
  transform-origin: right center;
  transition: transform var(--dur-slow) var(--ease-out);
}
.link:hover::after { transform: scaleX(0); }
.link .arrow { transition: transform var(--dur) var(--ease-out); }
.link:hover .arrow { transform: translateX(4px); }

.link--inline {
  text-transform: none;
  letter-spacing: 0;
  font-size: inherit;
  color: var(--clay-ink);
}

.arrow { width: 1em; height: 1em; flex: none; }

/* ==========================================================================
   06. HEADER / NAVIGATION
   ========================================================================== */

.header {
  position: fixed;
  inset: 0 0 auto 0;
  /* Above the drawer, not below it — otherwise the open menu covers its own
     close button and the only way out is the Escape key. */
  z-index: var(--z-overlay);
  /* Keep the bar clear of notches / rounded corners on phones */
  padding-top: env(safe-area-inset-top, 0px);
  transition: transform var(--dur-slow) var(--ease-out),
              background-color var(--dur-slow) var(--ease-out),
              border-color var(--dur-slow) var(--ease-out);
  border-bottom: 1px solid transparent;
}
/* The header spans the viewport rather than the 1440px measure the sections
   use. Inside the measure it is centred, so past ~1570px the mark drifts
   inward off the left edge; full-bleed keeps it on the gutter at every width
   while the gutter itself still scales. */
.header .container { max-width: none; }

.header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  height: 4.75rem;
}

/* Over a dark hero image the header is light; once scrolled it becomes solid */
.header--over-hero { color: var(--on-noir); }
.header--solid {
  background: color-mix(in srgb, var(--bone) 88%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  backdrop-filter: saturate(180%) blur(14px);
  border-bottom-color: var(--line);
  color: var(--ink);
}
.header--hidden { transform: translateY(-105%); }

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .header--solid { background: var(--bone); }
}

/* While the drawer is open the header floats over it: no bar, light text,
   and never auto-hidden. */
body.is-locked .header {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-bottom-color: transparent;
  color: var(--on-noir);
  transform: none;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  flex: none;
}
/* --- The mark -------------------------------------------------------------
   brbinteriors.webp is a transparent PNG whose artwork sits inside a large
   empty margin: the ink occupies x 688–5563, y 1313–2088 of a 6250 × 6250
   canvas. Rather than ask for a re-export, the window below crops to exactly
   that box — the same geometry as .footer__mark.

   If you later supply a tightly-cropped SVG, replace this whole block with
   `.brand__mark img { height: 1rem; width: auto; position: static; }`.      */
.brand__mark {
  display: block;
  position: relative;
  /* The wordmark is the only branding in the bar, so it carries real weight
     — roughly half the header's height at full size. The crop window below
     holds the plate's own proportions, so it never distorts. */
  height: clamp(1.5rem, 2.4vw, 2.25rem);
  aspect-ratio: 4875 / 775;  /* the ink box */
  overflow: hidden;
  flex: none;
}
.brand__mark img {
  position: absolute;
  width: 128.205%;           /* 6250 / 4875 */
  left: -14.113%;            /*  -688 / 4875 */
  top: -169.419%;            /* -1313 /  775 */
  max-width: none;
  background: none;          /* the global sand placeholder would show through */
  /* Black artwork. Inverting to white is a true interpolation, so it
     cross-fades in step with the header's background. */
  filter: invert(0);
  transition: filter var(--dur-slow) var(--ease-out);
}
.header--over-hero .brand__mark img,
body.is-locked .brand__mark img { filter: invert(1); }
/* The wordmark and descriptor that used to sit beside the mark are gone —
   the top-left is the symbol alone, and the brand name lives in the nav. */

.nav { display: none; }
@media (min-width: 900px) {
  .nav {
    display: flex;
    align-items: center;
    gap: var(--space-6);
    /* Absorbs the free space after the mark, so the links and the language
       switcher travel together to the far right: mark … nav EN/TR. */
    margin-left: auto;
  }
}
.nav__link {
  position: relative;
  font-size: var(--t-small);
  letter-spacing: 0.06em;
  padding-block: var(--space-2);
}
.nav__link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0.15rem;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-slow) var(--ease-out);
}
.nav__link:hover::after,
.nav__link:focus-visible::after { transform: scaleX(1); }
/* Current page is always marked — never rely on hover to show location */
.nav__link[aria-current="page"] { font-weight: 500; }
.nav__link[aria-current="page"]::after {
  transform: scaleX(1);
  background: var(--clay);
  height: 2px;
}

/* --- Language switcher ---------------------------------------------------
   Compact EN / TR pair. Inherits the header's colour, so it reads correctly
   both over the dark film and against the solid bar. Visible at every width,
   including mobile, where the rest of the nav collapses into the drawer. */
.lang {
  display: inline-flex;
  align-items: center;
  flex: none;
}
.lang__btn {
  /* 44px tall hit area; the visible text stays small and quiet */
  min-width: 2rem;
  height: 2.75rem;
  padding-inline: 0.25rem;
  font-size: var(--t-eyebrow);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: currentColor;
  opacity: 0.5;
  transition: opacity var(--dur) var(--ease-out);
  touch-action: manipulation;
}
.lang__btn:hover { opacity: 0.85; }
.lang__btn[aria-pressed="true"] { opacity: 1; }
.lang__sep {
  opacity: 0.35;
  font-size: var(--t-eyebrow);
  user-select: none;
}

/* In the drawer the pair sits on the dark ground with a little more presence */
.drawer .lang { margin-bottom: var(--space-2); }
.drawer .lang__btn { font-size: var(--t-small); letter-spacing: 0.1em; }

.header__actions { display: flex; align-items: center; gap: var(--space-3); }
@media (min-width: 900px) { .header__actions { gap: var(--space-4); } }
.header .btn { display: none; }
@media (min-width: 900px) { .header .btn { display: inline-flex; } }

/* --- Menu toggle (mobile) ------------------------------------------------ */
.menu-toggle {
  display: inline-grid;
  place-content: center;
  width: 3rem;
  height: 3rem;
  margin-right: -0.75rem; /* optical alignment to gutter */
  touch-action: manipulation;
}
@media (min-width: 900px) { .menu-toggle { display: none; } }

.menu-toggle__bars {
  position: relative;
  display: block;
  width: 22px;
  height: 10px;
}
.menu-toggle__bars span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1.5px;
  background: currentColor;
  transition: transform var(--dur) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out);
}
.menu-toggle__bars span:nth-child(1) { top: 0; }
.menu-toggle__bars span:nth-child(2) { bottom: 0; }
[aria-expanded="true"] .menu-toggle__bars span:nth-child(1) {
  transform: translateY(4.25px) rotate(45deg);
}
[aria-expanded="true"] .menu-toggle__bars span:nth-child(2) {
  transform: translateY(-4.25px) rotate(-45deg);
}

/* --- Mobile drawer ------------------------------------------------------- */
.drawer {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-overlay) - 1);
  background: var(--noir);
  color: var(--on-noir);
  display: flex;
  flex-direction: column;
  padding: calc(env(safe-area-inset-top, 0px) + 5.5rem) var(--gutter)
           calc(env(safe-area-inset-bottom, 0px) + var(--space-7));
  clip-path: inset(0 0 100% 0);
  transition: clip-path var(--dur-slow) var(--ease-out),
              visibility 0s linear var(--dur-slow);
  visibility: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.drawer.is-open {
  clip-path: inset(0 0 0 0);
  visibility: visible;
  transition: clip-path var(--dur-slow) var(--ease-out),
              visibility 0s;
}

.drawer__nav { display: flex; flex-direction: column; gap: var(--space-2); }
.drawer__link {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  padding-block: var(--space-4);
  min-height: 3rem;
  font-family: var(--font-display);
  font-size: clamp(2rem, 9vw, 3rem);
  line-height: 1.1;
  border-bottom: 1px solid var(--line-noir);
  opacity: 0;
  transform: translateY(1.25rem);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
}
.drawer.is-open .drawer__link {
  opacity: 1;
  transform: none;
  /* Stagger: 60ms apart — quick enough to feel responsive */
  transition-delay: calc(140ms + var(--i, 0) * 60ms);
}
.drawer__link[aria-current="page"] { color: var(--clay); }
.drawer__index {
  font-family: var(--font-sans);
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  color: var(--on-noir-mut);
}
.drawer__foot {
  margin-top: auto;
  padding-top: var(--space-7);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  opacity: 0;
  transform: translateY(1rem);
  transition: opacity var(--dur-slow) var(--ease-out) 380ms,
              transform var(--dur-slow) var(--ease-out) 380ms;
}
.drawer.is-open .drawer__foot { opacity: 1; transform: none; }

/* ==========================================================================
   07. HERO
   ========================================================================== */

.hero {
  position: relative;
  /* dvh, not vh — avoids the mobile browser-chrome jump */
  min-height: 100svh;
  display: flex;
  align-items: flex-end;
  overflow: clip;
  background: var(--noir);
  color: var(--on-noir);
  padding-bottom: clamp(3rem, 7vw, 6rem);
}
.hero--short { min-height: 78svh; }

.hero__media {
  position: absolute;
  inset: -8% 0 0 0;   /* extra height so parallax never exposes an edge */
  z-index: 0;
}
.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  will-change: transform;
}
/* Scrim: guarantees ≥4.5:1 for text over any photograph, however bright.
   Two layers — a heavy foot for the text block, a light wash for the header. */
.hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to top,
      rgba(10, 9, 7, 0.90)  0%,
      rgba(10, 9, 7, 0.78) 22%,
      rgba(10, 9, 7, 0.46) 46%,
      rgba(10, 9, 7, 0.20) 72%,
      rgba(10, 9, 7, 0.52) 100%);
}

.hero__inner { position: relative; z-index: var(--z-raised); width: 100%; }

.hero__title { color: var(--on-noir); max-width: 16ch; }

/* On a photograph the muted greys disappear — lift them onto the dark scale */
.hero .eyebrow { color: var(--on-noir); }

.hero__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-6);
  margin-top: clamp(2rem, 4vw, 3.5rem);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line-noir);
}
.hero__blurb {
  max-width: 42ch;
  color: var(--on-noir);
}

.scroll-cue {
  display: none;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--t-eyebrow);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--on-noir-mut);
}
@media (min-width: 700px) { .scroll-cue { display: inline-flex; } }
.scroll-cue__track {
  position: relative;
  width: 1px;
  height: 3rem;
  background: var(--line-noir);
  overflow: hidden;
}
.scroll-cue__track::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 45%;
  background: var(--clay);
  animation: cue 2.4s var(--ease-soft) infinite;
}
@keyframes cue {
  0%   { transform: translateY(-100%); }
  55%  { transform: translateY(120%); }
  100% { transform: translateY(120%); }
}

/* ==========================================================================
   07b. SCROLL HERO (home)
   --------------------------------------------------------------------------
   Native port of the 21st.dev "SmoothScrollHero" component — CSS custom
   properties and one rAF-throttled scroll listener instead of framer-motion's
   useScroll / useTransform / useMotionTemplate.

   The component's props map to custom properties on .shero:

     scrollHeight          --run   1500px   the runway the reveal is spread over
     initialClipPercentage --c0    25       window inset at rest
     finalClipPercentage   --c1    75       window outset at rest

   framer-motion                     →  here
     useScroll()                     →  window scroll listener, rAF-throttled
     useTransform(scrollY, …)        →  --p and --pz, written from JS
     useMotionTemplate`polygon(…)`   →  the clip-path calc() below
     backgroundSize 170% → 100%      →  transform: scale() on the film

   Two progress values, exactly as in the original: --p runs over `scrollHeight`
   and drives the clip window; --pz runs over `scrollHeight + 500` and drives
   the zoom, so the film keeps easing in fractionally after the window is open.

   How it works — and what it deliberately does NOT do:
   • Scroll is never hijacked. The section is simply taller than the viewport
     and its inner panel is `position: sticky`, so the browser's own scrolling
     drives everything. Wheel, trackpad, touch, Space, PageDown, arrow keys,
     find-in-page and the scrollbar all behave exactly as the user expects,
     and nothing calls preventDefault().
   • Only two numbers are written from JS. Every visual change is derived from
     them in CSS via clip-path and transform — no layout, no width/height
     animation.
   • Both default to 1, i.e. the finished state. With JS disabled the hero is
     simply a full-bleed film; the runway is only added once JS is driving.
     Nothing is ever stuck half-open.
   ========================================================================== */

@property --p {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}
@property --pz {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

.shero {
  --run: 1500px;   /* scrollHeight */
  --c0: 25;        /* initialClipPercentage */
  --c1: 75;        /* finalClipPercentage */
  position: relative;
  width: 100%;
  height: 100svh;  /* no runway until JS confirms it can drive the reveal */
  background: var(--noir);
}
.shero--live { height: calc(var(--run) + 100svh); }

.shero__pin {
  position: sticky;
  top: 0;
  height: 100vh;   /* fallback for browsers without svh */
  height: 100svh;
  overflow: clip;
  isolation: isolate;
  color: var(--on-noir);
}

/* The film runs pale in places, and the header and wordmark sit on it in
   white for the whole reveal — so the top of the panel keeps a soft scrim.
   Above the window, below the wordmark and the pause control. */
.shero__pin::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 34vh;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(to bottom,
    rgba(10, 9, 7, 0.58) 0%,
    rgba(10, 9, 7, 0.26) 45%,
    transparent 100%);
}

/* The window the film is revealed through. The polygon is the component's
   template with the two interpolations folded in:
     clipStart : --c0 → 0     as --p goes 0 → 1
     clipEnd   : --c1 → 100   as --p goes 0 → 1                            */
.shero__window {
  --cs: calc(var(--c0) * (1 - var(--p)));
  --ce: calc(var(--c1) + (100 - var(--c1)) * var(--p));
  position: absolute;
  inset: 0;
  background: #000;
  clip-path: polygon(
    calc(var(--cs) * 1%) calc(var(--cs) * 1%),
    calc(var(--ce) * 1%) calc(var(--cs) * 1%),
    calc(var(--ce) * 1%) calc(var(--ce) * 1%),
    calc(var(--cs) * 1%) calc(var(--ce) * 1%)
  );
  will-change: clip-path;
}

/* The film. `background-size: 170% → 100%` in the original; a <video> is not
   a background, so the same movement is done with a scale transform, which
   is also the cheaper of the two to composite. */
.shero__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(calc(1.7 - 0.7 * var(--pz)));
  transform-origin: 50% 50%;
  will-change: transform;
}

/* The hero carries no mark of its own, so the pause control keeps the top
   right corner it defaults to. */

@media (max-width: 700px) {
  .shero { --run: 900px; --c0: 12; --c1: 88; }
}

/* Reduced motion.
   The reveal itself is NOT disabled here, and that is deliberate: it is
   scroll-linked, so it only ever moves exactly as far as the reader scrolls
   and stops the instant they stop. That is not the autonomous, unbidden
   motion `prefers-reduced-motion` exists to suppress — killing it just
   removed the hero. What IS autonomous is the film looping by itself, so
   that gets a visible pause control (.film-toggle), always available. */
@media (prefers-reduced-motion: reduce) {
  .shero { --run: 700px; }
  .shero__media { will-change: auto; }
}
/* --- Film pause control -------------------------------------------------
   Moving content that lasts more than a few seconds needs a way to stop it
   (WCAG 2.2.2). Sits clear of the headline, 44px target, keyboard reachable. */
.film-toggle {
  position: absolute;
  right: var(--gutter);
  /* Below the header, clear of the headline and the scroll cue at the foot */
  top: calc(env(safe-area-inset-top, 0px) + 6rem);
  z-index: 4;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid var(--line-noir);
  border-radius: var(--radius-pill);
  color: var(--on-noir);
  background: rgba(10, 9, 7, 0.34);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  transition: background-color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              opacity var(--dur) var(--ease-out);
  touch-action: manipulation;
}
.film-toggle:hover { background: rgba(10, 9, 7, 0.6); border-color: var(--on-noir-mut); }
.film-toggle svg { width: 0.875rem; height: 0.875rem; }
.film-toggle .film-toggle__play { display: none; }
.film-toggle[aria-pressed="true"] .film-toggle__play { display: block; }
.film-toggle[aria-pressed="true"] .film-toggle__pause { display: none; }

/* Page hero for interior pages (light) */
.page-hero {
  padding-top: clamp(9rem, 16vw, 13rem);
  padding-bottom: clamp(3rem, 6vw, 5rem);
}
.page-hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--space-6) var(--space-7);
  align-items: end;
}
@media (max-width: 900px) { .page-hero__grid { grid-template-columns: 1fr; } }

/* ==========================================================================
   07d. HERO FOOT NAVIGATION (home) — EXPERIMENTAL
   --------------------------------------------------------------------------
   The footer's Navigate / Connect content, moved into the foot of the hero so
   the homepage reads as one full-screen composition instead of a hero with a
   large block bolted underneath.

   Deliberately weightless: one line, eyebrow-sized, uppercase, in the muted
   on-dark tone, with no headings, no rules and no panel. It carries its own
   gradient rather than adding a scrim to `.shero__pin`, so nothing in §07b —
   the reveal, the film, the timing — is touched by this at all.

   Rollback: _backup/homepage-before-hero-nav/
   ========================================================================== */

.hnav {
  position: absolute;
  inset: auto 0 0 0;
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-7);
  /* The top padding is the scrim's runway, not spacing — the row itself sits
     close to the bottom edge. */
  padding: clamp(3rem, 9vh, 5.5rem) var(--gutter) clamp(1.1rem, 2.6vh, 1.75rem);
  font-size: var(--t-eyebrow);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--on-noir-mut);
  background: linear-gradient(to top,
    rgba(10, 9, 7, 0.74) 0%,
    rgba(10, 9, 7, 0.34) 52%,
    transparent 100%);
}

.hnav__set {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
  min-width: 0;
}

.hnav a {
  color: inherit;
  text-decoration: none;
  transition: color var(--dur) var(--ease-out);
}
/* An address is a literal string, so it is the one item here that keeps its
   own casing rather than being uppercased with the rest of the row. */
.hnav a[data-studio-email] { text-transform: lowercase; }

/* The same bar closing an ordinary page rather than overlaying the film:
   identical type, spacing and alignment, but static, on the page's own
   ground, with a hairline instead of a gradient. */
.hnav--flat {
  position: static;
  background: none;
  color: var(--muted);
  padding: clamp(1.25rem, 3vh, 2rem) var(--gutter);
  border-top: 1px solid var(--line);
}
.hnav--flat a:hover,
.hnav--flat a:focus-visible { color: var(--ink); }
.hnav--flat a:focus-visible { outline-color: var(--focus); }
.hnav a:hover,
.hnav a:focus-visible { color: var(--on-noir); }
.hnav a:focus-visible { outline: 1px solid var(--on-noir); outline-offset: 4px; }

/* On a phone the three sets stack into a short block in the same corner
   rather than stretching edge to edge, which reads as a list, not a bar. */
@media (max-width: 700px) {
  .hnav {
    justify-content: flex-start;
    gap: var(--space-2) var(--space-4);
    padding-bottom: clamp(1rem, 3vh, 1.5rem);
  }
  .hnav__set { gap: var(--space-2) var(--space-4); }
}

/* ==========================================================================
   07c. PROJECT FLYTHROUGH (projects page)
   --------------------------------------------------------------------------
   All fifteen projects arranged down a receding corridor — a native CSS 3D
   port of the 21st.dev "3d-gallery-photography" component. No Three.js, no
   WebGL: one `perspective` container and a `translate3d()` per slide, which
   is cheap enough to sit on the same page as the backdrop shader.

   How it works — and what it deliberately does NOT do:
   • Scroll is never hijacked, exactly as in §07b. The section is taller than
     the viewport and its panel is `position: sticky`, so the browser's own
     scrolling flies the camera down the corridor. Wheel, trackpad, touch,
     Space, PageDown, find-in-page and the scrollbar all behave normally and
     nothing calls preventDefault() on wheel or touch.
   • Four inputs feed one depth value in JS — scroll position, an idle
     auto-drift, pointer drag and the arrow buttons. The corridor is modular
     (project 15 is followed by project 01), so they compose without ever
     running out of runway.
   • Only `transform`, `opacity` and `z-index` are written from JS. No
     layout, no width/height animation.
   • With JS off the section collapses to nothing and the grid below is the
     whole story — the flythrough is never the only route to a project.
   ========================================================================== */

/* No background of its own: the corridor sits directly on the Plasma shader
   from backdrop.js, the same ground the light sections above and below it
   already show. The shader is untouched — this section simply stops covering
   it. If WebGL is unavailable the html-level bone ground shows through
   instead, so the section is never unpainted. */
.gal {
  position: relative;
  color: var(--ink);
  /* Collapsed until JS confirms it can drive the corridor. */
  display: none;
}
.js .gal { display: block; height: 100svh; }
/* Only once JS is driving does the section get its scroll runway. */
.js .gal--live { height: 300svh; }

.gal__pin {
  position: sticky;
  top: 0;
  height: 100vh;          /* fallback for browsers without svh */
  height: 100svh;
  overflow: hidden;
  isolation: isolate;
}

/* A slow haze in the shader's own bone, so the corridor reads as depth and
   the frames recede into the same warm ground rather than into a void.
   Sits under the slides; the HUD sits over them. */
.gal__pin::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 400;
  pointer-events: none;
  background:
    radial-gradient(120% 90% at 50% 45%, transparent 42%, rgba(246, 243, 238, 0.58) 100%),
    /* On a light ground the copy is dark-on-pale, so these bands have to do
       more work than the dark ones they replaced: the title card and the
       caption must stay legible over whatever frame happens to be passing. */
    linear-gradient(to bottom,
      rgba(246, 243, 238, 0.95) 0%,
      rgba(246, 243, 238, 0.78) 22%,
      transparent 48% 62%,
      rgba(246, 243, 238, 0.8) 82%,
      rgba(246, 243, 238, 0.96) 100%);
}

/* The perspective host. `overflow: hidden` here is safe: it clips, but this
   element carries `perspective` rather than `transform-style: preserve-3d`,
   so nothing is flattened. Slide stacking is set explicitly from JS. */
.gal__scene {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* A long lens. A tighter perspective (~1200px) collapses everything past
     the third project into the vanishing point; at this distance the whole
     twelve-deep corridor stays legible. */
  perspective: 2400px;
  perspective-origin: 50% 47%;
  touch-action: pan-y;    /* vertical scrolling stays the browser's job */
  cursor: grab;
}
.gal__scene.is-dragging { cursor: grabbing; }

.gal__slide {
  position: absolute;
  top: 50%;
  left: 50%;
  /* One source for the frame width: the negative margins that centre it are
     derived from --w, so the two can never drift apart. -0.625 is half of
     the 4:5 frame's height. */
  --w: clamp(11rem, 26vw, 24rem);
  width: var(--w);
  margin: calc(var(--w) * -0.625) 0 0 calc(var(--w) * -0.5);
  text-decoration: none;
  color: inherit;
  opacity: 0;
  /* JS writes transform, opacity and z-index every frame — declaring them
     here keeps the compositor from re-promoting the layer each time. */
  will-change: transform, opacity;
  transition: none;
}
.gal__slide:focus-visible { outline: none; }
.gal__slide:focus-visible .gal__frame {
  outline: 2px solid var(--focus);
  outline-offset: 6px;
}

.gal__frame {
  --ratio: 4 / 5;
  display: block;
  background: var(--sand);
  box-shadow: var(--shadow-2);
}
.gal__frame > img {
  filter: saturate(0.92);
  transition: filter var(--dur) var(--ease-out);
}
.gal__slide:hover .gal__frame > img,
.gal__slide:focus-visible .gal__frame > img { filter: saturate(1); }

/* Per-slide label — the project's own title, so the corridor is readable
   without waiting for the caption to catch up. */
.gal__tag {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-top: var(--space-3);
  font-size: var(--t-small);
  line-height: 1.3;
  color: var(--ink);
  /* Set per frame from JS — see initGallery(). Deeper frames drop their
     label so the corridor does not fill up with overlapping titles. */
  opacity: var(--tag, 1);
}
.gal__tag-index {
  font-variant-numeric: tabular-nums;
  font-size: var(--t-eyebrow);
  letter-spacing: 0.14em;
  color: var(--muted);
}

/* --- Heads-up display --------------------------------------------------- */

.gal__hud {
  position: absolute;
  inset: auto 0 0 0;
  z-index: 500;
  padding: 0 var(--gutter) clamp(1.5rem, 4vw, 3rem);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--space-5);
  pointer-events: none;   /* only the controls take the pointer back */
}
.gal__hud > * { pointer-events: auto; }

.gal__intro {
  position: absolute;
  top: clamp(5.5rem, 12vw, 9rem);
  left: var(--gutter);
  right: var(--gutter);
  z-index: 500;
  max-width: 34ch;
  pointer-events: none;
}
.gal__intro .eyebrow { color: var(--muted); }
.gal__intro h2 {
  margin-top: var(--space-4);
  color: var(--ink);
}
.gal__intro .body {
  margin-top: var(--space-4);
  max-width: 30ch;
  color: var(--muted);
}

.gal__caption { min-width: 0; }
.gal__caption-index {
  display: block;
  font-size: var(--t-eyebrow);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.gal__caption-title {
  display: block;
  margin-top: var(--space-2);
  font-family: var(--font-display);
  font-size: var(--t-h3);
  line-height: var(--lh-heading);
  color: var(--ink);
}
.gal__caption-meta {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--t-small);
  color: var(--muted);
}
/* Swapped mid-flight, so it cross-fades rather than snapping. */
.gal__caption-title,
.gal__caption-meta,
.gal__caption-index { transition: opacity var(--dur) var(--ease-out); }
.gal__caption.is-swapping .gal__caption-title,
.gal__caption.is-swapping .gal__caption-meta,
.gal__caption.is-swapping .gal__caption-index { opacity: 0; }

.gal__controls {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.gal__btn {
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out);
}
.gal__btn:hover { background: rgba(26, 24, 21, 0.06); border-color: var(--muted); }
.gal__btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
.gal__btn svg { width: 1rem; height: 1rem; display: block; }
.gal__btn .gal__icon-play { display: none; }
.gal__btn[aria-pressed="true"] .gal__icon-play { display: block; }
.gal__btn[aria-pressed="true"] .gal__icon-pause { display: none; }

.gal__hint {
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  bottom: clamp(0.5rem, 1.5vw, 1rem);
  z-index: 500;
  text-align: center;
  font-size: var(--t-eyebrow);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  pointer-events: none;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.gal__hint.is-gone { opacity: 0; }

@media (max-width: 700px) {
  .gal__hud { grid-template-columns: 1fr; }
  .gal__intro { max-width: none; }
  .gal__intro h2 { font-size: var(--t-h3); }
  .gal__hint { display: none; }
  /* Narrower frames so the pair either side of the camera — and their
     titles — still fit inside a phone's width. */
  .gal__slide { --w: clamp(8rem, 40vw, 12rem); }
  .gal__tag { font-size: var(--t-eyebrow); }
}

/* Under reduced motion the corridor keeps its scroll-linked depth — the
   reader advances it themselves, exactly as with the film stage in §07b —
   but the autonomous drift never starts and the runway is shortened. */
@media (prefers-reduced-motion: reduce) {
  .js .gal--live { height: 180svh; }
  .gal__frame > img { transition: none; }
}

/* ==========================================================================
   08. REVEAL & MOTION UTILITIES
   --------------------------------------------------------------------------
   Content is visible by default. Only when JS confirms it can drive the
   animation (html.js) do elements start hidden — so a JS failure, an old
   browser, or reduced-motion all still render a complete page.
   ========================================================================== */

.js [data-reveal] {
  opacity: 0;
  transform: translate3d(0, var(--reveal-y, 1.75rem), 0);
  transition:
    opacity var(--dur-reveal) var(--ease-out) var(--reveal-delay, 0ms),
    transform var(--dur-reveal) var(--ease-out) var(--reveal-delay, 0ms);
}
.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
}

/* Line-by-line mask reveal for display type.
   The heading itself stays put — only its lines slide up out of their mask. */
.js [data-reveal="lines"] { opacity: 1; transform: none; }
.line { display: block; overflow: hidden; padding-bottom: 0.06em; }
.line__inner { display: block; will-change: transform; }
.js .line__inner {
  transform: translate3d(0, 105%, 0);
  transition: transform var(--dur-reveal) var(--ease-out) var(--reveal-delay, 0ms);
}
.js .is-in .line__inner,
.js .line.is-in .line__inner { transform: none; }

/* Curtain reveal for photographs: the frame opens, the image settles back
   from a slight scale — reads as the camera coming to rest. */
.js [data-reveal="media"] {
  opacity: 1;
  transform: none;
  clip-path: inset(0 0 100% 0);
  transition: clip-path 1100ms var(--ease-out) var(--reveal-delay, 0ms);
}
.js [data-reveal="media"] > img {
  transform: scale(1.14);
  transition: transform 1500ms var(--ease-out) var(--reveal-delay, 0ms);
}
.js [data-reveal="media"].is-in { clip-path: inset(0 0 0 0); }
.js [data-reveal="media"].is-in > img { transform: scale(1); }

/* Rule that draws itself */
.js [data-reveal="rule"] {
  opacity: 1;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-reveal) var(--ease-out) var(--reveal-delay, 0ms);
}
.js [data-reveal="rule"].is-in { transform: scaleX(1); }

/* Hero choreography runs on load, not on scroll */
.js .hero [data-reveal] { transition-duration: 1100ms; }

/* --- Custom cursor -------------------------------------------------------
   Mounted by JS only on a real desktop pointer, and never under reduced
   motion. The `has-cursor` class is added *after* the element is in the DOM,
   so if anything fails the native cursor is simply never hidden. */
.cursor {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 10000;          /* above the page-transition veil */
  pointer-events: none;     /* never intercepts clicks, hover or selection */
  opacity: 0;               /* revealed on the first real mouse move */
  transition: opacity var(--dur) var(--ease-out);
  will-change: transform;
}
.cursor.is-visible { opacity: 1; }
.cursor svg { display: block; filter: drop-shadow(0 2px 6px rgba(10, 9, 7, 0.28)); }

/* Hide the native cursor only while ours is actually running */
html.has-cursor,
html.has-cursor * { cursor: none; }

/* …but give it back over text entry, where the I-beam and caret matter.
   Higher specificity than the rule above, so it wins. */
html.has-cursor.cursor-native,
html.has-cursor.cursor-native * { cursor: auto; }
html.has-cursor.cursor-native input,
html.has-cursor.cursor-native textarea { cursor: text; }
html.has-cursor.cursor-native .cursor { opacity: 0; }

/* Page transition veil — a 220ms fade so navigation feels continuous */
.veil {
  position: fixed;
  inset: 0;
  z-index: var(--z-toast);
  background: var(--bone);
  opacity: 0;
  pointer-events: none;
  transition: opacity 220ms var(--ease-out);
}
.js body.is-leaving .veil { opacity: 1; }

/* ==========================================================================
   09. COMPONENTS
   ========================================================================== */

/* --- Marquee -------------------------------------------------------------- */
.marquee {
  --marquee-duration: 42s;
  position: relative;
  display: flex;
  overflow: hidden;
  padding-block: clamp(2rem, 4vw, 3.5rem);
  border-block: 1px solid var(--line);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
}
.marquee__track {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--space-7);
  padding-right: var(--space-7);
  animation: marquee var(--marquee-duration) linear infinite;
}
.marquee:hover .marquee__track { animation-play-state: paused; }
.marquee__item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-7);
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 4vw, 3rem);
  line-height: 1;
  color: var(--ink-2);
  white-space: nowrap;
}
.marquee__item::after {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--clay);
  flex: none;
}
@keyframes marquee { to { transform: translateX(-100%); } }

/* --- Project frame (a tile in the mosaic) --------------------------------- */
.pframe {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--sand);
}
.pframe__media {
  --ratio: auto;      /* the tile owns the height; the frame fills it */
  position: absolute;
  inset: 0;
  height: 100%;
}

/* Hover uses the standalone `scale` property, not `transform`.
   The reveal already owns `transform` on this image, so the two compose
   instead of colliding. `mediaSize` in the original was a static scale; here
   it is the resting state and hover eases it up a little. */
.pframe__media > img { scale: 1; }

/* The reveal rule in §08 also targets this image, and its `transition`
   shorthand replaces the whole list — which would leave `scale` untransitioned
   and make the hover snap. Both are declared together here, at a specificity
   that ties `.js [data-reveal="media"] > img` and a position that wins. */
.js .pframe__media > img {
  transition: transform 1500ms var(--ease-out) var(--reveal-delay, 0ms),
              scale var(--dur-slow) var(--ease-out);
}

/* The label. Imagery first, then the title, then the number and category —
   nothing else. It rides in from the foot on hover and is always present for
   keyboard and touch. */
.pframe__label {
  position: absolute;
  inset: auto 0 0 0;
  z-index: 2;
  display: grid;
  gap: var(--space-1);
  padding: var(--space-5);
  color: var(--on-noir);
  background: linear-gradient(to top,
    rgba(10, 9, 7, 0.72) 0%,
    rgba(10, 9, 7, 0.34) 55%,
    transparent 100%);
}
.pframe__index {
  font-size: var(--t-eyebrow);
  letter-spacing: 0.18em;
  color: var(--on-noir-mut);
}
.pframe__title {
  font-family: var(--font-display);
  font-size: var(--t-h4);
  line-height: 1.15;
  letter-spacing: -0.015em;
}
.pframe__cat {
  font-size: var(--t-small);
  color: var(--on-noir-mut);
}

/* The hovered tile lifts a little. Driven by the `is-hot` class rather than
   `:hover` inside `@media (hover: hover) and (pointer: fine)`: this browser
   reports `hover: none` / `pointer: coarse` with maxTouchPoints 10 despite a
   real mouse, so that media query never matches and every rule inside it is
   dead. initFrameGrid() sets the class from real pointer events instead —
   the same reason initCursor() waits for mouse events rather than trusting
   the media query. */
.pgrid > li.is-hot .pframe__media > img,
.pframe:focus-visible .pframe__media > img { scale: 1.045; }

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

/* Featured — alternating full-bleed editorial rows */
.feature {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(1.5rem, 4vw, 4rem);
  align-items: center;
}
.feature + .feature { margin-top: clamp(4rem, 9vw, 9rem); }
.feature:nth-child(even) .feature__media { order: 2; }
.feature__media { --ratio: 5 / 4; }
.feature__body { display: flex; flex-direction: column; gap: var(--space-5); }
.feature__specs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4) var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line);
}
.spec__label {
  display: block;
  font-size: var(--t-eyebrow);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted-quiet);
  margin-bottom: var(--space-1);
}
.spec__value { font-size: var(--t-small); color: var(--ink-2); }

@media (max-width: 900px) {
  .feature { grid-template-columns: 1fr; }
  .feature:nth-child(even) .feature__media { order: 0; }
}

/* --- Services list -------------------------------------------------------- */
.service {
  border-top: 1px solid var(--line);
}
.service:last-child { border-bottom: 1px solid var(--line); }

.service__trigger {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  width: 100%;
  padding-block: clamp(1.25rem, 2.5vw, 2rem);
  text-align: left;
  min-height: 3.5rem;
  transition: color var(--dur) var(--ease-out);
}
.service__trigger:hover { color: var(--clay-ink); }
.service__num {
  font-size: var(--t-eyebrow);
  letter-spacing: 0.18em;
  color: var(--muted-quiet);
  flex: none;
  width: 2.5rem;
}
.service__name {
  flex: 1;
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  line-height: 1.15;
  letter-spacing: -0.015em;
}
.service__icon {
  position: relative;
  width: 1rem;
  height: 1rem;
  flex: none;
}
.service__icon::before,
.service__icon::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1px;
  background: currentColor;
  transition: transform var(--dur-slow) var(--ease-out);
}
.service__icon::after { transform: rotate(90deg); }
[aria-expanded="true"] .service__icon::after { transform: rotate(0deg); }

/* grid-template-rows 0fr→1fr animates height without measuring it in JS */
.service__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-slow) var(--ease-out);
}
.service__panel[data-open="true"] { grid-template-rows: 1fr; }
.service__panel > div { overflow: hidden; }
.service__content {
  display: grid;
  grid-template-columns: 2.5rem minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-5);
  padding-bottom: clamp(1.5rem, 3vw, 2.5rem);
}
.service__content > :first-child { grid-column: 2; }
@media (max-width: 780px) {
  .service__content { grid-template-columns: 1fr; }
  .service__content > :first-child { grid-column: 1; }
}
.service__tags { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.tag {
  font-size: var(--t-small);
  padding: 0.35rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  color: var(--muted);
}

/* --- Process -------------------------------------------------------------- */
.process { display: grid; grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); gap: var(--space-7); }
@media (max-width: 900px) { .process { grid-template-columns: 1fr; } }
.process__sticky { position: sticky; top: 8rem; align-self: start; }
@media (max-width: 900px) { .process__sticky { position: static; } }

.step {
  display: grid;
  grid-template-columns: 4rem minmax(0, 1fr);
  gap: var(--space-5);
  padding-block: clamp(1.75rem, 3.5vw, 2.75rem);
  border-top: 1px solid var(--line-noir);
}
.step:last-child { border-bottom: 1px solid var(--line-noir); }
.step__num {
  font-family: var(--font-display);
  font-size: var(--t-h3);
  color: var(--clay);
  line-height: 1;
}
@media (max-width: 560px) {
  .step { grid-template-columns: 1fr; gap: var(--space-3); }
}

/* --- Stats ---------------------------------------------------------------- */
.stat__value {
  font-family: var(--font-display);
  font-size: clamp(2.75rem, 6vw, 4.5rem);
  line-height: 1;
  letter-spacing: -0.02em;
}
.stat__label {
  margin-top: var(--space-3);
  font-size: var(--t-small);
  color: var(--on-noir-mut);
  max-width: 22ch;
}

/* --- Testimonials --------------------------------------------------------- */
.quotes { position: relative; }
.quotes__viewport { position: relative; }
.quote {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  transform: translateY(0.75rem);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out),
              visibility 0s linear var(--dur-slow);
}
.quote.is-active {
  position: relative;
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out),
              visibility 0s;
}
.quote__text {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 3.2vw, 2.5rem);
  line-height: 1.28;
  letter-spacing: -0.018em;
  text-wrap: balance;
}
.quote__by {
  margin-top: var(--space-6);
  font-size: var(--t-small);
  color: var(--muted);
}
.quotes__controls {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-7);
}
.dot {
  width: 2.75rem;   /* 44px touch target, 8px visual */
  height: 2.75rem;
  display: grid;
  place-content: center;
  touch-action: manipulation;
}
.dot::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  transition: background-color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out);
}
.dot[aria-selected="true"]::before {
  background: var(--clay);
  border-color: var(--clay);
  transform: scale(1.25);
}

/* --- Filters -------------------------------------------------------------- */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.filter {
  min-height: 2.75rem;
  padding: 0.5rem 1.15rem;
  font-size: var(--t-small);
  letter-spacing: 0.02em;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  transition: color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              background-color var(--dur) var(--ease-out);
  touch-action: manipulation;
}
.filter:hover { color: var(--ink); border-color: var(--line-strong); }
.filter[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--bone);
}

/* --- The mosaic ----------------------------------------------------------
   Native port of the 21st.dev "DynamicFrameLayout". Hovering a tile widens
   its column and heightens its row while the others give way — the whole
   grid redistributes rather than the tile floating above its neighbours.

   The original's props map to custom properties:
     hoverSize  --hover  6      fr given to the hovered track
     gapSize    --gap    6px    the near-flush mosaic gap
     (each track rests at 4fr, exactly as in the original's "4fr 4fr 4fr")

   Generalised from the original's fixed 3 × 3 to however many projects
   exist: --cols is set per breakpoint here and --rows is written from JS,
   which also writes the two track lists on hover. See initFrameGrid().     */
.pgrid {
  --cols: 1;
  --rows: 1;
  --hover: 6;
  --gap: 6px;
  --row-h: clamp(13rem, 26vw, 20rem);
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  grid-auto-rows: var(--row-h);
  transition: grid-template-columns var(--dur-slow) var(--ease-soft),
              grid-template-rows var(--dur-slow) var(--ease-soft);
}
.pgrid > li {
  position: relative;
  min-width: 0;
  min-height: 0;
  transition: opacity var(--dur-slow) var(--ease-out),
              filter var(--dur-slow) var(--ease-out);
}
.pgrid > li[hidden] { display: none; }

/* While one tile is being read the rest step back — a little softer and a
   little quieter, so the hovered project is the only sharp thing on screen.
   Class-driven for the reason given above: neither a hover media query nor
   `:has(> li:hover)` fires on a browser that claims it has no mouse. */
.pgrid.is-hovering > li {
  opacity: 0.65;
  filter: blur(0.8px);
}
.pgrid > li.is-hot {
  opacity: 1;
  filter: none;
}

/* Only once JS is driving does the mosaic take a fixed height and explicit
   rows — the two things the hover redistribution needs. Without JS it stays
   an ordinary auto-flowing grid of tiles. */
.pgrid.is-live {
  grid-auto-rows: auto;
  grid-template-rows: repeat(var(--rows), minmax(0, 1fr));
  height: calc(var(--rows) * var(--row-h) + (var(--rows) - 1) * var(--gap));
}

@media (min-width: 700px)  { .pgrid { --cols: 2; } }
@media (min-width: 1180px) { .pgrid { --cols: 3; } }

/* Mobile is deliberately not a shrunk desktop: one column, taller tiles, no
   hover redistribution, and the label always open so every project reads at
   a glance and stays easy to tap. */
@media (max-width: 699px) {
  .pgrid { --gap: var(--space-3); --row-h: clamp(15rem, 62vw, 22rem); }
  .pframe__label { padding: var(--space-4); }
}

.empty-state {
  padding: var(--space-9) var(--space-5);
  text-align: center;
  border: 1px dashed var(--line-strong);
}

/* --- Definition rows (awards, press, specs) ------------------------------- */
.rows { border-top: 1px solid var(--line); }
.row {
  display: grid;
  grid-template-columns: 6rem minmax(0, 1fr) auto;
  gap: var(--space-5);
  align-items: baseline;
  padding-block: var(--space-5);
  border-bottom: 1px solid var(--line);
}
.noir .rows, .noir .row { border-color: var(--line-noir); }
@media (max-width: 640px) {
  .row { grid-template-columns: 4.5rem minmax(0, 1fr); }
  .row > :last-child { grid-column: 2; }
}

/* --- Gallery -------------------------------------------------------------- */
.gallery { display: grid; gap: clamp(1rem, 2.5vw, 2rem); }
.gallery--split { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 640px) { .gallery--split { grid-template-columns: 1fr; } }
.gallery figcaption {
  margin-top: var(--space-3);
  font-size: var(--t-small);
  color: var(--muted-quiet);
}

/* --- CTA band ------------------------------------------------------------- */
.cta { text-align: center; }
.cta__title { max-width: 18ch; margin-inline: auto; }
.cta__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  justify-content: center;
  margin-top: var(--space-7);
}

/* --- Pager (prev/next project) -------------------------------------------- */
.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  padding-block: var(--space-7);
  border-top: 1px solid var(--line);
}

/* ==========================================================================
   09b. FOUNDERS (About)
   --------------------------------------------------------------------------
   The founders' portrait beside the studio prose, on the same 5fr/7fr rag as
   `.section-head` so the block sits on the site's existing editorial indent.

   The frame is given the photograph's own proportions rather than a house
   ratio: the figures reach the top and bottom edges of the plate, so any
   crop would cut through a head or a pair of shoes. `object-fit: cover` on a
   matching ratio is a no-op, which is the point — the photograph is never
   cropped, stretched or re-proportioned at any breakpoint.
   ========================================================================== */

.founders {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--space-7) clamp(2.5rem, 6vw, 6rem);
  align-items: start;
}

.founders__frame {
  --ratio: 3364 / 5056;   /* the plate's own proportions — never cropped */
}

/* The portrait holds while the prose passes it — the same sticky idiom as
   `.section-head--sticky` on the home page. */
@media (min-width: 900px) {
  .founders__media {
    position: sticky;
    top: 7rem;
  }
}

.founders__body > * + * { margin-top: var(--space-5); }

/* The opening paragraph carries the studio's introduction, so it is set one
   step up from the rest — hierarchy without adding another heading. */
.founders__body .body:first-of-type {
  font-size: var(--t-lead);
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 46ch;
}

.founders__body .body { max-width: 56ch; }

@media (max-width: 900px) {
  .founders { grid-template-columns: 1fr; gap: var(--space-6); }
  /* Keeps the portrait from taking a whole phone screen before the text
     starts — the ratio is untouched, only the drawn size is bounded. */
  .founders__media { max-width: 26rem; }
}

/* ==========================================================================
   09c. CONTACT CHANNELS (About)
   --------------------------------------------------------------------------
   Four direct channels across one measure, each under its own hairline —
   the same rule-and-column rhythm the project specs and credits already use.
   No labels: the address, the number and the two platform names identify
   themselves, so nothing has to be said twice.

   The two lists are `display: contents` so their items sit directly in this
   grid. That lets the socials keep coming from fillStudio() — which owns
   [data-studio-social] and writes plain <li><a> — without their markup
   having to differ from the two written by hand.
   ========================================================================== */

.channels {
  display: grid;
  /* One column per channel — email, telephone and the three social links. */
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-6);
  margin-top: clamp(2rem, 5vw, 3.5rem);
}
.channels__set { display: contents; }

.channels li {
  list-style: none;
  padding-top: var(--space-5);
  border-top: 1px solid var(--line);
}
.channels a {
  display: inline-block;
  font-family: var(--font-display);
  font-size: var(--t-h4);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
  overflow-wrap: anywhere;   /* the address must never widen the column */
  transition: color var(--dur) var(--ease-out);
}
.channels a:hover,
.channels a:focus-visible { color: var(--clay-ink); }

/* Five across needs real width — the address is the widest thing here, and
   below this it would start breaking mid-word. */
@media (max-width: 1180px) {
  .channels { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
  .channels { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-5); }
}
@media (max-width: 560px) {
  /* One per line on a phone: a stack of four rules reads as a list rather
     than a cramped grid, and every target is full width. */
  .channels { grid-template-columns: 1fr; gap: 0; }
  .channels li { padding-block: var(--space-4); }
}

/* ==========================================================================
   09d. ABOUT PAGE — COMPACT COMPOSITION
   --------------------------------------------------------------------------
   Spacing and scale only: nothing is removed and no copy is rewritten. The
   page was 2.6 screens tall, and more than a third of that was section
   padding — four sections at 160px top and bottom. Scoped to the About page
   so the rest of the site keeps the roomier editorial rhythm.
   ========================================================================== */

body[data-page="studio"] .section { padding-block: clamp(2.25rem, 4.5vw, 4rem); }

body[data-page="studio"] .founders { gap: var(--space-5) clamp(2rem, 4vw, 3.5rem); }
body[data-page="studio"] .founders__media { max-width: 21rem; }
body[data-page="studio"] .founders__body > * + * { margin-top: var(--space-4); }
/* The opening paragraph keeps its lead role, one step down from before. */
body[data-page="studio"] .founders__body .body:first-of-type {
  font-size: clamp(1.0625rem, 1.05vw, 1.1875rem);
}

/* Seven rows, so the trigger's padding and title size are what the section's
   height is really made of. */
body[data-page="studio"] .service__name { font-size: clamp(1.1875rem, 1.9vw, 1.5rem); }
body[data-page="studio"] .service__trigger {
  padding-block: clamp(0.8rem, 1.5vw, 1.15rem);
  min-height: 2.75rem;
}
body[data-page="studio"] .service__content { padding-bottom: clamp(1rem, 2vw, 1.75rem); }

body[data-page="studio"] .channels { margin-top: clamp(1.25rem, 3vw, 2rem); }

/* ==========================================================================
   09e. SHADER PAGES (Projects, About)
   --------------------------------------------------------------------------
   Both pages sit directly on the Plasma shader from backdrop.js — the same
   canvas that has always been there, untouched. Two things make it read:

   1. The ground under it becomes --noir instead of --bone. It has to go on
      <html>, because the canvas is at z-index -1: above the root background,
      below the page.
   2. The canvas is composited down. The plasma is a warm *light* field, so
      at full strength on a dark page it would simply repaint it light. At
      this opacity the drift and the light variation are legible while the
      page stays black. The shader is not modified — only how much of it
      lands.

   The palette flip is just the semantic tokens restated. Every component
   already draws from them, so nothing else in the stylesheet changes and no
   section gets a background of its own — which is what keeps the field
   continuous, with no seam between sections.
   ========================================================================== */

html:has(body[data-page="projects"]),
html:has(body[data-page="studio"]) { background-color: var(--noir); }

html:has(body[data-page="projects"]) .backdrop,
html:has(body[data-page="studio"]) .backdrop { opacity: 0.2; }

body[data-page="projects"],
body[data-page="studio"] {
  /* Surfaces dissolve so the shader is never covered. */
  --bone: transparent;
  --bone-2: transparent;
  --sand: var(--noir-2);      /* image placeholders */
  --sand-2: var(--noir-2);
  /* Type and rules invert. */
  --ink: var(--on-noir);
  --ink-2: var(--on-noir);
  --muted: var(--on-noir-mut);
  --muted-quiet: var(--on-noir-mut);
  --line: var(--line-noir);
  --line-strong: rgba(242, 238, 231, 0.28);
  --clay-ink: var(--clay);    /* the link accent needs to lift off black */
  --focus: var(--on-noir);
}

/* The mark is black artwork, so on these pages it inverts exactly as it
   already does over the hero and in the footer. */
body[data-page="projects"] .brand__mark img,
body[data-page="studio"] .brand__mark img { filter: invert(1); }

/* ==========================================================================
   09f. ABOUT — SPLIT COMPOSITION  (experimental, reversible)
   --------------------------------------------------------------------------
   [ large portrait ]  [ About text / Services / Contact ]

   Gated entirely on `body[data-about="split"]`, and there is no markup for
   it: the three sections are hoisted into one grid with `display: contents`
   so the portrait and the prose become siblings. Drop the attribute in
   studio.html and every rule below stops matching — the page returns to the
   stacked layout with nothing to undo. Snapshot in
   _backup/about-before-split/.

   Only from 1000px up. Narrower than that the existing stacked layout is
   already the better read, so it is left alone entirely.
   ========================================================================== */

@media (min-width: 1000px) {
  body[data-about="split"] main {
    display: grid;
    /* The portrait column takes its width from the plate; the prose takes
       what is left. */
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: clamp(2.5rem, 5vw, 5rem);
    align-items: start;
    width: 100%;
    max-width: var(--container);
    margin-inline: auto;
    padding: clamp(6rem, 8vw, 7.5rem) var(--gutter) clamp(2.5rem, 4vw, 3.5rem);
  }

  /* The About section, its container and the founders grid all dissolve, so
     the portrait and the prose land directly in the grid above. */
  body[data-about="split"] main > section:first-of-type,
  body[data-about="split"] main > section:first-of-type > .container,
  body[data-about="split"] main > section:first-of-type .founders { display: contents; }

  /* The portrait spans the full height of the column and holds while the
     right-hand column scrolls past it. Sized from the viewport rather than
     the column so it stays a single, uncropped plate. */
  body[data-about="split"] .founders__media {
    grid-column: 1;
    grid-row: 1 / -1;
    position: sticky;
    top: 6.5rem;
    max-width: none;
  }
  body[data-about="split"] .founders__frame {
    height: min(74vh, 46rem);
    width: auto;          /* aspect-ratio derives it — never cropped */
  }

  /* Everything else stacks in the right-hand column, tight. */
  body[data-about="split"] .founders__body,
  body[data-about="split"] main > section#services,
  body[data-about="split"] main > section#contact { grid-column: 2; }

  body[data-about="split"] main > section#services,
  body[data-about="split"] main > section#contact {
    padding-block: 0;
    margin-top: clamp(1.5rem, 2.5vw, 2.25rem);
  }
  /* Seven rows in a narrower column, so the trigger tightens again here. */
  body[data-about="split"] .service__trigger { padding-block: clamp(0.7rem, 1.2vw, 0.95rem); }
  body[data-about="split"] .founders__body > * + * { margin-top: var(--space-3); }
  body[data-about="split"] main > section#services > .container,
  body[data-about="split"] main > section#contact > .container {
    max-width: none;
    padding-inline: 0;
  }

  /* Contact is the last thing in the column, so it holds fewer columns than
     it would at full page width. */
  body[data-about="split"] .channels { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ==========================================================================
   09g. ABOUT — TWO-PLATE SCROLL SWAP  (reversible)
   --------------------------------------------------------------------------
   `about-couple.webp` at rest; one intentional scroll down cross-fades to
   `about-material.webp`, and scrolling back up returns it. Nothing else moves:
   `.frame > img` already absolutely positions both plates inside a frame
   whose height comes from its aspect-ratio, so only `opacity` ever changes
   and the column beside it cannot reflow.

   The reveal rule in §08 also targets these images and its `transition`
   shorthand would replace the whole list, leaving opacity untransitioned and
   the swap snapping — so both properties are declared together here, at a
   specificity that ties `.js [data-reveal="media"] > img` and a position that
   wins.

   Rollback: _backup/about-before-image-swap/
   ========================================================================== */

.js .founders__frame > .founders__plate {
  transition: transform 1500ms var(--ease-out) var(--reveal-delay, 0ms),
              opacity 900ms var(--ease-soft);
}

.founders__plate--alt { opacity: 0; }
[data-about-media].is-second .founders__plate--alt { opacity: 1; }
[data-about-media].is-second .founders__plate:not(.founders__plate--alt) { opacity: 0; }

/* ==========================================================================
   10. FORMS
   ========================================================================== */

.form { display: grid; gap: var(--space-6); }
.form__row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-6); }
@media (max-width: 640px) { .form__row { grid-template-columns: 1fr; } }

.field { display: flex; flex-direction: column; gap: var(--space-2); }

.field__label {
  font-size: var(--t-eyebrow);
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
}
.field__req { color: var(--clay-ink); }

.field__control {
  width: 100%;
  /* ≥3rem tall so it is a comfortable touch target; 16px font stops iOS zoom */
  min-height: 3rem;
  padding: 0.75rem 0;
  font-family: inherit;
  font-size: 1rem;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line-strong);
  border-radius: 0;
  transition: border-color var(--dur) var(--ease-out);
  appearance: none;
}
.field__control::placeholder { color: var(--muted-quiet); }
.field__control:hover { border-color: var(--ink-2); }
.field__control:focus { outline: none; border-color: var(--clay); border-bottom-width: 2px; }
.field__control:focus-visible { outline: 2px solid var(--focus); outline-offset: 4px; }
textarea.field__control { min-height: 8rem; resize: vertical; line-height: 1.6; }
select.field__control {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%236e665b' stroke-width='1.25'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.25rem center;
  background-size: 12px 8px;
  padding-right: 2rem;
  cursor: pointer;
}

.field__help {
  font-size: var(--t-small);
  color: var(--muted-quiet);
}

/* Errors sit directly under their field, carry an icon (never colour alone),
   and are announced to screen readers. */
.field__error {
  display: none;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--t-small);
  color: var(--danger);
}
.field.is-invalid .field__error { display: flex; }
.field.is-invalid .field__control { border-color: var(--danger); }
.field__error svg { width: 1em; height: 1em; flex: none; }

.form__status {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--t-small);
}
.form__status:empty { display: none; }
.form__status[data-tone="error"]   { color: var(--danger); }
.form__status[data-tone="success"] { color: var(--success); }

.form__consent {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--t-small);
  color: var(--muted);
  cursor: pointer;
}
.form__consent input {
  width: 1.125rem;
  height: 1.125rem;
  margin: 0.28rem 0 0;
  accent-color: var(--clay);
  flex: none;
}
/* The label is the real tap target — keep it generous on touch */
.form__consent label {
  cursor: pointer;
  padding-block: var(--space-2);
}

/* ==========================================================================
   11. FOOTER
   ========================================================================== */

.footer {
  background: var(--noir);
  color: var(--on-noir);
  padding-top: clamp(4rem, 8vw, 7rem);
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + var(--space-6));
}
.footer__grid {
  display: grid;
  /* Brand block, then Navigate and Connect */
  grid-template-columns: minmax(0, 5fr) repeat(2, minmax(0, 3.5fr));
  gap: var(--space-7) var(--space-6);
  padding-bottom: clamp(3rem, 6vw, 5rem);
}
@media (max-width: 980px) { .footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { .footer__grid { grid-template-columns: 1fr; } }

/* The brand signature that closes the page. Same crop window as .brand__mark
   — the ink box of a 6250² plate — and the artwork is dark, so it is inverted
   to sit on the noir ground. */
.footer__mark {
  display: block;
  position: relative;
  width: min(100%, 15rem);
  aspect-ratio: 4875 / 775;
  overflow: hidden;
  margin-top: clamp(2rem, 4vw, 3rem);
}
.footer__mark img {
  position: absolute;
  width: 128.205%;               /* 6250 / 4875 */
  left: -14.113%;                /*  -688 / 4875 */
  top: -169.419%;                /* -1313 /  775 */
  max-width: none;
  background: none;
  filter: invert(1);
}

.footer__title {
  font-size: var(--t-eyebrow);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--on-noir-mut);
  margin-bottom: var(--space-5);
}
.footer__list { display: flex; flex-direction: column; gap: var(--space-1); }
.footer__list a,
.footer__list span {
  display: inline-block;
  padding-block: var(--space-2);
  font-size: var(--t-small);
  color: var(--on-noir);
}
.footer__list a { position: relative; }
.footer__list a::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0.4rem;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-slow) var(--ease-out);
}
.footer__list a:hover::after { transform: scaleX(1); }

.footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line-noir);
  font-size: var(--t-small);
  color: var(--on-noir-mut);
}

.to-top {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 2.75rem;
  font-size: var(--t-eyebrow);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.to-top .arrow { transition: transform var(--dur) var(--ease-out); }
.to-top:hover .arrow { transform: translateY(-3px); }

/* ==========================================================================
   12. REDUCED MOTION & PRINT
   ========================================================================== */

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

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  /* Everything renders in its final state — no hidden content, ever */
  .js [data-reveal],
  .js [data-reveal="media"],
  .js [data-reveal="rule"] {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }
  .js [data-reveal="media"] > img { transform: none !important; }
  .js .line__inner { transform: none !important; }
  .marquee__track { animation: none; transform: none; }
  .scroll-cue__track::after { animation: none; }
  .drawer__link, .drawer__foot { opacity: 1; transform: none; }
}

@media print {
  .header, .drawer, .veil, .scroll-cue, .marquee, .to-top { display: none !important; }
  body { background: #fff; color: #000; }
  .hero { min-height: auto; color: #000; background: none; }
  .hero__media { display: none; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.75em; }
}
