/* ==========================================================================
   DWEAR — black & gold luxury system, palette pulled from the 3D logo's metal
   ground #0A0A0B · text #F2EEE5 · gold #D3A24C · bright #EEC57A · surface #121116
   type: Archivo — wide (wdth ~118%) 800 display / normal 400-600 body · Newsreader italic accent
   shape rule: image/card blocks 20px, buttons + tags full pill
   ========================================================================== */

/* Archivo (self-hosted variable, latin subset) — the site's face by request:
   a bold geometric-grotesque sans, uppercase-friendly, and WIDE. This is the same
   Archivo the earlier build rejected for reading ~14% wider than Roboto; that width
   is now the point. One file carries both axes — wght 400-900 and the width axis
   wdth 62-125% (exposed via font-stretch) — so display type widens to ~118% while
   body stays at normal width for readability. Newsreader italic stays the accent. */
@font-face {
  font-family: "Archivo";
  src: url("../fonts/archivo-wide-var.woff2") format("woff2");
  font-weight: 400 900;
  font-stretch: 62% 125%;
  font-display: swap;
}
@font-face {
  font-family: "Newsreader";
  src: url("../fonts/newsreader-it.woff2") format("woff2");
  font-weight: 500;
  font-style: italic;
  font-display: swap;
}

:root {
  /* DWEAR black & gold - palette pulled straight from the logo's metal.
     Token names kept from the old warm system so the cascade flips in place;
     values are now black grounds + the logo's gold. No brown anywhere. */
  --paper: #0A0A0B;        /* primary background (near-black) */
  --ink: #F2EEE5;          /* primary text (warm off-white) */
  --ink-2: #A79E8C;        /* muted text */
  --brown: #D3A24C;        /* PRIMARY GOLD accent (logo mid-gold) */
  --brown-deep: #B9862F;   /* deeper gold */
  --tan: #C79640;          /* gold, mid */
  --peach: #EEC57A;        /* gold, bright highlight */
  --cream: #121116;        /* raised surface (cards, inputs) */
  --line: rgba(211, 162, 76, 0.18);  /* gold hairline */
  --dark: #060607;         /* deepest black (bands, footer) */
  --white: #FFFFFF;
  --gold: #D3A24C;
  --gold-bright: #EEC57A;
  --gold-deep: #B9862F;
  --font-mono: ui-monospace, "Cascadia Mono", "Segoe UI Mono", Consolas, "SF Mono", monospace;
  --r-block: 20px;
  --r-big: 28px;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --shadow-card: 0 22px 50px -22px rgba(0, 0, 0, 0.75);
  --nav-h: 84px;   /* raised from 72 so the bigger logo has room to breathe */
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }

body {
  font-family: "Archivo", "Helvetica Neue", Arial, sans-serif;
  font-stretch: 100%;               /* body stays normal width for readability */
  background: var(--paper);
  color: var(--ink);
  font-size: 1.0625rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img { max-width: 100%; display: block; }
a { color: inherit; }
ul { list-style: none; }

::selection { background: var(--brown); color: var(--white); }

.skip-link {
  position: absolute; left: -999px; top: 0; z-index: 200;
  background: var(--ink); color: var(--white);
  padding: 0.6rem 1.2rem; border-radius: 0 0 12px 0;
}
.skip-link:focus { left: 0; }

[data-nav-sentinel] { height: 32px; margin-bottom: -32px; }

/* page cross-fade */
body { opacity: 1; transition: opacity 0.24s ease; }
body.is-leaving { opacity: 0; }
body.is-entering { animation: page-in 0.4s ease both; }
@keyframes page-in { from { opacity: 0; } to { opacity: 1; } }

/* ---------------------------------------------------------------- layout */
.wrap { max-width: 1200px; margin: 0 auto; padding: 0 clamp(1.25rem, 4vw, 2.5rem); }
.section { padding: clamp(4.5rem, 9vw, 8rem) 0; }
.section--tight { padding: clamp(3rem, 6vw, 5rem) 0; }
/* nav "Services" lands here; keep the heading clear of the fixed nav */
#full-production { scroll-margin-top: var(--nav-h); }

/* ------------------------------------------------------------------ type */
h1, h2, h3 { font-weight: 800; line-height: 1.04; letter-spacing: -0.02em; text-wrap: balance; }

/* Display tier rides Archivo's width axis out to ~118% — this is the wide,
   geometric, uppercase-friendly look the brief asked for. Body inherits normal
   width from <body>; only these display faces widen. Sizes/spacing untouched. */
h1, h2, h3,
.display, .h-page, .h-med, .h-card,
.brand, .mobile-menu a,
.stat-num, .pro-no, .pro-head h2,
.noir-title, .banner-content h3, .cta-band h2, .case-head h2 {
  font-stretch: 118%;
}

.display {
  font-size: clamp(2.5rem, 6.4vw, 4.75rem);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: -0.025em;
}
.h-page { font-size: clamp(2.6rem, 6.8vw, 5rem); font-weight: 800; text-transform: uppercase; }
.h-med { font-size: clamp(1.7rem, 3.4vw, 2.6rem); font-weight: 800; }
.h-card { font-size: clamp(1.25rem, 2vw, 1.5rem); font-weight: 800; }

.lede { font-size: clamp(1.1rem, 1.7vw, 1.3rem); color: var(--ink-2); max-width: 46ch; }
.eyebrow {
  display: inline-block;
  font-size: 0.75rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.16em;
  color: var(--tan); margin-bottom: 1rem;
}
.script-accent {
  font-family: "Newsreader", Georgia, serif;
  font-style: italic; font-weight: 500;
  letter-spacing: 0; text-transform: none;
}

.ph-tag {
  display: inline-block;
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em;
  color: var(--tan); border: 1px dashed var(--tan);
  border-radius: 999px; padding: 0.15rem 0.65rem;
  vertical-align: middle; white-space: nowrap;
}

/* --------------------------------------------------------------- buttons */
.btn {
  display: inline-flex; align-items: center; gap: 0.6rem;
  border-radius: 999px;
  padding: 0.95rem 1.9rem;
  font: 600 0.95rem/1 "Archivo", sans-serif;
  letter-spacing: 0.02em;
  text-decoration: none; cursor: pointer; border: 0;
  transition: transform 0.45s var(--ease), background-color 0.3s ease, color 0.3s ease, box-shadow 0.45s var(--ease);
  will-change: transform;
}
.btn:active { transform: scale(0.97); }
.btn .btn-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: currentColor; opacity: 0.55;
  transition: transform 0.45s var(--ease);
}
.btn:hover .btn-dot { transform: translateX(3px); }

.btn--brown { background: var(--brown); color: var(--white); }
.btn--brown:hover { background: var(--brown-deep); transform: translateY(-2px); box-shadow: var(--shadow-card); }
.btn--peach { background: var(--peach); color: var(--ink); }
.btn--peach:hover { background: #D5C6BC; transform: translateY(-2px); }
.btn--ghost { background: transparent; color: inherit; box-shadow: inset 0 0 0 1.5px currentColor; }
.btn--ghost:hover { transform: translateY(-2px); }

/* ------------------------------------------------------------------- nav */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  height: var(--nav-h);
  transition: background-color 0.35s ease, color 0.35s ease, box-shadow 0.35s ease;
  color: var(--ink);
}
.nav .wrap {
  height: 100%; display: flex; align-items: center; justify-content: space-between; gap: 1rem;
}
.nav.is-solid {
  background: rgba(253, 252, 250, 0.86);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  box-shadow: 0 1px 0 var(--line);
  color: var(--ink);
}
.nav--on-dark:not(.is-solid) { color: var(--white); }

.brand {
  font-weight: 800; font-size: 1.15rem; letter-spacing: 0.04em;
  text-transform: uppercase; text-decoration: none;
}
.brand span { color: var(--tan); }

/* holds the slot at roughly a logo's size until the real file lands */
.brand--logo {
  display: inline-flex; align-items: center;
  min-width: 132px; min-height: 40px;
}
.brand--logo .ph-tag { font-size: 0.75rem; letter-spacing: 0.1em; }
.nav--on-dark:not(.is-solid) .brand--logo .ph-tag {
  color: rgba(255, 255, 255, 0.85); border-color: rgba(255, 255, 255, 0.5);
}
/* the DWEAR mark. Primary: a continuously-rotating transparent WebM (the
   logo floats, no box/shadow). Fallbacks: mp4 on black, then the still png.
   aspect-ratio reserves the width so there is no load-time layout shift. */
.brand-video {
  /* 74px inside an 84px bar keeps ~5px of air top and bottom; the mark floats
     with its own padding inside the frame, so it reads big without touching the
     nav edges. Grown twice on request: 56 -> 64 -> 74 (2026-08-18).
     aspect-ratio matches the new upscaled clip's crop (3488x1584) so no space
     is reserved around it. */
  display: block; height: 74px; width: auto;
  aspect-ratio: 218 / 99;
  object-fit: contain;
  pointer-events: none;
}
.brand-img { display: block; height: 46px; width: auto; }

/* Phones + tablets: swap the rotating video for a transparent PNG still.
   iOS/iPadOS Safari cannot render the VP9-alpha WebM and falls back to the
   MP4, which is on black — so the logo showed as a black box. The PNG is
   genuinely transparent, so the mark floats on the dark nav and the light
   (scrolled) nav alike. Desktop keeps the animation. url() resolves against
   THIS stylesheet, hence ../img. Held smaller than the desktop video so it
   never crowds the burger or the open menu. */
/* Desktop shows the rotating video; the animated still is mobile's stand-in */
.brand-anim, .footer-anim { display: none; }

@media (max-width: 900px) {
  :root { --nav-h: 72px; }
  .brand-video, .brand-img { display: none; }
  /* The 3D logo keeps spinning on the phone. It ships as an animated WebP with
     alpha — a real <img>, not a CSS background, because Safari animates an
     <img> reliably and can be lazy about animated backgrounds. iOS cannot play
     the VP9-alpha WebM at all and silently falls back to the on-black MP4,
     which is where the black box came from. 90 frames / 15fps ≈ 6s per turn,
     looping forever. */
  .brand-anim {
    display: block;
    height: 52px; width: auto;
    /* the crop's own ratio, so the box never reserves empty space */
    aspect-ratio: 218 / 99;
    object-fit: contain;
    pointer-events: none;
  }
  .brand--logo { min-width: 0; }

  /* .footer-brand prefix is load-bearing: the base `.footer-mark-video {
     display: block }` lives further down the file, so an equal-specificity
     rule here would lose on source order. The descendant selector outscores
     it and the video actually hides. */
  .footer-brand .footer-mark-video,
  .footer-brand .footer-mark > img:not(.footer-anim) { display: none; }
  .footer-brand .footer-anim {
    display: block;
    width: 150px; height: auto;
    aspect-ratio: 218 / 99;
    object-fit: contain;
  }
}

/* -------------------------------------------------------- intro preloader
   First load of the home page: a black screen plays the DWEAR logo reveal
   (assets/video/dwear-intro.mp4), then fades to the site. Once per session
   (sessionStorage), driven by the intro block in main.js. The overlay is
   pure black to blend seamlessly with the video's own black background.
   It is also REPLAYABLE: clicking "Home" in the nav runs the reveal again,
   so the hidden state below is a state, not a teardown. */
.intro {
  position: fixed; inset: 0; z-index: 9999;
  display: grid; place-items: center;
  background: #08080A;
  opacity: 1;
  /* appears instantly (a replay must not fade the black in over the page);
     only the exit is timed - see .intro.is-done below */
  transition: opacity 0s, visibility 0s;
  /* failsafe: if main.js never runs, still reveal the site after 6s */
  animation: intro-failsafe 800ms var(--ease) 6s forwards;
}
/* main.js owns the overlay the moment it runs, so the failsafe is only for the
   no-JS case. Leaving it armed would hide a replay behind its finished
   `forwards` fill. */
.js .intro { animation: none; }
@keyframes intro-failsafe { to { opacity: 0; visibility: hidden; pointer-events: none; } }
/* soft gold aura behind the floating logo - the WebM is transparent, so the
   mark reads as suspended in space (no box, no shadow, no ground) */
.intro-glow {
  position: absolute; width: min(80vw, 780px); aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(210, 162, 76, 0.17), rgba(210, 162, 76, 0.05) 42%, transparent 66%);
  filter: blur(30px);
  pointer-events: none;
}
/* The reveal is a transparent WebM, but Safari (iPhone, iPad and macOS) cannot
   decode VP9 alpha and silently falls back to the MP4 — which is rendered on
   PURE BLACK (sampled: 0,0,0 in every corner). On this #08080A overlay that
   read as an obvious black rectangle around the mark.

   `screen` fixes it exactly rather than approximately: screen(0, backdrop)
   returns the backdrop untouched, so every black pixel of the MP4 becomes the
   overlay — and the gold, being far brighter than the near-black behind it,
   comes through unchanged. It also lets .intro-glow's aura show THROUGH the
   footage, which is the "suspended in space" effect the overlay was after.
   Harmless where the alpha WebM does play: transparent pixels contribute
   nothing, and gold over near-black is lifted imperceptibly. */
.intro-video {
  position: relative; width: min(74vw, 460px); height: auto; border: 0;
  mix-blend-mode: screen;
}
/* keep the blend inside the overlay rather than reaching the page behind it */
.intro { isolation: isolate; }
/* hidden: either the reveal has just finished (is-done) or the session already
   saw it and the head script skipped it before first paint (intro-seen).
   visibility, not display, so a replay can bring the same node back. */
.intro.is-done,
.intro-seen .intro { opacity: 0; visibility: hidden; pointer-events: none; }
/* the exit fades, and only then goes invisible */
.intro.is-done { transition: opacity 750ms var(--ease), visibility 0s linear 750ms; }
/* set by the inline head script before first paint */
.intro-active { overflow: hidden; }

.nav-links { display: flex; gap: clamp(1rem, 2.6vw, 2.2rem); }
.nav-links a {
  text-decoration: none; font-weight: 500; font-size: 0.95rem;
  opacity: 0.82; transition: opacity 0.2s ease;
  padding: 0.3rem 0;
}
.nav-links a:hover { opacity: 1; }
.nav-links a[aria-current="page"],
.nav-links a.is-here {
  opacity: 1; font-weight: 700;
  border-bottom: 2px solid var(--tan);
}
.nav-cta { flex-shrink: 0; }
.nav-cta .btn { padding: 0.7rem 1.4rem; font-size: 0.875rem; }

.nav-burger {
  display: none;
  background: none; border: 0; cursor: pointer;
  width: 44px; height: 44px; border-radius: 50%;
  position: relative; color: inherit;
}
.nav-burger span {
  position: absolute; left: 11px; right: 11px; height: 2px;
  background: currentColor; border-radius: 2px;
  transition: transform 0.4s var(--ease), opacity 0.3s ease;
}
.nav-burger span:nth-child(1) { top: 17px; }
.nav-burger span:nth-child(2) { top: 25px; }
body.menu-open .nav-burger span:nth-child(1) { transform: translateY(4px) rotate(45deg); }
body.menu-open .nav-burger span:nth-child(2) { transform: translateY(-4px) rotate(-45deg); }

.mobile-menu {
  position: fixed; inset: 0; z-index: 90;
  background: var(--dark); color: var(--white);
  display: flex; flex-direction: column; justify-content: center;
  padding: 0 clamp(1.5rem, 8vw, 4rem);
  visibility: hidden; opacity: 0;
  transition: opacity 0.4s var(--ease), visibility 0s linear 0.4s;
}
body.menu-open .mobile-menu { visibility: visible; opacity: 1; transition-delay: 0s; }
.mobile-menu a {
  text-decoration: none;
  font-size: clamp(2rem, 9vw, 3rem); font-weight: 800;
  text-transform: uppercase; letter-spacing: -0.02em;
  padding: 0.55rem 0;
  opacity: 0; transform: translateY(22px);
  transition: opacity 0.5s var(--ease), transform 0.5s var(--ease);
}
.mobile-menu a[aria-current="page"] { color: var(--peach); }
body.menu-open .mobile-menu a { opacity: 1; transform: none; }
body.menu-open .mobile-menu a:nth-child(1) { transition-delay: 0.08s; }
body.menu-open .mobile-menu a:nth-child(2) { transition-delay: 0.13s; }
body.menu-open .mobile-menu a:nth-child(3) { transition-delay: 0.18s; }
body.menu-open .mobile-menu a:nth-child(4) { transition-delay: 0.23s; }
body.menu-open .mobile-menu a:nth-child(5) { transition-delay: 0.28s; }
.mobile-menu .menu-note { margin-top: 2rem; color: var(--peach); font-size: 0.9rem; opacity: 0; transition: opacity 0.5s ease 0.35s; }
body.menu-open .mobile-menu .menu-note { opacity: 0.8; }

/* The gold "Contact us" button in the bar is hidden below 860px, so on the two
   devices that can actually place the call, the primary action was demoted to
   the fifth item of a five-item list. This puts the channel this business is
   really reached on back at the bottom of the menu - one row, dialable, not a
   second copy of the Contact link sitting above it. */
.menu-wa {
  display: inline-flex; align-items: center; gap: 0.75rem;
  align-self: flex-start;
  margin-top: 1.5rem;
  padding: 0.85rem 1.3rem;
  border: 1px solid rgba(211, 162, 76, 0.4);
  border-radius: 999px;
  background: rgba(211, 162, 76, 0.07);
  color: var(--ink);
  /* the list above is display type; this must not join it */
  font-size: 1rem; font-weight: 600;
  text-transform: none; letter-spacing: 0;
  transition: opacity 0.5s var(--ease) 0.4s, transform 0.5s var(--ease) 0.4s,
              border-color 0.25s ease, background-color 0.25s ease;
}
.menu-wa svg { width: 20px; height: 20px; flex: none; color: var(--gold); }
.menu-wa strong { font-weight: 700; color: var(--gold-bright); }
.menu-wa:hover, .menu-wa:focus-visible {
  border-color: var(--gold); background: rgba(211, 162, 76, 0.14);
}
/* The menu link rule (.mobile-menu a, clamp 2–3rem) was out-specifying
   .menu-wa's own 1rem, so the WhatsApp pill rendered at headline size and the
   number wrapped onto two lines. Naming both classes lands the intended size,
   and the number (a <strong>) follows it down. */
.mobile-menu .menu-wa { font-size: 1rem; }
.mobile-menu .menu-wa strong { font-size: 1em; }
/* The logo lives in the fixed nav, which sits above the open menu. Centring
   the menu items in the FULL height let the first one (Home) ride up under the
   logo on short viewports. Reserving the nav's height at the top keeps every
   item clear of it, and the menu scrolls if it ever runs long. */
.mobile-menu {
  justify-content: center;
  padding-top: calc(var(--nav-h) + 1.4rem);
  padding-bottom: 1.6rem;
  overflow-y: auto;
}

@media (max-width: 860px) {
  .nav-links, .nav-cta { display: none; }
  .nav-burger { display: block; }
}

/* ------------------------------------------------------------------ hero
   Composition from Home.png: left-aligned display headline over a warm-washed
   factory frame, the standing figure held clear on the right.                */
/* Scrubbed reel, ported from the abdessamad.co hero (same beats, this palette):
   the section is tall, the pin holds one frame still, and everything inside is
   driven by how far through the section you've scrolled. No overflow:hidden
   here - it would kill the sticky child. */
.hero {
  position: relative;
  height: 380vh;
  background: var(--dark);
  view-timeline-name: --hero;
  view-timeline-axis: block;
}
.hero-pin {
  position: sticky; top: 0;
  height: 100vh;            /* fallback first: browsers without dvh never collapse the pin */
  height: 100dvh;
  overflow: hidden;
  display: flex; align-items: center;
  color: var(--white);
  isolation: isolate;
  /* Backstop so the hero can never be an empty grey box: the pin paints the
     factory photo behind every child, independent of the scrubbed .hero-media
     layer. If that layer's <img> is slow, fails, or a browser mishandles the
     scroll-timeline transform, the factory frame still fills the section. The
     <img> above covers this pixel-for-pixel whenever it renders. */
  background: var(--dark) center / cover no-repeat url("../img/hero-wide.jpg");
}
@media (max-width: 900px) { .hero-pin { background-image: url("../img/hero-tall.jpg"); } }
/* NOTE: every scrubbed rule below sets `animation-timeline` and
   `animation-range` as longhands AFTER its `animation` shorthand, and never via
   a shared blanket selector. Both matter: the shorthand resets
   animation-timeline to `auto`, and a blanket selector outranks a single class,
   so a shared rule silently overrides each element's own range. */

.hero-media { position: absolute; inset: 0; z-index: -2; }
.hero-photo, .hero-photo img { width: 100%; height: 100%; display: block; }
.hero-photo img { object-fit: cover; object-position: 0% 46%; }

/* the pull-out: opens tight on Abdessamad at the press, retreats to the whole
   floor with him standing in it - the mockup framing is the END state.
   Origin sits on him deliberately: at 2.05x this source is soft, and a figure
   stays legible when soft where machinery and steam turn to mush. */
.hero-media {
  transform-origin: 82% 52%;
  animation: hero-ken linear both;
  animation-timeline: --hero;
  animation-range: contain 0% contain 100%;
  will-change: transform;
}
@keyframes hero-ken {
  from { transform: scale(2.05); animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1); }
  to { transform: scale(1); }
}

/* fine warm grain over the frame: gives the upscaled source texture to sit on
   and keeps the punch-in from reading as a soft JPEG */
.hero-pin::after {
  content: "";
  position: absolute; inset: 0; z-index: 5;
  pointer-events: none;
  opacity: 0.5;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}

/* letterbox: the reason the frame doesn't show all of itself at rest */
.hero-bar {
  position: absolute; left: 0; right: 0; z-index: 6;
  height: 12vh; background: var(--dark);
}
.hero-bar--top {
  top: 0;
  animation: bar-open-top linear both;
  animation-timeline: --hero;
  animation-range: contain 0% contain 100%;
}
.hero-bar--bot {
  bottom: 0;
  animation: bar-open-bot linear both;
  animation-timeline: --hero;
  animation-range: contain 0% contain 100%;
}
@keyframes bar-open-top { 0%, 2% { transform: translateY(0); } 18%, 100% { transform: translateY(-101%); } }
@keyframes bar-open-bot { 0%, 2% { transform: translateY(0); } 18%, 100% { transform: translateY(101%); } }

/* the location stamp that sat here (PRINT FLOOR / GUANGDONG, CN) was removed
   2026-08-10 by request, along with its boot-fade keyframe. The mono voice it
   spoke lives on in the brief's part codes at the end of this file. */

/* scroll cue */
.hero-cue {
  position: absolute; z-index: 10;
  bottom: 14vh; left: 50%;
  display: flex; flex-direction: column; align-items: center; gap: 0.8rem;
  font: 500 0.7rem/1 var(--font-mono);
  letter-spacing: 0.24em; color: rgba(255, 255, 255, 0.8);
  animation: cue-fade linear both;
  animation-timeline: --hero;
  animation-range: contain 0% contain 100%;
}
@keyframes cue-fade {
  0%, 2% { opacity: 1; transform: translateX(-50%); }
  9%, 100% { opacity: 0; transform: translateX(-50%); }
}
.cue-line {
  width: 1px; height: 42px;
  background: linear-gradient(to bottom, var(--peach), transparent);
  animation: cue-pulse 2.2s var(--ease) infinite;
  animation-timeline: auto;
}
@keyframes cue-pulse {
  0%, 100% { opacity: 0.35; transform: scaleY(0.6); transform-origin: top; }
  50% { opacity: 1; transform: scaleY(1); transform-origin: top; }
}

/* warm wash, tuned against Home.png: brown over the whole frame, a scrim
   pooled behind the copy so white type still clears AA, and a fade into the
   stats strip below */
.hero-wash {
  position: absolute; inset: 0; z-index: -1;
  background:
    radial-gradient(125% 88% at 6% 54%, rgba(6, 6, 8, 0.52) 0%, rgba(6, 6, 8, 0.30) 40%, rgba(6, 6, 8, 0) 76%),
    linear-gradient(to bottom, rgba(6, 6, 8, 0.24) 0%, rgba(6, 6, 8, 0) 16%, rgba(6, 6, 8, 0) 60%, rgba(10, 10, 11, 0.78) 100%),
    rgba(8, 8, 10, 0.30);
}
/* deepens on scroll so the frame hands off softly into the stats strip.
   Its own layer because opacity composites; animating filter would repaint. */
.hero-wash::after {
  content: "";
  position: absolute; inset: 0;
  background: rgba(22, 14, 7, 0.9);
  opacity: 0;
}
/* no load-time settle: the Ken-Burns above owns this frame's scale */

.hero-inner {
  position: relative; z-index: 8; width: 100%;
  padding-top: calc(var(--nav-h) + 2rem);
  padding-bottom: 3rem;
}
/* the whole block lifts away once the reel is done */
.hero-copy {
  max-width: 78%;
  animation: hero-exit linear both;
  animation-timeline: --hero;
  animation-range: contain 86% contain 98%;
  will-change: transform, opacity;
}
@keyframes hero-exit {
  from { opacity: 1; transform: translateY(0); }
  to { opacity: 0; transform: translateY(-60px); }
}

/* headline assembles line by line as the frame pulls out.
   Ranges are the original's 0.07 + index * 0.085, each running 0.13. */
.hero h1 .line-mask > span {
  animation: line-rise linear both;
  animation-timeline: --hero;
  will-change: transform;
}
.hero h1 .line-mask:nth-child(1) > span { animation-range: contain 7% contain 20%; }
.hero h1 .line-mask:nth-child(2) > span { animation-range: contain 15.5% contain 28.5%; }
.hero h1 .line-mask:nth-child(3) > span { animation-range: contain 24% contain 37%; }
@keyframes line-rise {
  from { transform: translateY(112%); opacity: 0; }
  46% { opacity: 1; }              /* original fades over 0.06 of the 0.13 rise */
  to { transform: translateY(0); opacity: 1; }
}

.hero-sub {
  animation: rise-in linear both;
  animation-timeline: --hero;
  animation-range: contain 38% contain 48%;
}
.hero-actions {
  animation: rise-in linear both;
  animation-timeline: --hero;
  animation-range: contain 48% contain 58%;
}
@keyframes rise-in {
  from { opacity: 0; transform: translateY(30px); }
  to { opacity: 1; transform: translateY(0); }
}
.hero h1 {
  font-size: clamp(2.15rem, 4.7vw, 4.3rem);
  font-weight: 800; text-transform: uppercase;
  letter-spacing: -0.025em; line-height: 1.02;
  text-shadow: 0 2px 28px rgba(20, 12, 6, 0.45);
}
.hero .hero-sub {
  margin: 1.3rem 0 2.2rem;
  font-size: clamp(1.05rem, 1.7vw, 1.35rem);
  font-weight: 600; max-width: 40ch;
  color: rgba(255, 255, 255, 0.94);
  text-shadow: 0 2px 18px rgba(20, 12, 6, 0.5);
}
.hero-actions { display: flex; gap: 0.9rem; flex-wrap: wrap; }
@media (max-width: 900px) {
  .hero-copy { max-width: 100%; }
  .hero-photo img { object-position: 62% 40%; }
}
/* The reel is scrubbed by scroll distance, and a thumb covers far less ground
   per swipe than a wheel does - 380vh on a phone is four screens of swiping,
   and the last one is dead: the headline has already exited at 98% and the
   next section hasn't arrived. Shortening the track keeps every beat (the
   ranges are percentages of it) and cuts the dead screen. */
@media (max-width: 1024px) { .hero { height: 320vh; } }
@media (max-width: 640px)  { .hero { height: 250vh; } }

/* the wash deepens as the reel plays out, easing into the section below */
.hero-wash::after {
  animation: hero-deepen linear both;
  animation-timeline: --hero;
  animation-range: contain 0% contain 100%;
}
@keyframes hero-deepen { 0%, 60% { opacity: 0; } 100% { opacity: 0.24; } }

/* line-mask headline reveal */
.line-mask { display: block; overflow: hidden; padding-bottom: 0.08em; margin-bottom: -0.08em; }
.line-mask > span { display: block; }
.js .line-mask > span { transform: translateY(112%); }
.js .line-mask.is-in > span,
.js .is-in .line-mask > span { transform: none; transition: transform 0.9s var(--ease); }
.js .line-mask:nth-child(2).is-in > span { transition-delay: 0.1s; }
.js .line-mask:nth-child(3).is-in > span { transition-delay: 0.2s; }

/* --------------------------------------------------------------- reveals
   Rise + fade as each block enters, children staggered by --i. Fires once. */
.js .reveal {
  opacity: 0; transform: translateY(30px);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
  transition-delay: calc(var(--i, 0) * 90ms);
}
.js .reveal.is-in { opacity: 1; transform: none; }

/* ------------------------------------------------------- figures
   The home stats strip was removed 2026-07-17; these two survive because the
   320K case study's .case-stats still uses them. */
.stat-num {
  font-size: clamp(2.4rem, 4.6vw, 3.8rem);
  font-weight: 800; letter-spacing: -0.03em; line-height: 1;
  color: var(--brown);
  font-variant-numeric: tabular-nums;
}
.stat-label { margin-top: 0.5rem; color: var(--ink-2); font-size: 0.95rem; font-weight: 500; }

/* --------------------------------------------------- banner (take it from here) */
.banner-block {
  position: relative; border-radius: var(--r-big); overflow: hidden;
  min-height: clamp(320px, 44vw, 460px);
  display: flex; align-items: center; justify-content: center; text-align: center;
  color: var(--white); isolation: isolate;
}
/* live footage behind the copy; the poster (banner.jpg) holds the frame until
   the video arrives, and main.js pauses it while the block is offscreen */
.banner-block .banner-video {
  position: absolute; inset: 0; z-index: -1;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
.banner-block::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background: rgba(30, 20, 12, 0.52);
}
.banner-content { position: relative; z-index: 1; padding: 2rem; }
.banner-content h3 {
  font-size: clamp(1.9rem, 4.6vw, 3.2rem);
  font-weight: 800; text-transform: uppercase; letter-spacing: -0.02em;
}
.banner-content p { margin: 0.7rem 0 1.6rem; font-weight: 500; color: rgba(255,255,255,0.9); }

/* -------------------------------------------- service cards: noir field
   Dark canvas, five cards absolutely placed, drifting on desynchronised
   Lissajous orbits and turning magnetic under the cursor. The physics live
   in main.js, and the JS owns .noir-card's transform every frame - so the
   entry reveal sits on .noir-slot, keeping the two transforms apart. */
.noir-band {
  position: relative;
  isolation: isolate;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(206, 158, 74, 0.16) 0%, rgba(14, 12, 10, 0) 55%),
    #0E0C0A;
  color: var(--white);
  padding: clamp(4rem, 8vw, 6.5rem) 0 clamp(3rem, 6vw, 5rem);
  overflow-x: clip; /* drifting cards at the edges must not widen the page */
}
.noir-band .wrap { position: relative; z-index: 1; }
/* heading centered over whatever fills in behind it; the shadow keeps it
   legible on the bright showroom photo without dimming the image */
.noir-band .display,
.noir-band .lede {
  text-align: center;
  margin-left: auto; margin-right: auto;
  text-shadow: 0 2px 26px rgba(0, 0, 0, 0.6);
}
.noir-band .lede { color: rgba(255, 255, 255, 0.62); }

/* hover fill: the hovered card's photo takes the whole wall behind the cards */
.noir-fill { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.noir-fill-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 500ms cubic-bezier(0.16, 1, 0.3, 1);
}
.noir-fill-img.is-on { opacity: 1; }

/* Canvas breaks out wider than the text column so five 480px cards float with
   real air, and it's tall enough that no two baselines share vertical space.
   Positions are fixed px (not %) so widening the canvas only grows the middle
   gaps - it can never push a card back onto its neighbour. Every same-column
   pair keeps a >=110px vertical gap; the drift (12px) + magnet (45px) + spring
   overshoot worst case is ~62px, so the cards can't touch. Verified with the
   envelope-collision script. */
.noir-canvas {
  position: relative;
  width: min(1280px, 92vw);
  margin-left: 50%;
  transform: translateX(-50%);
  height: 1305px;
  margin-top: clamp(2rem, 4vw, 3rem);
}
/* baselines: the anchors the physics orbit around and spring back to.
   Cards grew 440x310 -> 480x340 (2026-08-13); the vertical baselines moved
   down with them so every same-column gap is unchanged (>=110px), and the
   3/4 insets tightened 40px -> 20px so the widest pair still clears 105px
   at the 1201px worst case - above the ~64px the physics can close. */
.noir-slot { position: absolute; width: 480px; }
.noir-slot--1 { top: 0;     left: 0; }
.noir-slot--2 { top: 60px;  right: 0; }
.noir-slot--3 { top: 460px; left: 20px; }
.noir-slot--4 { top: 510px; right: 20px; }
.noir-slot--5 { top: 950px; left: 50%; margin-left: -240px; }

.noir-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  height: 340px;
  overflow: hidden;
  border-radius: 10px;
  border: 1px solid #27272A;              /* zinc-800, per spec */
  background: rgba(0, 0, 0, 0.8);
  padding: clamp(1.3rem, 2.2vw, 1.8rem) clamp(1.3rem, 2.2vw, 1.7rem);
  text-decoration: none;
  color: var(--white);
  box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 0.8);
  will-change: transform;                 /* main.js writes translate3d here */
}
.noir-bg {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
/* gradient veil: darkest under the text. Gradients can't tween, so hover
   lightens the LAYER's opacity - stays inside the transform/opacity rule */
.noir-veil {
  position: absolute; inset: 0;
  background: linear-gradient(to top,
    rgba(10, 8, 7, 0.86) 0%,
    rgba(10, 8, 7, 0.52) 48%,
    rgba(10, 8, 7, 0.30) 100%);
  transition: opacity 400ms cubic-bezier(0.16, 1, 0.3, 1);
}
.noir-card:hover .noir-veil { opacity: 0.45; }
.noir-body { position: relative; display: block; }
.noir-title {
  display: block;
  font-size: 19px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.2em;
  line-height: 1.3;
  padding-bottom: 6px; margin-bottom: 12px;
  border-bottom: 1px solid #3F3F46;       /* zinc-700 */
}
/* the punch line under each card's name - it was 13px regular, which on a
   480px floating card read as a caption rather than the thing that sells it */
.noir-text {
  display: block;
  font-size: 15.5px; font-weight: 600;
  letter-spacing: 0.02em; line-height: 1.55;
  color: #C8C6C0;
}

/* Below 1201px the wrap is too narrow to float five 480px cards without
   crowding, so the absolute field is off - the breakout, the height and the
   slot offsets all reset here, and each width below picks its own layout. */
@media (max-width: 1200px) {
  .noir-canvas {
    width: 100%; margin-left: 0; transform: none;
    height: auto;
  }
  .noir-slot { position: static; width: 100%; margin-left: 0; top: auto; left: auto; right: auto; }
  .noir-card { height: auto; min-height: 240px; }
}

/* ---- Individual services on tablet (641-1200)
   A tablet has room for two cards, and stacking five full-bleed ones threw
   away half the width for no gain. Two-up, with the right column dropped half
   a card - the field is meant to read as five things floating at their own
   heights, and a flush 2x2 grid is the one arrangement that says the opposite.
   The last card takes the full width on its own: five into two leaves one
   over, and a lone half-width card at the end reads as a mistake. */
@media (max-width: 1200px) and (min-width: 641px) {
  .noir-canvas {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(1rem, 2.5vw, 1.6rem);
    align-items: start;
  }
  .noir-slot:nth-child(even) { margin-top: clamp(2rem, 5vw, 3.5rem); }
  .noir-slot:last-child { grid-column: 1 / -1; margin-top: 0; }
  .noir-card { height: clamp(280px, 34vw, 360px); min-height: 0; }
  .noir-slot:last-child .noir-card { height: clamp(260px, 30vw, 320px); }
}

/* ---- Individual services on phone (<=640): the swipe rail
   These five are alternatives, not a sequence - you take the one piece that's
   breaking. The desktop says that by scattering them in space; a vertical
   stack says the opposite, that there's a first one and a last one, and it
   spent 1540px doing it. A rail says it in the phone's own gesture: five
   parallel things, one at a time, none of them ranked.

   The card to the right stays half in frame on purpose. That peek is the only
   thing telling a thumb there is more, so the slot is sized to guarantee it
   rather than to fill the screen. */
.noir-rail { display: none; }   /* phone affordance only; above 640 all five are in view */
@media (max-width: 640px) {
  .noir-canvas {
    display: flex;
    gap: 12px;
    /* auto, not the `width: 100%` inherited from the <=1200 block: an explicit
       100% resolves against .wrap's 350px content box, so the negative margins
       below moved the rail left without ever widening it - it stayed 350 wide
       inside a 390 viewport and the peek lost 40px */
    width: auto;
    /* full bleed: the rail has to touch both edges or the peek reads as a
       cropped card instead of the next one waiting */
    margin-inline: calc(50% - 50vw);
    padding-inline: clamp(1.25rem, 4vw, 2.5rem);
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: clamp(1.25rem, 4vw, 2.5rem);
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    overscroll-behavior-x: contain;
  }
  .noir-canvas::-webkit-scrollbar { display: none; }
  .noir-slot {
    /* vw, not %: a percentage basis resolves against the scroller's content
       box, so the gutters would eat into the card and the peek would drift
       with them. Against the viewport the peek is the same on every phone. */
    flex: 0 0 min(78vw, 310px);
    scroll-snap-align: start;
  }
  .noir-card { height: 400px; min-height: 0; }
  .noir-title { font-size: 17px; letter-spacing: 0.16em; }

  /* The reveal observer wants 18% of an element in view before it fades it in.
     The peeking card shows 54px of 304 - 17.8%, just under - so the one card
     whose entire job is to advertise the swipe was the one card sitting at
     opacity 0, and the rail read as a single card with dead space beside it.
     In a rail the five arrive as one object anyway, so they opt out of the
     per-card reveal and come in with the section around them. */
  .js .noir-canvas .reveal { opacity: 1; transform: none; }

  /* the veil is tuned for a 480px-wide card; on a 300px one the copy sits
     lower in the frame and needs the ground brought up with it */
  .noir-veil {
    background: linear-gradient(to top,
      rgba(10, 8, 7, 0.92) 0%,
      rgba(10, 8, 7, 0.58) 42%,
      rgba(10, 8, 7, 0.26) 100%);
  }

  /* the position rail: the same gold fill the brief's completion bar uses, so
     "how far along am I" is one idea across the site rather than two */
  .noir-rail {
    display: block;
    height: 2px;
    margin: 1.5rem auto 0;
    width: min(140px, 40%);
    background: rgba(211, 162, 76, 0.2);
    border-radius: 999px;
    overflow: hidden;
  }
  .noir-rail-fill {
    display: block; height: 100%; width: 100%;
    background: linear-gradient(90deg, var(--gold-deep), var(--gold-bright));
    border-radius: 999px;
    transform-origin: left center;
    /* one card of five until main.js reports the real scroll position, so the
       rail is never empty and never wrong with JS off */
    transform: scaleX(0.2);
    transition: transform 220ms var(--ease);
  }
}

/* --------------------------------------------- service detail view (home)
   Slides in over the noir grid when a card is clicked - same page, no
   reload (engine: the svc-view block in main.js). The layout mirrors
   services.html's .svc-detail (title + punch left, description right)
   with the card's own photo underneath as the 3D-tilt image. Colors are
   the noir band's own: #0E0C0A wall, #3F3F46 hairlines, #A1A1AA body. */
.svc-view {
  /* fixed and fullscreen on purpose: while a service is open it is the
     ONLY thing on screen - no hero, no Full Production, no testimonials.
     The engine locks page scroll behind it (html.svc-open + lenis.stop) */
  position: fixed; inset: 0; z-index: 250;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overscroll-behavior: contain;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(206, 158, 74, 0.16) 0%, rgba(14, 12, 10, 0) 55%),
    #0E0C0A;
  opacity: 0;
  transform: translateY(28px);
  pointer-events: none;
  transition: opacity 500ms var(--ease), transform 500ms var(--ease);
}
/* column flex + auto vertical margins: centres the panel when it fits, and
   when it's taller than the viewport the margins collapse so it anchors to
   the TOP (back button always reachable) and the rest scrolls - never clips */
.svc-view > .wrap { margin: auto; width: 100%; padding-top: clamp(1.5rem, 4vh, 3rem); padding-bottom: clamp(1.5rem, 4vh, 3rem); }
html.svc-open { overflow: hidden; }
.noir-band.is-detail .svc-view { opacity: 1; transform: none; pointer-events: auto; }
.noir-band > .wrap { transition: opacity 500ms var(--ease), transform 500ms var(--ease); }
.noir-band.is-detail > .wrap { opacity: 0; transform: translateY(-18px); pointer-events: none; }
/* static mode: show/hide without the slide */
.noir-band.svc-static .svc-view,
.noir-band.svc-static > .wrap { transition: none; }

.svc-view-back {
  display: inline-block;
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 0.85rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.16em;
  color: #A1A1AA;
  padding: 0.4rem 0;
  margin-bottom: clamp(1.2rem, 2.5vw, 2rem);
  transition: color 300ms var(--ease), transform 300ms var(--ease);
}
.svc-view-back:hover { color: var(--white); transform: translateX(-4px); }
.svc-view-back:focus-visible { outline: 2px solid var(--peach); outline-offset: 4px; }

.svc-view-grid {
  display: grid; grid-template-columns: 0.9fr 1.4fr;
  gap: clamp(1.5rem, 4vw, 4rem);
  align-items: start;
}
.svc-view-title { font-size: clamp(1.7rem, 3vw, 2.4rem); text-transform: uppercase; }
.svc-view-punch {
  margin-top: 0.6rem; font-weight: 800; color: var(--peach);
  font-size: clamp(1.05rem, 1.8vw, 1.3rem);
}
.svc-view-text { color: #A1A1AA; max-width: 58ch; line-height: 1.7; }

/* the service's own photo, framed like the cards; the wrapper is the 3D
   stage and the tilt engine in main.js owns the img transform */
.svc-view-media {
  margin: clamp(1.8rem, 3.5vw, 2.8rem) auto 0;
  width: min(760px, 100%);
  perspective: 1000px;
  transform-style: preserve-3d;
}
.svc-view-img {
  /* 16:9 frame for every service (his explicit ask). A consistent landscape
     crop keeps each detail short enough to fit the viewport, so the back
     button and image stay visible. object-fit:cover fills the frame from
     the portrait sources; object-position centres the subject band. */
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  height: auto;
  object-fit: cover;
  object-position: center;
  border-radius: 10px;
  border: 1px solid #3F3F46;
  will-change: transform;
  box-shadow: 0 30px 70px -20px rgba(0, 0, 0, 0.7);
}

@media (max-width: 820px) {
  .svc-view-grid { grid-template-columns: 1fr; gap: 0.9rem; }
}

/* ------------------------------------------------- process: dark carousel
   Tall section, sticky pin, cards springing through the centre - engine is
   the .pro block in main.js, which owns .pro-card transform/opacity and the
   .pro-media tilt every frame. Slim fixed progress bar up top; the header
   fades as the first card arrives. */
.pro {
  position: relative;
  height: calc(100vh + 12 * 62vh);
  background:
    radial-gradient(120% 80% at 50% 0%, rgba(206, 158, 74, 0.18) 0%, rgba(14, 12, 10, 0) 55%),
    #0E0C0A;
  color: var(--white);
}
.pro-pin {
  position: sticky; top: 0;
  height: 100vh; height: 100svh;
  overflow: hidden;
  perspective: 1800px;   /* the 3D stage the coverflow rotates inside */
}
/* ambient glow drifting behind the cards */
.pro-glow {
  position: absolute; left: 50%; top: 50%;
  width: 60vw; height: 60vh;
  margin: -30vh 0 0 -30vw;
  background:
    radial-gradient(closest-side at 35% 40%, rgba(206, 158, 74, 0.75), transparent 70%),
    radial-gradient(closest-side at 70% 65%, rgba(238, 197, 122, 0.35), transparent 65%);
  filter: blur(80px);
  animation: pro-glow-drift 16s ease-in-out infinite alternate;
  pointer-events: none;
}
@keyframes pro-glow-drift {
  from { transform: translate(-6%, -4%) scale(1); }
  to { transform: translate(6%, 5%) scale(1.18); }
}
/* slim progress bar at the very top, visible only while the carousel holds */
.pro-progress {
  position: fixed; top: 0; left: 0; right: 0; height: 3px;
  z-index: 130;
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
}
.pro-progress-fill {
  display: block; width: 100%; height: 100%;
  background: linear-gradient(90deg, var(--brown), var(--peach));
  transform: scaleX(0);
  transform-origin: left center;
}
.pro-head {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
  pointer-events: none;
}
.pro-head h2 {
  font-size: clamp(2.5rem, 6.4vw, 4.75rem);
  font-weight: 800; text-transform: uppercase; letter-spacing: -0.025em;
}
.pro-head p { margin-top: 0.8rem; color: #A1A1AA; }

/* coverflow: every step is one dark rounded panel (image left, copy right).
   The active panel faces forward; JS rotates the neighbours away on either
   side and dims them by distance. transform-origin centre so they pivot in
   place. */
.pro-card {
  position: absolute; left: 50%; top: 50%;
  transform-origin: center center;
  transform: translate(-50%, -50%);
  width: min(1000px, 92vw);
  display: grid; grid-template-columns: 0.82fr 1fr;
  gap: clamp(1.6rem, 3vw, 2.8rem);
  align-items: center;
  padding: clamp(1.1rem, 1.8vw, 1.5rem);
  background: #050505;
  border: 1px solid #26211D;
  border-radius: 26px;
  box-shadow:
    0 50px 110px -30px rgba(0, 0, 0, 0.85),
    0 0 0 1px rgba(0, 0, 0, 0.4);
  opacity: 0; visibility: hidden;
  backface-visibility: hidden;
  will-change: transform, opacity;
}
.pro-copy { padding-right: clamp(0.5rem, 1.5vw, 1.4rem); }
.pro-no {
  font-size: clamp(2.6rem, 4.4vw, 3.6rem); font-weight: 800;
  color: var(--peach); line-height: 1; letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}
.pro-copy h3 { font-size: clamp(1.5rem, 2.6vw, 2.1rem); margin: 0.5rem 0 1rem; }
/* set heavier and larger than the 0.95rem/regular it was: on a card that holds
   the stage on its own, this paragraph is the step, not a caption for it */
.pro-text {
  color: rgba(242, 238, 229, 0.82);
  font-weight: 600;
  max-width: 40ch; line-height: 1.62;
  font-size: clamp(1.06rem, 1.3vw, 1.22rem);
}

/* the image lives in its own rounded frame inside the panel (image left) */
.pro-media {
  order: -1;
  position: relative;
  border-radius: 18px;
  overflow: hidden;
  background: #C9C6C1;
}
/* Steps 08/11/12 carry looping factory clips instead of stills; the video sits
   in exactly the same frame as an img so the card geometry never shifts. */
.pro-media img,
.pro-media video {
  width: 100%; height: clamp(320px, 56vh, 520px);
  object-fit: cover; display: block;
}

/* Step 03's tech pack is a DOCUMENT, not a photo: the whole page has to be
   readable, so it is contained rather than cropped, and it sits on white so the
   letterbox reads as the sheet itself. The veil is dropped here too - a dark
   gradient over a measurement table just makes the numbers harder to read. */
.pro-media--doc { background: #FFFFFF; }
.pro-media--doc img { object-fit: contain; }
.pro-media--doc .pro-veil { display: none; }
.pro-veil {
  position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(10, 8, 7, 0.28), rgba(10, 8, 7, 0));
}

/* focus glow: a soft brown/peach halo pinned behind whichever card is
   centred (the active card always sits at the stage centre); proFrame
   fades it in once the header hands off to the deck, opacity only */
.pro-card-glow {
  position: absolute; left: 50%; top: 50%;
  width: 400px; height: 400px;
  margin: -200px 0 0 -200px;
  border-radius: 50%;
  background:
    radial-gradient(closest-side, rgba(238, 197, 122, 0.5), rgba(206, 158, 74, 0.4) 55%, transparent 78%);
  filter: blur(80px);
  opacity: 0;
  z-index: 5;
  pointer-events: none;
}

/* image-tilt stage: the card supplies the perspective so the framed media
   can lean toward the cursor; the media transform itself is owned by the
   tilt engine in main.js (never by proFrame, which owns the card) */
.pro-card { perspective: 1000px; }
.pro-media { transform-style: preserve-3d; will-change: transform; }

/* ----------------------------------------------------- process: scroll cue
   The deck is driven by VERTICAL scroll but travels HORIZONTALLY, and nothing
   on screen said so - readers landed on step 01 and waited for it to advance
   by itself. So the instruction is drawn rather than written: a dot falling
   down a gold hairline, which is the gesture, with the step counter beside it
   so the reader can see there are eleven more behind this one. proFrame owns
   the opacity and the current number; everything else is CSS.
   Parked below the card - the tallest card is 383px of a 720px stage, so the
   cue sits in clear air - and lifted above the deck's z-index so a neighbour
   rotating past cannot cut through it. */
.pro-cue {
  position: absolute; left: 50%; bottom: clamp(2rem, 7vh, 4.5rem);
  transform: translateX(-50%);
  z-index: 210;
  display: flex; flex-direction: column; align-items: center; gap: 0.6rem;
  opacity: 0;
  pointer-events: none;
}
/* the hairline the dot falls down: fades out at both ends so it reads as a
   path rather than a 30px stick */
.pro-cue-rail {
  position: relative;
  --fall: 28px;   /* dot travel, kept a touch short of the rail's own height */
  width: 1px; height: 30px;
  background: linear-gradient(to bottom,
    rgba(211, 162, 76, 0), rgba(211, 162, 76, 0.5), rgba(211, 162, 76, 0));
}
.pro-cue-rail::after {
  content: ""; position: absolute; left: 50%; top: 0;
  width: 6px; height: 6px; margin-left: -3px;
  border-radius: 50%;
  background: var(--gold-bright);
  box-shadow: 0 0 12px 3px rgba(238, 197, 122, 0.55);
  animation: pro-cue-fall 2.1s cubic-bezier(0.45, 0, 0.4, 1) infinite;
}
@keyframes pro-cue-fall {
  0%   { transform: translateY(-4px); opacity: 0; }
  14%  { opacity: 1; }
  84%  { opacity: 1; }
  100% { transform: translateY(var(--fall, 28px)); opacity: 0; }
}
.pro-cue-read {
  display: flex; align-items: center; gap: 0.65rem;
  font-size: 0.68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.2em;
  color: rgba(242, 238, 229, 0.6);
  white-space: nowrap;
}
.pro-cue-dot {
  width: 3px; height: 3px; border-radius: 50%;
  background: rgba(211, 162, 76, 0.55);
}
/* tabular so the counter does not jitter sideways as it ticks 01 -> 12 */
.pro-cue-count { font-variant-numeric: tabular-nums; letter-spacing: 0.14em; }
.pro-cue-now { color: var(--gold-bright); }

/* no-JS the deck is already a plain stacked list - there is nothing to scroll
   THROUGH, so the instruction would be a lie */
html:not(.js) .pro-cue { display: none; }

@media (max-width: 700px) {
  .pro-cue { gap: 0.5rem; bottom: 1.2rem; }
  .pro-cue-rail { height: 24px; --fall: 22px; }
  .pro-cue-read { font-size: 0.62rem; letter-spacing: 0.16em; gap: 0.5rem; }
}

/* no-JS fallback: a plain stacked list instead of an empty black wall */
html:not(.js) .pro { height: auto; }
html:not(.js) .pro-pin { height: auto; overflow: visible; padding: 3rem 0; perspective: none; }
html:not(.js) .pro-head { position: static; }
html:not(.js) .pro-card {
  position: static; transform: none; opacity: 1; visibility: visible;
  margin: 2.4rem auto;
}

/* A tablet still has room for the panel's two columns (image left, copy right),
   and that reads far better than one full-width letterbox frame - so the stack
   only kicks in on phones. The media frame's own sizing lives in the responsive
   block at the end of this file. */
/* Below ~1100px the card owns the screen and main.js parks the neighbours
   off-stage (see proGeometry): one step is on screen at a time, so the long
   travel that let a desktop reader watch three cards turn past each other is
   buying nothing here but scrolling. Ordered before the phone block below so
   the narrower rule still wins. */
@media (max-width: 1024px) { .pro { height: calc(100vh + 12 * 54vh); } }

@media (max-width: 700px) {
  /* 72vh a step was 844vh of track - seventeen screens of thumb for twelve
     steps, and the deck is a spring, so it arrives long before the scroll
     does. 46vh still gives each card its own beat and its own settle. */
  .pro { height: calc(100vh + 12 * 46vh); }
  .pro-card {
    grid-template-columns: 1fr; gap: 1.1rem; width: 90vw;
    padding: 1rem;
  }
  .pro-copy { padding-right: 0; }
}


/* ----------------------------------------------------------- case study
   Collage rebuilt from Job Opportunities.png: title + underlined script over
   four columns - two of stacked images, the story panel, then the digitizing
   file above its machine readout. */
.case-band {
  background: var(--tan); color: var(--white);
  padding: clamp(3rem, 6vw, 5rem) 0 clamp(3.5rem, 7vw, 6rem);
  scroll-margin-top: var(--nav-h); /* it's an #anchor; keep the title clear of the fixed nav */
  /* a card grown at the grid's edge must not push the page sideways.
     clip, not hidden: hidden would make this a scroll container. */
  overflow-x: clip;
}
.case-head h2 {
  font-size: clamp(2.1rem, 5.4vw, 4rem);
  text-transform: uppercase; font-weight: 800; letter-spacing: -0.025em;
}
/* the mockup underlines the script line and tucks it under the headline */
.case-head .script-accent {
  display: block; width: fit-content;
  margin: 0.1rem 0 0 auto;
  font-size: clamp(1.15rem, 2.5vw, 2rem);
  color: #F6EBE0;
  text-decoration: underline;
  text-underline-offset: 0.12em;
  text-decoration-thickness: from-font;
}
.case-collage {
  margin-top: clamp(1.6rem, 3vw, 2.4rem);
  display: grid;
  /* photos sit small so the hover growth reads; the story panel takes the slack */
  grid-template-columns: 0.66fr 0.66fr 1.78fr 0.8fr;
  gap: clamp(0.7rem, 1.3vw, 1.1rem);
  align-items: start;
}
.case-col { display: flex; flex-direction: column; gap: clamp(0.7rem, 1.3vw, 1.1rem); }
.case-col--right { padding-top: clamp(0.5rem, 3vw, 2.5rem); }

/* Hover: the pointed-at card grows to full clarity, the rest dim back.
   Clarity is carried by filter+opacity, size by transform - never by blur.
   Opaque background: at any opacity below 1 the tan band reads through the
   card and the photo looks washed out, which was the reported bug. */
.case-fig {
  position: relative;
  border-radius: 12px; overflow: hidden;
  background: #6B4F3C;
  filter: none;
  opacity: 1;
  transition:
    transform 500ms cubic-bezier(0.16, 1, 0.3, 1),
    box-shadow 500ms cubic-bezier(0.16, 1, 0.3, 1),
    filter 400ms cubic-bezier(0.16, 1, 0.3, 1),
    opacity 400ms cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform, filter;
}
.case-fig img {
  width: 100%; height: 100%; display: block;
  object-fit: cover; aspect-ratio: 4 / 5;
  transition: transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
}
/* hover-grow only for a real mouse: (hover:hover) alone still let some touch
   browsers latch the :hover state on tap, so a photo grew while the reader was
   just scrolling. `and (pointer: fine)` excludes touch outright. */
@media (hover: hover) and (pointer: fine) {
  /* Dim the OTHERS only - `:not(:hover)` is load-bearing, not decoration.
     Without it this selector scores (0,4,0) because :has() takes its
     argument's specificity, which beats the (0,3,0) :hover rule below and
     dims the very card being pointed at: it grows but stays washed out.
     :has() (not :hover) on the collage so reading the story panel doesn't
     dim every photo. */
  .case-collage:has(.case-fig:hover) .case-fig:not(:hover) {
    filter: brightness(0.55);
    opacity: 0.6;
  }
  .case-collage .case-fig:hover {
    filter: none;
    opacity: 1;
    transform: scale(1.28);
    z-index: 3;
    box-shadow: 0 30px 64px -18px rgba(38, 20, 8, 0.6);
  }
  .case-fig:hover img { transform: scale(1.05); }
}
/* the readout is a text panel: `contain` keeps the stitch numbers uncropped,
   and its letterbox needs white, not the card's brown */
.case-fig--readout { background: var(--white); }
.case-fig--readout img { object-fit: contain; aspect-ratio: 292 / 322; }

.case-body {
  background: var(--white); color: var(--ink);
  border-radius: var(--r-block);
  padding: clamp(1.5rem, 2.4vw, 2.2rem);
  font-size: clamp(0.95rem, 1.1vw, 1.05rem);
}
.case-body p + p { margin-top: 0.7rem; }
.case-body .case-kicker { font-weight: 800; }

@media (max-width: 1024px) {
  .case-collage { grid-template-columns: 1fr 1fr; }
  .case-body { grid-column: 1 / -1; order: -1; }
  .case-col--right { padding-top: 0; }
  .case-col--right { grid-column: 1 / -1; flex-direction: row; }
  .case-col--right .case-fig { flex: 1; }
}
@media (max-width: 620px) {
  .case-collage { grid-template-columns: 1fr; }
  .case-col--right { flex-direction: column; }
  .case-head .script-accent { margin-left: 0; }
  /* no tap-to-grow on phones: the images stay put so a scroll is just a scroll
     (the desktop mouse-hover grow above is unaffected) */
}

/* --------------------------------------------------------- produce grid */
.produce-grid {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: clamp(0.9rem, 1.6vw, 1.25rem);
  margin-top: clamp(2.2rem, 4vw, 3.2rem);
}
.produce-card {
  background: var(--brown); color: var(--white);
  border-radius: var(--r-block);
  padding: clamp(1.5rem, 2.4vw, 2.1rem);
  transition: transform 0.5s var(--ease), box-shadow 0.5s var(--ease);
}
.produce-card:hover { transform: translateY(-6px); box-shadow: var(--shadow-card); }
.produce-card h3 {
  text-transform: uppercase; font-size: 1.3rem; letter-spacing: 0.03em;
  padding-bottom: 0.7rem; margin-bottom: 0.8rem;
  border-bottom: 2px solid rgba(255, 255, 255, 0.35);
}
.produce-card p { color: #EBD9CC; font-size: 0.98rem; line-height: 1.7; }
@media (max-width: 900px) { .produce-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .produce-grid { grid-template-columns: 1fr; } }

/* -------------------------------------------------------- service rows */
.svc-detail {
  display: grid; grid-template-columns: 0.9fr 1.4fr;
  gap: clamp(1.5rem, 4vw, 4rem);
  padding: clamp(2.2rem, 4.5vw, 3.6rem) 0;
  border-top: 1px solid var(--line);
  align-items: start;
}
.svc-detail:first-of-type { border-top: 0; }
.svc-detail h3 { font-size: clamp(1.5rem, 2.6vw, 2rem); text-transform: uppercase; }
.svc-detail .punch {
  margin-top: 0.6rem; font-weight: 800; color: var(--brown);
  font-size: clamp(1.05rem, 1.8vw, 1.3rem);
}
.svc-detail p { color: var(--ink-2); max-width: 58ch; }
/* the footer links straight to each service now, so the fixed nav must not
   land on top of the heading it just jumped to */
.svc-detail { scroll-margin-top: calc(var(--nav-h) + 1.5rem); }
@media (max-width: 820px) { .svc-detail { grid-template-columns: 1fr; gap: 0.9rem; } }

/* ----------------------------------------------------------- dark bands */
.dark-band {
  position: relative; background: var(--dark); color: var(--white);
  overflow: hidden; isolation: isolate;
}
.dark-band .band-bg {
  position: absolute; inset: 0; z-index: -1;
  background-size: cover; background-position: center;
  opacity: 0.42;
}
.dark-band .band-bg--dim { opacity: 0.3; }
.dark-band::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(to bottom, rgba(25, 20, 16, 0.4), rgba(25, 20, 16, 0.72));
}

/* ------------------------------------------------------------ about page
   The opening is copy on the photograph of the room it was written in. The
   inset skyline print that used to sit beside it was saying the same thing
   the band's own background already says, so it is gone and the story runs
   as a single measured column. (2026-08-04) */
.story-copy { max-width: 68ch; }
.story-copy p { margin-top: 1.1rem; color: rgba(255, 255, 255, 0.9); max-width: 58ch; }
.story-copy p strong { color: var(--white); }

.way-row {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(1rem, 2vw, 1.5rem);
  margin-top: clamp(1rem, 2vw, 1.5rem);
  align-items: stretch;
}
.way-card {
  background: var(--brown); color: var(--white);
  border-radius: var(--r-block);
  padding: clamp(1.6rem, 2.8vw, 2.4rem);
  display: flex; flex-direction: column; justify-content: center;
}
.way-card .way-no { font-size: 0.85rem; font-weight: 700; color: var(--peach); letter-spacing: 0.1em; }
.way-card h3 { font-size: clamp(1.2rem, 2vw, 1.55rem); text-transform: uppercase; margin: 0.6rem 0 0.8rem; }
.way-card p { color: #EBD9CC; font-size: 0.98rem; }
.way-media { border-radius: var(--r-block); overflow: hidden; min-height: 240px; }
/* the inner zoom on hover retired 2026-08-04: the plate itself now comes
   forward, and two things growing at once just reads as blur */
.way-media img { width: 100%; height: 100%; object-fit: cover; }
.way-row--flip .way-reveal { order: -1; }

/* ------------------------------------------------ the photo comes out later
   At rest a row is the principle alone. Point at it and the card lifts and
   lights, and its photograph slides out from behind it - the evidence arriving
   after the claim, which is the order you'd say them in.

   The slot owns the appear, the plate inside owns the cursor tilt, so the two
   never write the same transform. The space is reserved either way, so nothing
   on the page jumps when a photo arrives. Fine pointers only: with no cursor
   there is no hover to arrive on, and a phone keeps its photographs. */
.way-reveal { position: relative; z-index: 0; display: grid; }
/* min-width 901 matters as much as the pointer query: below it the row stacks,
   the photo sits UNDER the card rather than beside it, so there is nothing to
   emerge from - and the sideways slide would push it past the screen edge. */
@media (hover: hover) and (pointer: fine) and (min-width: 901px) {
  .way-reveal {
    opacity: 0;
    /* toward the card it hides behind, and a touch smaller for the depth */
    transform: translateX(-32%) scale(0.93);
    transition:
      opacity 620ms var(--ease),
      transform 720ms var(--ease);
    will-change: transform, opacity;
  }
  .way-row--flip .way-reveal { transform: translateX(32%) scale(0.93); }
  /* the whole row is the trigger, so moving from the card onto the photo it
     just produced does not send it back behind the card */
  .way-row:hover .way-reveal,
  .way-row:focus-within .way-reveal {
    opacity: 1;
    transform: none;
  }
}
/* Stacks at 900, not 760 (2026-08-05). A portrait tablet used to get two
   ~333px columns, which left the copy on a 29-character measure, snapped
   the headings mid-word, and made the row's photograph shorter than the
   card beside it - so the one picture meant to be seen whole sat small
   between two blurred bars. Full width gives all three back at once. */
/* Phones + tablets: each row becomes ONE image-backed card — photograph as the
   card's ground, the principle captioned on an ink plate at its foot — instead
   of a gold text card stacked above a separate photo. (Desktop keeps the
   card-beside-photo reveal.) 2026-08-18, by request.

   EVERY selector below is prefixed `.way-row`, and that is load-bearing, not
   tidiness: the black-and-gold theme re-declares `.way-card h3` (gold) and
   `.way-card p` at lines ~1993 and ~2403, AFTER this block. At equal
   specificity the later rule wins, so an unprefixed `.way-card h3 { color:#fff }`
   here silently lost and the claim kept rendering gold on the photograph —
   which is exactly the "blend" that was unreadable. `.way-row .way-card h3` is
   (0,3,0) and settles it. */
@media (max-width: 900px) {
  .way-row {
    display: block; position: relative;
    border-radius: var(--r-big); overflow: hidden;
  }
  .way-row .way-reveal { position: absolute; inset: 0; z-index: 0; display: block; }
  .way-row .way-media,
  .way-row .way-media.way-plate {
    position: absolute; inset: 0;
    width: 100%; height: 100%; min-height: 0;
    border-radius: 0; box-shadow: none;
  }
  .way-row .way-media img { width: 100%; height: 100%; object-fit: cover; }
  /* rows 01/02 are "shown whole" on desktop (contained over a blur); as a card
     ground they simply cover, so the blurred fill goes */
  .way-row .way-media--full { background: #0B0A0C; }
  .way-row .way-media--full .way-fill { display: none; }
  .way-row .way-media--full img:not(.way-fill) { object-fit: cover; }

  /* The card's OWN min-height sets the row height and the absolute image fills
     it. Putting min-height on the row and height:100% on the card broke in
     Safari, which resolves a %-height against a min-height-only parent as auto:
     the card collapsed to its text, the copy jammed to the top and the photo
     spilled below it. This is Safari-safe. */
  .way-row .way-card,
  .way-row .way-card.way-plate {
    position: relative; z-index: 1;
    min-height: clamp(380px, 98vw, 510px);
    height: auto;
    background: none; box-shadow: none;
    display: flex; flex-direction: column; justify-content: flex-start;
    padding: 0;        /* the plate runs edge to edge, so padding is per-child */
  }
  /* only the TOP is darkened, and only enough to seat the numeral — the
     photograph keeps its own light through the middle of the card */
  .way-row .way-card::before {
    content: ""; position: absolute; inset: 0; z-index: -1;
    /* Top third seats the numeral. The bottom third then eases the photograph
       DOWN into the plate, so the picture is already dim by the time it meets
       the gold hairline — without it the plate's top edge read as a hard cut
       pasted across the middle of the image. */
    background: linear-gradient(to bottom,
      rgba(3, 3, 5, 0.80) 0%, rgba(3, 3, 5, 0.32) 20%, rgba(3, 3, 5, 0) 42%,
      rgba(3, 3, 5, 0.16) 62%, rgba(3, 3, 5, 0.42) 82%, rgba(3, 3, 5, 0.60) 100%);
  }
  /* no 3D plane here — nothing sets a perspective on this layout */
  .way-row .way-card .way-no,
  .way-row .way-card h3,
  .way-row .way-card p { transform: none; position: relative; }

  /* These six are a numbered creed (01–06): the number is real information, so
     it anchors the plate — gold, large, with a short rule under it marking one
     of a set. `.way-no` IS a <p>, hence the :not(.way-no) scoping further down;
     without it the ink plate lands on the numeral too. */
  .way-row .way-card .way-no {
    padding: clamp(1.15rem, 5vw, 1.5rem) clamp(1.25rem, 5.4vw, 1.7rem) 0;
    color: var(--gold-bright); font-weight: 800;
    font-size: clamp(2.4rem, 12vw, 3.4rem); line-height: 0.85;
    letter-spacing: -0.03em; font-variant-numeric: tabular-nums;
    text-shadow: 0 2px 18px rgba(0, 0, 0, 0.55);
  }
  .way-row .way-card .way-no::after {
    content: ""; display: block;
    width: 1.9rem; height: 2px; margin-top: 0.6rem;
    background: var(--gold);
  }

  /* THE CAPTION PLATE — the fix for type "blending" into the picture. The claim
     and its explanation sit on solid ink, edge to edge, under a gold hairline
     (the site's own rule language), so legibility no longer depends on whatever
     happens to be in the photograph behind them. h3 and p carry the same
     background and meet with no gap, which is what makes them read as one
     continuous block without needing a wrapper element.

     The plate is OPAQUE. At rgba(...,0.94) six percent of the photograph came
     through it, and because what sits behind varies across the card (a bright
     desk and laptop on the right of 05, near-black on the left) the coverage
     read as patchy — the blend looked unfinished halfway across the image. A
     solid fill makes it uniform edge to edge. */
  .way-row .way-card h3 {
    margin: auto 0 0;        /* auto-top drops the plate to the card's floor */
    padding: clamp(0.95rem, 4.4vw, 1.25rem) clamp(1.25rem, 5.4vw, 1.7rem) 0.45rem;
    background: #0A0A0C;   /* opaque: see note above the plate */
    border-top: 1px solid rgba(211, 162, 76, 0.55);
    color: #FFFFFF; text-transform: uppercase; font-weight: 800;
    font-size: clamp(1.28rem, 5.5vw, 1.7rem); line-height: 1.06;
    letter-spacing: -0.01em;
  }
  .way-row .way-card p:not(.way-no) {
    margin: 0;
    padding: 0 clamp(1.25rem, 5.4vw, 1.7rem) clamp(1.2rem, 5.2vw, 1.6rem);
    background: #0A0A0C;   /* opaque: see note above the plate */
    color: rgba(255, 255, 255, 0.8); font-weight: 500;
    font-size: clamp(0.95rem, 3.65vw, 1.04rem); line-height: 1.6;
    max-width: none;
  }
}

/* ---------------------------------------------------------- testimonials */
.quote-grid {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(1rem, 2vw, 1.5rem);
  margin-top: clamp(2.2rem, 4vw, 3.2rem);
}
.quote-card {
  background: var(--cream);
  border-radius: var(--r-block);
  padding: clamp(1.7rem, 3vw, 2.4rem);
}
.quote-card blockquote {
  font-size: clamp(1.15rem, 1.9vw, 1.45rem);
  font-weight: 600; line-height: 1.4; letter-spacing: -0.01em;
}
.quote-card figcaption { margin-top: 1.3rem; color: var(--ink-2); font-size: 0.92rem; font-weight: 500; }
@media (max-width: 760px) { .quote-grid { grid-template-columns: 1fr; } }

/* -------------------------------------------------------------- CTA band */
.cta-band { text-align: center; }
.cta-band h2 {
  font-size: clamp(2rem, 5vw, 3.6rem);
  text-transform: uppercase; font-weight: 800; letter-spacing: -0.02em;
  max-width: 20ch; margin: 0 auto;
}
.cta-band p { margin: 1.2rem auto 2rem; color: rgba(255, 255, 255, 0.85); max-width: 48ch; }

/* --------------------------------------------------------------- contact */
.contact-grid {
  display: grid; grid-template-columns: 0.85fr 1.15fr;
  gap: clamp(2.5rem, 6vw, 6rem);
  align-items: start;
}
.contact-intro p { margin-top: 1.1rem; color: var(--ink-2); max-width: 40ch; }
.contact-meta { margin-top: 2rem; padding-top: 1.6rem; border-top: 1px solid var(--line); }
.contact-meta li { padding: 0.35rem 0; color: var(--ink-2); font-size: 0.98rem; }
.contact-meta strong { color: var(--ink); font-weight: 700; }
/* email + WhatsApp are live links now (2026-08-13). They inherited the muted
   list colour and a permanent underline, which read as body copy that happened
   to be clickable; gold with the underline held back for hover puts them where
   the gold placeholder pills used to sit. */
.contact-meta a {
  color: var(--gold); font-weight: 600;
  text-decoration: none;
  transition: color 200ms var(--ease);
}
.contact-meta a:hover { color: var(--gold-bright); text-decoration: underline; text-underline-offset: 3px; }
/* On the page whose whole job is being contacted, the address and the number
   should not be 17px of inline text to hit. Same gate as the footer rows. */
@media (pointer: coarse), (max-width: 1024px) {
  .contact-meta li { padding: 0.15rem 0; }
  .contact-meta a { display: flex; align-items: center; min-height: 44px; font-size: 1.05rem; }
}
/* On a phone the value also drops under its label - side by side, "Email:"
   plus a full address is two lines of wrap with the tap target buried in it */
@media (max-width: 640px) {
  .contact-meta li:has(a) { display: flex; flex-direction: column; gap: 0.1rem; }
}
/* grid items default to min-width:auto, so one unbreakable child props the
   whole column open - this lets them shrink instead */
.contact-grid > * { min-width: 0; }
@media (max-width: 820px) { .contact-grid { grid-template-columns: 1fr; } }

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem; }
.field { display: flex; flex-direction: column; gap: 0.45rem; }
.field--full { grid-column: 1 / -1; }
.field label { font-size: 0.85rem; font-weight: 700; letter-spacing: 0.02em; }
.field label .opt { color: var(--ink-2); font-weight: 500; }
.field input, .field textarea {
  font: 500 1rem/1.5 "Archivo", sans-serif;
  color: var(--ink);
  background: var(--cream);
  border: 1.5px solid transparent;
  border-radius: 14px;
  padding: 0.9rem 1.1rem;
  transition: border-color 0.25s ease, background-color 0.25s ease;
}
.field input::placeholder, .field textarea::placeholder { color: #9C8F84; }
.field input:focus, .field textarea:focus {
  outline: none; border-color: var(--brown); background: var(--white);
}
.field textarea { min-height: 160px; resize: vertical; }
.field .field-error { display: none; font-size: 0.82rem; color: #9F2F2D; font-weight: 600; }
.field.has-error input, .field.has-error textarea { border-color: #B4443F; }
.field.has-error .field-error { display: block; }
.form-actions { grid-column: 1 / -1; display: flex; align-items: center; gap: 1.2rem; flex-wrap: wrap; min-width: 0; }
/* this pill is a sentence, not a tag: nowrap would force 352px of min-content */
.form-actions .ph-tag { white-space: normal; }
.form-status { font-size: 0.95rem; font-weight: 600; }
.form-status.ok { color: #346538; }
.form-status.err { color: #9F2F2D; }
/* Grid items default to min-width:auto, so ONE unshrinkable child sets the
   whole track's width. The dialling-code control is exactly that child: its
   min-content is ~333px, which on a 390px phone pushed every field 25px past
   the card - and .brief's `overflow: clip` then sliced the labels' right edge
   off ("(optional)" lost its bracket). Both form grids get the same release
   valve .contact-grid already had. */
.form-grid > *,
.brief-fields > * { min-width: 0; }
@media (max-width: 640px) { .form-grid { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------------- footer */
/* The shelf the mark sits on. Four blocks — the rotating mark and what the
   company is, then Services / Company / Contact — and NOT ONE RULE between
   them (2026-08-05, by request): the hairlines under each heading and above
   the copyright are gone. What separates the blocks instead is light. A slow
   pool of gold sits under the mark, the same halo the plates and the notify
   card are lit by, so the footer reads as a lit shelf rather than a table of
   contents; everywhere else it is space and type weight doing the work. */
.footer {
  background: var(--dark); color: rgba(255, 255, 255, 0.82);
  /* the gold pool below is deliberately wider than the column it sits in, so
     the light spills - without this it spills past the viewport instead and
     puts a horizontal scrollbar on every page at phone widths */
  overflow: hidden;
}
.footer .wrap {
  padding-top: clamp(3.5rem, 6vw, 5.5rem);
  padding-bottom: 2.2rem;
}
.footer-top {
  display: grid;
  /* Contact went from 1.05fr to 1.2fr (241px -> 276px at full width) when the
     copy control moved into it: the number is 170px and the control 85px, and
     at 241 the two were fighting over the same fifteen pixels. The brand
     column pays for it - it holds a logo and a 16ch line, and had the slack. */
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 0.72fr) minmax(0, 1.2fr);
  gap: clamp(2rem, 4.5vw, 3.6rem);
  padding-bottom: clamp(2.5rem, 5vw, 4rem);
}

/* the mark, in its own light */
.footer-brand { position: relative; }
.footer-brand::before {
  content: "";
  position: absolute;
  left: -22%; top: -46%;
  width: 150%; aspect-ratio: 1.5;
  /* an ellipse, not a disc: a disc reads as a drawn circle behind the logo,
     which is a shape - this has to read as light falling on it */
  background: radial-gradient(50% 50% at 42% 40%, rgba(211, 162, 76, 0.16), rgba(211, 162, 76, 0.055) 46%, transparent 72%);
  pointer-events: none;
}
.footer-brand > * { position: relative; }
.footer-mark { display: inline-block; line-height: 0; }
/* the same continuously-rotating transparent WebM the nav carries, given the
   room to actually be seen turn */
.footer-mark-video {
  display: block;
  width: clamp(190px, 17vw, 250px); height: auto;
  aspect-ratio: 391 / 220;
  object-fit: contain;
}
.footer-mark img { width: clamp(190px, 17vw, 250px); height: auto; }
/* the positioning line, set as a block of display type rather than a
   sentence - it is the one thing in the footer meant to be read as a claim */
.footer-tag {
  margin-top: 1.1rem;
  max-width: 16ch;
  font-size: clamp(1rem, 1.35vw, 1.28rem);
  font-weight: 800;
  font-stretch: 118%;
  text-transform: uppercase;
  line-height: 1.14;
  letter-spacing: -0.005em;
  color: var(--ink);
}

.footer h4 {
  font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--gold); margin-bottom: 1.1rem; font-weight: 700;
}
/* The padding lives INSIDE the anchor, not on the row around it. Visually
   identical - same 28px rhythm either way - but it is the anchor that gets
   pressed, and on the li that anchor was a 19px sliver with 9px of dead space
   above and below it that looked exactly as tappable. */
.footer li { padding: 0; }
.footer li a { display: block; padding: 0.3rem 0; }
/* :visited is spelled out because the browser will otherwise tint an already-
   read link its own blue, and half this list is pages the reader has been to */
.footer a,
.footer a:visited {
  text-decoration: none; color: rgba(255, 255, 255, 0.82);
  transition: color 0.25s ease;
}
.footer a:hover, .footer a:focus-visible { color: var(--gold-bright); }

.footer-contact a { display: inline-flex; align-items: center; gap: 0.55rem; }
.footer-icon { width: 15px; height: 15px; flex: none; color: var(--gold); }

/* ------------------------------------------------- footer: copy the number
   tel: is exactly right on a phone and does nothing at all on a desktop,
   where what the reader wants is the digits on their clipboard - so the link
   stays a link and a copy control fades in beside it on hover. It holds its
   space while invisible rather than appearing into the row, because a number
   that jumps sideways as you reach for it is worse than no control at all.
   Where there is no hover to fade in from, it is simply always there. */
.footer-phone { position: relative; }
/* The contact column is 241px and the number alone takes 170 of it, so a
   labelled control sitting BESIDE the number in flow broke it over two lines.
   It is absolutely placed into the empty right-hand end of the row instead:
   it costs the row no width, so the number never wraps, and it stays INSIDE
   the row's own box - which is what makes it reachable. Parked above the row
   it was unhittable: the chip only accepts the pointer once the row is
   hovered, and a chip outside the row's box can never be what puts it there.
   Below 1200 the contact column runs the full width of the footer and the
   rows are tap targets, so there the chip just sits at the end of its row. */
.copy-num {
  position: absolute; right: 0; top: 50%;
  z-index: 2;
  display: inline-flex; align-items: center; justify-content: center; gap: 0.34rem;
  /* held at the width of the longer label so the pill does not grow under the
     cursor that just pressed it - Copy and Copied share one box */
  min-width: 5.3rem;
  padding: 0.26rem 0.55rem;
  font-family: inherit; font-size: 0.62rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.12em;
  white-space: nowrap;
  color: var(--gold);
  background: #16130F;
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  opacity: 0; transform: translate(6px, -50%);
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease, color 0.2s ease,
              border-color 0.2s ease, background-color 0.2s ease;
}
.footer-phone:hover .copy-num,
.copy-num:focus-visible {
  opacity: 1; transform: translate(0, -50%); pointer-events: auto;
}
/* the number itself never breaks - it is one thing to read, not two */
.footer-phone > a { white-space: nowrap; }
.copy-num:hover {
  color: var(--gold-bright);
  border-color: rgba(211, 162, 76, 0.5);
  background: rgba(211, 162, 76, 0.12);
}
.copy-num:focus-visible { outline: 2px solid var(--peach); outline-offset: 3px; }
.copy-num svg { width: 12px; height: 12px; flex: none; }
.copy-num-tick { display: none; }
/* copied: the tick replaces the sheets in place, so the pill does not resize
   under the cursor that just pressed it */
.copy-num.is-done {
  color: var(--gold-bright);
  border-color: rgba(211, 162, 76, 0.5);
  background: rgba(211, 162, 76, 0.12);
}
.copy-num.is-done .copy-num-icon { display: none; }
.copy-num.is-done .copy-num-tick { display: block; }
/* clipboard refused (an insecure origin, or permission denied): the number is
   selected instead and the label says what to do about it */
.copy-num.is-failed { color: var(--peach); border-color: rgba(238, 197, 122, 0.45); }

/* no pointer to reveal it with, so it is simply always there */
@media (hover: none) {
  .copy-num { opacity: 1; transform: translate(0, -50%); pointer-events: auto; }
}

/* full-width contact rows: the chip goes in flow at the end of the row it
   belongs to, which is the same place it lands on the desktop column */
@media (max-width: 1200px) {
  .footer-phone {
    display: flex; align-items: center; justify-content: space-between;
    gap: 0.6rem;
  }
  .copy-num {
    position: static;
    transform: translateY(4px);
    padding: 0.34rem 0.62rem; font-size: 0.64rem;
  }
  .footer-phone:hover .copy-num,
  .copy-num:focus-visible { transform: none; }
}
@media (max-width: 1200px) and (hover: none) {
  /* a thumb, not a cursor: the pill grows to a real tap target inside the
     50px contact row it shares with the number */
  .copy-num { transform: none; padding: 0.55rem 0.8rem; }
}
.footer-legal {
  margin-top: 1.2rem;
  font-size: 0.85rem; line-height: 1.6;
  color: var(--ink-2);
}

.footer-bottom {
  display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
  padding-top: 1.8rem; font-size: 0.85rem; color: rgba(255, 255, 255, 0.5);
}

/* ---- footer on tablet (641-1200)
   Four columns across ~700px of usable width crushed the brand block to about
   14 characters, and the positioning line - the one thing here meant to be
   read as a claim - broke into six stacked fragments. So the mark takes a full
   row of its own and the three lists share a clean three-column row beneath
   it. Nothing is resized; the grid just stops competing with itself. */
@media (max-width: 1200px) and (min-width: 641px) {
  .footer-top {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(2rem, 4vw, 3rem) clamp(1.5rem, 3vw, 2.5rem);
  }
  .footer-brand { grid-column: 1 / -1; }
  .footer-tag { max-width: 30ch; font-size: clamp(1.15rem, 2.4vw, 1.45rem); }
}

/* ---- footer on phone (<=640)
   This was 1168px - nearly a screen and a half of link list closing every
   page, with 19px anchors nobody can reliably hit. Two things change.

   Order: contact leads. This business is reached on WhatsApp, so on the one
   device that can place the call, the way to reach it goes first and the
   sitemap goes second. The nav menu already carries navigation; the footer's
   job here is the phone number.

   Reach: see the touch block below - it covers tablets too. */
@media (max-width: 640px) {
  .footer .wrap { padding-top: clamp(2.75rem, 9vw, 3.5rem); }
  .footer-top {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 2rem 1.25rem;
    padding-bottom: 2.25rem;
    /* without this the two-link Company column stretches to match the
       seven-link Services one - 300px of column holding 80px of links */
    align-items: start;
  }
  .footer-brand { grid-column: 1 / -1; }
  .footer-tag { max-width: 20ch; font-size: 1.25rem; }
  .footer-mark-video, .footer-mark img { width: 150px; }

  /* contact leads, then the sitemap */
  .footer-col:has(.footer-contact) { grid-column: 1 / -1; order: -1; }
  .footer h4 { margin-bottom: 0.35rem; }

  /* seven service links down one phone column is a scroll of its own; two
     columns of four costs nothing and halves it */
  .footer-col:has(a[href="factory-linking.html"]) {
    grid-column: 1 / -1;
  }
  .footer-col:has(a[href="factory-linking.html"]) ul {
    columns: 2; column-gap: 1.25rem;
  }
  .footer-col:has(a[href="factory-linking.html"]) li { break-inside: avoid; }

  /* the contact rows are the ones that get pressed, so they get the size and
     a hairline each - a list of numbers with no edges reads as one paragraph */
  .footer-contact li + li { border-top: 1px solid rgba(211, 162, 76, 0.12); }
  .footer-contact li a { min-height: 50px; font-size: 1.05rem; gap: 0.7rem; }
  .footer-icon { width: 17px; height: 17px; }

  /* the sitemap is reference, not action: quieter than the contact rows */
  .footer-col:not(:has(.footer-contact)) li a {
    font-size: 0.95rem;
    color: rgba(255, 255, 255, 0.68);
  }
  .footer-legal { margin-top: 1rem; }
  .footer-bottom { flex-direction: column; gap: 0.4rem; padding-top: 1.4rem; }
}

/* ---- footer reach on a thumb
   The base rule above already makes the anchor the target; this takes it to
   the 44px a thumb actually needs. Asked two ways on purpose: `pointer:
   coarse` is the honest question, but hybrids and touch laptops answer it
   inconsistently, so the tablet width carries it as well. A mouse landing on
   a 44px row at 1024 loses nothing. */
@media (pointer: coarse), (max-width: 1024px) {
  /* flex, not block: the row spans the column, so the target is its whole
     width rather than the width of the words */
  .footer li a {
    display: flex; align-items: center;
    min-height: 44px; padding: 0.2rem 0;
  }
}

/* ------------------------------------------------------- page headers */
.page-head { padding-top: calc(var(--nav-h) + clamp(3rem, 7vw, 6rem)); }
.page-head .lede { margin-top: 1.3rem; }

/* ------------------------------------------------------- motion policy
   Motion runs for everyone, by explicit instruction (2026-07-16): this
   machine reports prefers-reduced-motion:reduce from a Windows setting, and
   gating on it left the site static for its owner. The former global
   `animation-duration: 0.01ms !important` kill block lived here and is
   deliberately gone — re-adding it switches every animation below back off. */

/* ============================================================
   BLACK & GOLD THEME — component fixes on top of the token flip.
   The :root remap turned every var() gold/black; these handle the
   spots where the old warm system baked in a light-on-dark or a
   solid-brown-fill assumption the flip alone can't invert.
   ============================================================ */
::selection { color: #0A0A0B; }
.skip-link { background: var(--gold); color: #0A0A0B; }

/* nav: black glass when pinned; links + current page glow gold */
.nav.is-solid { background: rgba(10, 10, 11, 0.82); color: var(--ink); box-shadow: 0 1px 0 var(--line); }
/* `.is-here` is the scroll-spy's half of this: on home the gold moves from
   Home to Services as the reader reaches Full production, and a service page
   carries it on Services outright. Same treatment either way. */
.nav-links a:hover,
.nav-links a[aria-current="page"],
.nav-links a.is-here { color: var(--gold); }

/* buttons: a gold fill only reads as luxury with an ink-black label */
.btn--brown { color: #0A0A0B; }
.btn--brown:hover { background: var(--gold-bright); }
.btn--peach { color: #0A0A0B; }

/* content cards: dark surface + gold hairline + gold heading, not the
   solid-gold slab the straight flip would have produced */
.produce-card, .way-card {
  background: var(--cream); color: var(--ink);
  border: 1px solid var(--line);
}
.produce-card:hover, .way-card:hover { border-color: rgba(211, 162, 76, 0.42); }
.produce-card h3, .way-card h3 { color: var(--gold); border-bottom-color: var(--line); }
.produce-card p, .way-card p { color: var(--ink-2); }

/* form fields keep their dark surface on focus (were flashing to white) */
.field input:focus, .field textarea:focus { background: var(--cream); border-color: var(--gold); }
.form-status.ok { color: var(--gold-bright); }

/* dead .case-band fallback, if ever surfaced, stays black not gold */
.case-band { background: var(--dark); }

/* a hair of gold warmth in the page ground for depth under the black */
body {
  background:
    radial-gradient(120% 60% at 50% -8%, rgba(211, 162, 76, 0.06), transparent 60%),
    var(--paper);
}

/* ============================================================
   PROCESS MEDIA — every step shown whole, on nothing (2026-08-05)
   The frame used to force every shot into a 4/5 box and then dress the
   leftover with a blurred, darkened enlargement of the same photo. Those
   greyed margins are gone by request. Instead the PICTURE'S OWN BOX shrinks
   to fit the column and the height cap, so the box is the picture — 0.71
   portrait, 1.65 wide, or the three 4/5 videos, each at its own shape — and
   nothing is left over to fill. The card keeps its footprint; the shot sits
   smaller and whole inside it, on the card's own black.
   ============================================================ */
.pro-media {
  background: transparent;
  border-radius: 0;
  overflow: visible;
  display: flex; align-items: center; justify-content: center;
}
.pro-media img,
.pro-media video {
  width: auto; height: auto;
  aspect-ratio: auto;
  max-width: 100%;
  max-height: clamp(260px, 44vh, 420px);
  object-fit: contain;
  border-radius: 18px;
  position: relative; z-index: 1;
}
/* both of these existed only to dress a letterbox that no longer exists:
   the blurred fill behind it, and the gradient veil over it */
.pro-media::before { display: none; }
.pro-veil { display: none; }
.pro-media--doc { background: transparent; }

/* ============================================================
   RESPONSIVE — phone + tablet pass (2026-07-30)
   Desktop (>=1201px) is untouched by everything below.
   ============================================================ */

/* A display word that overruns its column is not just tight, it is CUT: the
   .line-mask that drives every headline's rise reveal is overflow:hidden. This
   gives the last glyph's side bearing room to paint without changing the box. */
.line-mask { padding-right: 0.06em; margin-right: -0.06em; }

/* --- contact: the word CONTACT has to clear the form column ---------------- */
@media (min-width: 821px) {
  /* was 0.85fr/1.15fr, which left "CONTACT" ~27px short of fitting at 1440
     and the mask clipped the T. More column, slightly less display size. */
  .contact-grid { grid-template-columns: 1fr 1.1fr; }
  .contact-intro .h-page { font-size: clamp(2.2rem, 5vw, 4rem); }
}

/* --- phones: display type that fits the column ---------------------------- */
@media (max-width: 560px) {
  /* both scales meet their old values exactly at 560px, so nothing jumps.
     The hero h1 is deliberately NOT in here: its longest line wraps rather
     than overflows, so it is not clipped, and holding its size keeps the
     opening statement bigger than the interior page titles. */
  .h-page { font-size: clamp(1.95rem, 9vw, 2.6rem); }
  .display, .pro-head h2 { font-size: clamp(2.05rem, 9.4vw, 2.5rem); }
}

/* --- process carousel on a phone -----------------------------------------
   One column means the frame goes wide and short, and the card still has to
   fit inside the pin next to the longest step (10). Contain keeps the whole
   shot visible at this shape too. */
@media (max-width: 700px) {
  /* height cap only - forcing a 4/3 shape here would put the letterbox back */
  .pro-media img, .pro-media video {
    max-height: min(30svh, 260px);
  }
  .pro-no { font-size: 2.1rem; }
  .pro-copy h3 { font-size: 1.25rem; margin: 0.35rem 0 0.6rem; }
  .pro-text { font-size: 0.925rem; line-height: 1.6; max-width: none; }
}
/* landscape phones and other short viewports: the pin is only ~375px tall */
@media (max-height: 560px) {
  .pro-media img, .pro-media video { max-height: 34svh; }
  .pro-text { font-size: 0.9rem; line-height: 1.55; }
}
/* The 320K case study's phone layout lives with the rest of its styles, in the
   inline <style> block in process.html - that block loads after this file, so a
   rule for .csx here would lose the cascade at equal specificity. */

/* ============================================================
   COMMON QUESTIONS — disclosure rows (2026-08-03)
   One block at the foot of Full production, Individual services and About.
   Closed, a row is a full pill: the same 999px radius as every button on
   this site, at question size. Opening it unrolls the pill into a card.
   No numbering - these are questions, not a sequence; on this site a
   number means order (the 12 steps, the six principles), so putting one
   here would be a lie about the content.
   The card is black and lit rather than drawn: a gold halo sits under it
   at rest and swells as the row opens, and the open card takes a warm
   wash from its top-left corner - so the answer arrives on light, with no
   rule or divider anywhere in the block.
   Height is animated with grid-template-rows 0fr -> 1fr, so the panel
   opens to its real height without JS measuring anything. The engine in
   main.js only owns the class and the aria state - and on a fine pointer
   it opens on HOVER, so the answer shows without a click.
   ============================================================ */
.faq-list {
  margin-top: clamp(2rem, 4vw, 3rem);
  display: flex; flex-direction: column;
  gap: clamp(0.9rem, 1.5vw, 1.3rem);
}
.faq-item {
  position: relative;
  background: #000000;
  border: 1px solid var(--line);
  border-radius: 999px;
  /* the halo, at rest */
  box-shadow:
    0 0 30px -14px rgba(211, 162, 76, 0.5),
    0 18px 44px -30px rgba(0, 0, 0, 0.9);
  transition:
    border-radius 460ms var(--ease),
    border-color 380ms ease,
    box-shadow 520ms var(--ease);
}
/* the warm wash inside an open card - a light source at the question end,
   which is what replaced the divider */
.faq-item::after {
  content: "";
  position: absolute; inset: 0; z-index: 0;
  border-radius: inherit;
  background: radial-gradient(115% 150% at 6% 0%, rgba(211, 162, 76, 0.13), transparent 58%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 520ms var(--ease);
}
.faq-item.is-open::after { opacity: 1; }
.faq-q, .faq-panel { position: relative; z-index: 1; }

.faq-item.is-open {
  border-radius: var(--r-big);
  border-color: rgba(211, 162, 76, 0.46);
  box-shadow:
    0 0 60px -10px rgba(211, 162, 76, 0.42),
    0 28px 70px -34px rgba(0, 0, 0, 0.95);
}
@media (hover: hover) and (pointer: fine) {
  .faq-item:hover {
    border-color: rgba(211, 162, 76, 0.46);
    box-shadow:
      0 0 60px -10px rgba(211, 162, 76, 0.42),
      0 28px 70px -34px rgba(0, 0, 0, 0.95);
  }
}

/* the whole row is the target, not just the icon */
.faq-trigger {
  display: flex; align-items: center;
  gap: clamp(0.9rem, 2vw, 1.8rem);
  width: 100%;
  padding: clamp(1rem, 1.7vw, 1.35rem) clamp(1.3rem, 2.1vw, 2rem);
  background: none; border: 0; cursor: pointer;
  font: inherit; color: inherit; text-align: left;
  /* open, the question tucks down toward its own answer so the two read as
     one block instead of two stacked things */
  transition: padding-bottom 460ms var(--ease);
}
.faq-item.is-open .faq-trigger { padding-bottom: clamp(0.45rem, 0.8vw, 0.7rem); }
.faq-trigger:focus-visible { outline: 2px solid var(--peach); outline-offset: 3px; border-radius: 999px; }
.faq-q { margin: 0; }
.faq-label {
  flex: 1; min-width: 0;
  font-family: "Archivo", "Helvetica Neue", Arial, sans-serif;
  font-stretch: 118%;
  font-weight: 800;
  font-size: clamp(1rem, 1.55vw, 1.3rem);
  line-height: 1.28;
  letter-spacing: -0.015em;
  transition: color 300ms ease;
}
.faq-trigger:hover .faq-label,
.faq-item.is-open .faq-label { color: var(--gold-bright); }

/* the ring fills gold when the row is open - same read as .btn--brown,
   a gold field with an ink-black mark on it */
.faq-icon {
  flex-shrink: 0;
  display: grid; place-items: center;
  width: clamp(38px, 3.2vw, 46px); aspect-ratio: 1;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1.5px var(--gold);
  color: var(--gold);
  transition:
    transform 520ms var(--ease),
    background-color 400ms ease,
    color 400ms ease;
}
.faq-trigger:hover .faq-icon { background: rgba(211, 162, 76, 0.14); }
.faq-item.is-open .faq-icon {
  transform: rotate(180deg);
  background: var(--gold); color: #0A0A0B;
}
.faq-icon svg { width: 45%; height: 45%; display: block; }

.faq-panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 520ms var(--ease);
}
.faq-item.is-open .faq-panel { grid-template-rows: 1fr; }
/* hidden, not just zero-height: a collapsed answer must be out of the
   accessibility tree too. Visibility flips back instantly on open and only
   after the row has finished rolling up on close. */
.faq-panel-inner {
  overflow: hidden;
  visibility: hidden;
  transition: visibility 0s linear 520ms;
}
.faq-item.is-open .faq-panel-inner { visibility: visible; transition-delay: 0s; }

.faq-body {
  padding: 0 clamp(1.3rem, 2.1vw, 2rem) clamp(1.3rem, 2vw, 1.8rem);
  opacity: 0; transform: translateY(10px);
  transition: opacity 320ms ease, transform 460ms var(--ease);
}
.faq-item.is-open .faq-body { opacity: 1; transform: none; transition-delay: 130ms; }

/* the answer carries real weight - it is the payload of the whole block,
   not a caption under the question */
.faq-a {
  color: rgba(242, 238, 229, 0.84);
  font-weight: 500;
  font-size: clamp(1.02rem, 1.28vw, 1.18rem);
  line-height: 1.72;
  max-width: 62ch;
}
.faq-a + .faq-a { margin-top: 0.95rem; }

/* no-JS: every answer is simply open, and the control that does nothing goes */
html:not(.js) .faq-panel { grid-template-rows: 1fr; }
html:not(.js) .faq-panel-inner { visibility: visible; }
html:not(.js) .faq-body { opacity: 1; transform: none; }
html:not(.js) .faq-icon { display: none; }
html:not(.js) .faq-item { border-radius: var(--r-big); }
html:not(.js) .faq-item::after { opacity: 1; }

@media (max-width: 560px) {
  /* the pill is too round to hold a two-line question at this width */
  .faq-item { border-radius: 26px; }
  .faq-trigger { gap: 0.8rem; }
  .faq-label { font-size: 1rem; }
}

/* ============================================================
   WHAT WE PRODUCE — the six categories as 3D plates (2026-08-03)
   Black cards on a gold halo, same light as the Common questions block.
   No rule under the heading: the gold sets the title apart on its own.
   Under a cursor a card leans toward it and lifts toward the viewer, and
   its five neighbours step back so the one being read is the clear one.
   The transform itself belongs to the spring in main.js.
   ============================================================ */
.produce-card {
  position: relative;
  z-index: 1;
  background: #000000;
  border: 1px solid var(--line);
  /* the halo, at rest */
  box-shadow:
    0 0 30px -14px rgba(211, 162, 76, 0.5),
    0 18px 44px -30px rgba(0, 0, 0, 0.9);
  /* transform is deliberately NOT in this list: the tilt engine writes one
     every frame, and a transition on it would smear those writes into mush */
  transition:
    box-shadow 520ms var(--ease),
    border-color 380ms ease,
    filter 420ms ease,
    opacity 420ms ease;
  transform-style: preserve-3d;
  will-change: transform;
}
/* ...and the entry reveal's own transform transition has to stop once the
   card is in, for exactly the same reason. (The noir field solves this by
   putting the reveal on the slot and the tilt on the card; here there is
   only one element, so the transition is retired instead.) */
.js .produce-card.reveal.is-in {
  transition:
    box-shadow 520ms var(--ease),
    border-color 380ms ease,
    filter 420ms ease,
    opacity 420ms ease;
}
/* warm interior wash, from the corner the title sits in */
.produce-card::after {
  content: "";
  position: absolute; inset: 0; z-index: 0;
  border-radius: inherit;
  background: radial-gradient(115% 150% at 6% 0%, rgba(211, 162, 76, 0.14), transparent 58%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 420ms var(--ease);
}
/* the copy rides its own plane above the card face, so the lean reads as
   real depth rather than a picture of a tilted rectangle */
.produce-card h3, .produce-card p { position: relative; z-index: 1; transform: translateZ(22px); }

.produce-card h3 {
  border-bottom: 0;
  padding-bottom: 0;
  margin-bottom: 0.75rem;
  transition: color 380ms ease;
}
.produce-card p {
  color: rgba(242, 238, 229, 0.84);
  font-weight: 500;
  font-size: clamp(1rem, 1.18vw, 1.1rem);
  line-height: 1.7;
  transition: color 380ms ease;
}

@media (hover: hover) and (pointer: fine) {
  /* the lift and lean are the engine's; this rule only stops the base
     translateY from flashing in the frame before the first pointermove */
  .produce-card:hover { transform: none; }
  .produce-card:hover {
    border-color: rgba(211, 162, 76, 0.5);
    box-shadow:
      0 0 64px -10px rgba(211, 162, 76, 0.45),
      0 34px 80px -34px rgba(0, 0, 0, 0.95);
  }
  .produce-card:hover::after { opacity: 1; }
  .produce-card:hover h3 { color: var(--gold-bright); }
  .produce-card:hover p { color: rgba(242, 238, 229, 0.97); }
  /* step the others back. `:not(:hover)` is load-bearing here, the same as
     on the case collage: :has() takes its argument's specificity, so without
     it this rule outranks the :hover rule and dims the card being read. */
  .produce-grid:has(.produce-card:hover) .produce-card:not(:hover) {
    filter: brightness(0.6);
    opacity: 0.7;
  }
}

/* no JS: no spring, so the plain lift carries the hover on its own */
html:not(.js) .produce-card { transition: transform 500ms var(--ease), box-shadow 500ms var(--ease); }
html:not(.js) .produce-card:hover { transform: translateY(-6px); }

/* ============================================================
   THE DIRECTWEAR WAY — the six principles as 3D plates (2026-08-04)
   Every row is two plates: the principle, and the photograph that proves
   it. Both lean toward the cursor on the same spring as the What we
   produce grid — one hand across the site — but they are NOT equal.

   The gold halo belongs to the copy card alone (2026-08-05). A photograph
   is already an object; ringing it in light made the row read as two lit
   slabs and flattened the difference between them. The card is the thing
   with something to say, so it is the thing that gets lit, and it also
   comes forward roughly twice as far under the cursor — the photo moves
   just enough to answer the pointer. Those two distances live in main.js.
   Everything here is the light, the type and the rest state.
   ============================================================ */
.way-plate {
  position: relative;
  z-index: 1;
  /* transform is deliberately NOT in this list: the tilt engine writes one
     every frame, and a transition on it would smear those writes into mush */
  transition:
    box-shadow 520ms var(--ease),
    border-color 380ms ease,
    filter 420ms ease,
    opacity 420ms ease;
  transform-style: preserve-3d;
  will-change: transform;
}
/* the halo, at rest: the copy card is lit from behind before it is touched */
.way-card.way-plate {
  box-shadow:
    0 0 46px -14px rgba(211, 162, 76, 0.5),
    0 22px 52px -32px rgba(0, 0, 0, 0.9);
}
/* the photograph gets depth, not light - a black drop only */
.way-media.way-plate {
  box-shadow: 0 22px 52px -30px rgba(0, 0, 0, 0.95);
}
/* ...and the entry reveal's own transform transition has to stop once the
   plate is in, for exactly the same reason */
.js .way-plate.reveal.is-in {
  transition:
    box-shadow 520ms var(--ease),
    border-color 380ms ease,
    filter 420ms ease,
    opacity 420ms ease;
}

/* the copy rides its own plane above the card face, so the lean reads as
   real depth rather than a picture of a tilted rectangle */
.way-card .way-no,
.way-card h3,
.way-card p { position: relative; z-index: 1; transform: translateZ(20px); }

/* Set bigger and heavier than the old 0.98rem/regular by request: these
   six paragraphs are the argument of the page, not a caption under it. */
.way-card p {
  color: rgba(242, 238, 229, 0.84);
  font-weight: 600;
  font-size: clamp(1.08rem, 1.35vw, 1.22rem);
  line-height: 1.66;
  transition: color 380ms ease;
}
.way-card h3 { transition: color 380ms ease; }

/* Row 01's photograph is the one that has to be seen whole — a man alone on
   a print floor IS the picture of being there, and the crop threw away the
   room around him. So the frame stays the size every other row gets and the
   picture is contained inside it, over a blurred, dimmed copy of itself:
   the same device the process carousel uses, so no plate reads as a
   letterbox with dead bars.

   That fill is a real <img> rather than a background on a pseudo-element,
   because a url() written into a custom property in the HTML resolves
   against the stylesheet that CONSUMES it, not the page - so an inline
   `assets/img/x.jpg` becomes assets/css/assets/img/x.jpg and 404s. Same
   photo, so the browser fetches it once. */
.way-media--full { background: #0B0A0C; }
.way-media--full .way-fill {
  position: absolute; top: -12%; left: -12%; z-index: 0;
  width: 124%; height: 124%;
  object-fit: cover;
  filter: blur(34px) brightness(0.45) saturate(1.1);
  pointer-events: none;
}
.way-media--full img:not(.way-fill) {
  position: relative; z-index: 1;
  object-fit: contain;
}

@media (hover: hover) and (pointer: fine) {
  /* the lift and lean are the engine's; this rule only stops the base
     translateY from flashing in the frame before the first pointermove */
  .way-plate:hover { transform: none; }
  .way-card.way-plate:hover {
    box-shadow:
      0 0 84px -12px rgba(211, 162, 76, 0.5),
      0 42px 92px -36px rgba(0, 0, 0, 0.95);
  }
  /* the photo's shadow deepens as it rises; still no gold */
  .way-media.way-plate:hover {
    box-shadow: 0 34px 76px -34px rgba(0, 0, 0, 0.95);
  }
  .way-card:hover { border-color: rgba(211, 162, 76, 0.5); }
  .way-card:hover h3 { color: var(--gold-bright); }
  .way-card:hover p { color: rgba(242, 238, 229, 0.97); }
  /* the other half of the row steps back so the plate being read is the
     clear one. `:not(:hover)` is load-bearing, same as on the produce grid. */
  .way-row:has(.way-plate:hover) .way-plate:not(:hover) {
    filter: brightness(0.68);
    opacity: 0.78;
  }
}

@media (max-width: 560px) {
  /* the desktop step-up is a comfort gain in a 470px column and a cramped
     33-character line in a 275px one, so a phone gets the middle of the
     range - still heavier and larger than the 0.98rem this replaced */
  .way-card p { font-size: 1.02rem; line-height: 1.62; }
}

/* ============================================================
   INDIVIDUAL SERVICES — the five, in one place (2026-08-05)
   Every service card on the home page now lands here, at its own anchor,
   with the other four a scroll away instead of behind a fullscreen overlay.
   Each one gains the photograph of the work it describes, under the copy,
   and the description is set at the same weight the About principles got:
   these five paragraphs are the argument, not a caption.
   ============================================================ */
/* > p is the description only — .punch sits a level deeper, inside the
   reveal wrapper, so it keeps its own gold treatment untouched */
.svc-detail > p {
  color: rgba(242, 238, 229, 0.84);
  font-weight: 600;
  /* the floor matters more than the slope here: 1.25vw only overtakes the
     1.0625rem body size past ~1360px, so a laptop was getting the new weight
     at the old size */
  font-size: clamp(1.14rem, 1.3vw, 1.26rem);
  line-height: 1.68;
}
/* ----------------------------------------------------- the index of the five
   Each tile is the door to one service page: the photograph whole (these run
   0.56 portrait to 1.69 wide, so the frame holds one height and the picture
   finds its own width), the name, and the line that sells it. Same plate light
   as everything else on the site. */
.svc-index {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1rem, 2vw, 1.6rem);
  margin-top: clamp(2rem, 4vw, 3rem);
}
.svc-tile {
  display: flex; flex-direction: column;
  background: var(--cream);
  border: 1px solid var(--line);
  border-radius: var(--r-block);
  padding: clamp(1rem, 1.6vw, 1.3rem);
  text-decoration: none; color: var(--ink);
  box-shadow:
    0 0 40px -18px rgba(211, 162, 76, 0.4),
    0 18px 44px -30px rgba(0, 0, 0, 0.9);
  transition:
    box-shadow 480ms var(--ease),
    border-color 340ms ease,
    transform 480ms var(--ease);
}
.svc-tile-shot {
  display: flex; align-items: center; justify-content: center;
  height: clamp(200px, 20vw, 270px);
  margin-bottom: clamp(0.9rem, 1.6vw, 1.2rem);
}
.svc-tile-shot img {
  width: auto; height: auto;
  max-width: 100%;
  /* the cap repeats the box's own height in the same units on purpose: as a
     percentage it would resolve against an auto-sized track and the portrait
     shots would run straight out of the tile and over the copy */
  max-height: clamp(200px, 20vw, 270px);
  border-radius: 14px;
}
.svc-tile-body { display: block; }
.svc-tile-name {
  display: block;
  font-weight: 800; font-stretch: 118%;
  font-size: clamp(1.05rem, 1.5vw, 1.3rem);
  text-transform: uppercase; letter-spacing: -0.01em;
  color: var(--gold);
}
.svc-tile-punch {
  display: block; margin-top: 0.5rem;
  font-weight: 600; font-size: clamp(1rem, 1.15vw, 1.08rem);
  line-height: 1.5; color: rgba(242, 238, 229, 0.82);
}
.svc-tile-go {
  display: inline-flex; align-items: center; gap: 0.5rem;
  margin-top: 1rem;
  font-size: 0.85rem; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-2);
  transition: color 340ms ease;
}
@media (hover: hover) and (pointer: fine) {
  .svc-tile:hover {
    transform: translateY(-6px);
    border-color: rgba(211, 162, 76, 0.5);
    box-shadow:
      0 0 72px -14px rgba(211, 162, 76, 0.48),
      0 32px 70px -34px rgba(0, 0, 0, 0.95);
  }
  .svc-tile:hover .svc-tile-go { color: var(--gold-bright); }
}
.svc-tile:focus-visible { outline: 2px solid var(--gold-bright); outline-offset: 4px; }
@media (max-width: 980px) { .svc-index { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Letting the picture find its own width inside a fixed-height frame is right
   at three-up, where the tile is about as wide as the frame is tall. It stops
   being right as the tile widens: at two-up and one-up the 0.56 portraits
   became a narrow slab of image marooned in a wide card. Below 980 the frame
   takes the full width and the photograph is cropped to it instead - the same
   thing the home cards do, so the two views of these five services agree. */
@media (max-width: 980px) {
  .svc-tile-shot { display: block; height: auto; }
  .svc-tile-shot img {
    width: 100%; height: clamp(180px, 26vw, 230px);
    max-width: none; max-height: none;
    object-fit: cover; object-position: center 42%;
  }
}
@media (max-width: 620px) {
  .svc-index { grid-template-columns: minmax(0, 1fr); }
  .svc-tile-shot img { height: 200px; }
  /* the whole tile is the target, so its action line gets the size to match */
  .svc-tile-go { margin-top: 0.9rem; font-size: 0.8rem; }
}

/* ------------------------------------------------- one service, on its own
   Each service has its own page (2026-08-05). The punch line used to sit in
   the page head under the title; 2026-08-10 it moved down into the copy plate
   as `.svc-claim`, where it is the claim the photograph beside it answers —
   standing alone up top it left a screen of dead space above the paragraph it
   belongs to. `.svc-head-punch` went with it. */
.svc-head .h-page { margin-top: 0.6rem; }
/* the explanation and the photograph sit side by side: on a page about one
   service the picture is evidence for the sentence next to it, not a footnote
   under a column of black */
.svc-detail--solo {
  grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr);
  align-items: start;
  gap: clamp(1.8rem, 4vw, 3.4rem);
  border-top: 0;
  padding-top: clamp(1.6rem, 3vw, 2.4rem);
}
.svc-detail--solo > p { max-width: 46ch; }
.svc-detail--solo .svc-shot { grid-column: auto; margin-top: 0; }
@media (max-width: 820px) {
  /* .svc-detail already stacks here; the photo just needs its air back */
  .svc-detail--solo .svc-shot { margin-top: clamp(1.4rem, 3vw, 2rem); }
}

/* These five are shot on phones and run from 0.56 portrait to 1.69 wide. A
   single wide band would have cropped the portraits to a strip of factory
   ceiling, so the figure is only a centring frame and the picture keeps its
   own shape: every shot shares one height and finds its own width. Same rule
   as the process carousel — nothing cropped, nothing filled. */
.svc-shot {
  grid-column: 1 / -1;
  margin-top: clamp(1.4rem, 2.6vw, 2.2rem);
  display: flex; justify-content: center;
}
@media (max-width: 560px) {
  /* same call as the About principles: the desktop step-up is a comfort gain
     in a wide column and a cramped line in a 300px one */
  .svc-detail > p { font-size: 1.06rem; line-height: 1.62; }
}
/* Each photograph is a 3D plate on the same spring as the About principles
   and the What we produce grid. The reveal lives on the <figure> and the tilt
   on the <img>, so the entry animation's transform transition never fights
   the per-frame writes from the engine — the slot/card split the noir field
   uses. transform is deliberately absent from the transition list below. */
.svc-shot img {
  display: block;
  width: auto; height: auto;
  max-width: 100%;
  max-height: clamp(340px, 54vh, 600px);
  border-radius: var(--r-block);
  /* the same halo the plates and the notify card carry, so a photograph
     that arrived last still sits in the site's light */
  box-shadow:
    0 0 62px -22px rgba(211, 162, 76, 0.4),
    0 26px 58px -34px rgba(0, 0, 0, 0.9);
  transition: box-shadow 520ms var(--ease);
  transform-style: preserve-3d;
  will-change: transform;
}
@media (hover: hover) and (pointer: fine) {
  /* the lean and the lift are the engine's; this only keeps the halo in step */
  .svc-shot img:hover {
    box-shadow:
      0 0 92px -14px rgba(211, 162, 76, 0.52),
      0 44px 88px -36px rgba(0, 0, 0, 0.95);
  }
}
/* no JS: no spring, so a plain lift carries the hover on its own */
html:not(.js) .svc-shot img { transition: transform 500ms var(--ease), box-shadow 500ms var(--ease); }
html:not(.js) .svc-shot img:hover { transform: translateY(-6px) scale(1.02); }

/* ============================================================
   RESOURCES — the free factory list, before it exists (2026-08-05)
   The page has one job, so it is one centred column and nothing on it
   is not either the promise or the field that claims it. The card is
   the site's raised surface on its gold hairline, lit by the same halo
   the Common questions rows and the produce plates use - a page that
   arrived last still has to belong to the system.
   The rule under the title is the one piece of ornament, and it earns
   its place: it is the only thing separating a title from a sentence
   on a page with no images to do that work.
   ============================================================ */
.notify-wrap { max-width: 720px; text-align: center; }
.notify-title { position: relative; display: inline-block; }
.notify-title::after {
  content: "";
  display: block;
  height: 4px;
  margin-top: 0.28em;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--gold), var(--gold-bright));
}
.notify-soon {
  margin-top: clamp(1.2rem, 2.6vw, 1.9rem);
  font-size: clamp(1.45rem, 3.4vw, 2.35rem);
  font-weight: 500;
  line-height: 1.18;
  letter-spacing: -0.015em;
  text-wrap: balance;
}
.notify-card {
  margin-top: clamp(2rem, 4vw, 3rem);
  background: var(--cream);
  border: 1px solid var(--line);
  border-radius: var(--r-big);
  padding: clamp(1.5rem, 3.4vw, 2.6rem);
  text-align: left;
  box-shadow:
    0 0 64px -18px rgba(211, 162, 76, 0.45),
    0 26px 62px -34px rgba(0, 0, 0, 0.9);
}
/* The shared .field input fill IS --cream, which is also this card's surface -
   on the contact page the form sits on the page black so that reads as raised,
   but inside the card the fields vanished into it entirely. Here they are cut
   the other way, as black wells in the raised surface, with the hairline the
   rest of the site uses. Focus still takes them gold. */
.notify-card .field input {
  background: var(--paper);
  border-color: var(--line);
}
.notify-card .field input:focus { background: var(--paper); border-color: var(--gold); }

.notify-note {
  margin-bottom: clamp(1.2rem, 2.4vw, 1.7rem);
  text-align: center;
  font-size: clamp(1rem, 1.4vw, 1.1rem);
  font-weight: 600;
  color: var(--gold-bright);
  text-wrap: balance;
}
/* the button holds the right edge; the status line takes the space to its
   left, so a message appears beside the control that produced it */
.notify-actions { justify-content: flex-end; }
.notify-actions .form-status { margin-right: auto; text-align: left; }

@media (max-width: 560px) {
  /* one column of controls, and the button goes full width - a right-aligned
     pill on a 280px row reads as a stray rather than the page's one action */
  .notify-wrap { text-align: left; }
  .notify-soon, .notify-note { text-align: left; }
  .notify-actions { justify-content: stretch; }
  .notify-actions .btn { width: 100%; justify-content: center; }
  .notify-actions .form-status { margin-right: 0; }
}

/* no JS: no spring, so the plain lift carries the hover on its own */
html:not(.js) .way-plate { transition: transform 500ms var(--ease), box-shadow 500ms var(--ease); }
html:not(.js) .way-plate:hover { transform: translateY(-6px) scale(1.02); }

/* ============================================================
   INDIVIDUAL SERVICE ROWS — one claim, one photograph, one height (2026-08-10)
   The two halves were sized by rules that had nothing to do with each other:
   the copy ran to whatever length it ran to, while the photograph was capped
   at its own max-height and centred on nothing. So factory linking put a
   small picture beside a tall column of text, and final quality check put a
   huge one beside four lines — same page, two unrelated shapes.

   A service page is the same shape of content as an About principle: one
   claim, and one photograph that proves it. So it now gets that exact build
   — copy in a plate, photograph in a plate, the grid stretching both to a
   SINGLE height. The halo stays on the copy plate, because that is still
   the half with something to say.
   ============================================================ */
.svc-detail--solo {
  grid-template-columns: 1fr 1fr;
  gap: clamp(1rem, 2vw, 1.5rem);
  align-items: stretch;
  padding-top: 0;
}
/* the photograph sets the floor; past it, a long service pushes BOTH halves
   taller together rather than one of them alone */
.svc-detail--solo .svc-shot {
  position: relative;
  display: block;
  margin-top: 0;
  min-height: clamp(400px, 56vh, 580px);
}
/* The base .svc-shot rules show a picture whole on nothing, which is exactly
   what left it unrelated to the text beside it. In a matched row the frame is
   the shape and the picture fills it.

   ABSOLUTE is load-bearing: in flow, `height:100%` against a parent that only
   has a min-height cannot resolve, so the <img> falls back to its intrinsic
   ratio and DRIVES the row — svc-link is 768x1367, which made the row 975px
   tall and put a quarter-screen of empty card beside it. Out of flow, the row
   is sized by the copy and the floor above, and the picture covers it. */
.svc-detail--solo .svc-shot img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  max-width: none; max-height: none;
  object-fit: cover;
  border-radius: 0;                /* the figure does the clipping now */
  box-shadow: none;                /* the light belongs to the copy plate */
}
/* Framed on the subject, not on the middle. Every one of these is a vertical
   phone shot with half a frame of ceiling above the people, so a centred crop
   would hold fluorescent tiles and cut the work off the bottom edge. Measured
   off each source: the work sits about two thirds of the way down. */
.svc-detail--solo .svc-shot img[src$="svc-link.jpg"]  { object-position: 50% 86%; }
.svc-detail--solo .svc-shot img[src$="svc-eval.jpg"]  { object-position: 50% 92%; }
.svc-detail--solo .svc-shot img[src$="svc-deal.jpg"]  { object-position: 50% 78%; }
.svc-detail--solo .svc-shot img[src$="svc-qc.jpg"]    { object-position: 50% 60%; }
/* svc-final is the one landscape source — it barely crops, so it keeps its
   own centre */
.svc-detail--solo .svc-shot img[src$="svc-final.jpg"] { object-position: 50% 50%; }

/* The punchline moved out of the page head and into the plate (2026-08-10):
   it is the claim the photograph answers, and standing alone up top it left a
   screen of dead space between itself and the paragraph it belongs to. Styled
   here rather than by joining `.way-card h3` — the About cards number their
   principles, a service has one claim and no sequence to be part of. */
.svc-detail--solo .way-card .svc-claim {
  position: relative; z-index: 1;
  transform: translateZ(20px);
  margin: 0 0 0.9rem;
  font-size: clamp(1.25rem, 2.1vw, 1.65rem);
  text-transform: uppercase;
  color: var(--gold);
  transition: color 380ms ease;
}

@media (hover: hover) and (pointer: fine) {
  /* the other half steps back while one is being read — the About rows'
     rule, which only ever knew about .way-row */
  .svc-detail--solo:has(.way-plate:hover) .way-plate:not(:hover) {
    filter: brightness(0.68);
    opacity: 0.78;
  }
  .svc-detail--solo .way-card:hover .svc-claim { color: var(--gold-bright); }
}
@media (max-width: 820px) {
  .svc-detail--solo { grid-template-columns: 1fr; }
  .svc-detail--solo .svc-shot { margin-top: 0; min-height: clamp(240px, 40vh, 340px); }
}
/* the plate is the figure now, so the picture inside it must not lift too */
html:not(.js) .svc-detail--solo .svc-shot img:hover { transform: none; }

/* ============================================================
   START YOUR PRODUCTION — the hand-off under the twelve steps (2026-08-10)
   The carousel ends on step 12 and the reader had nowhere to go. This is the
   terminus: the gold line drops out of the deck, one sentence, one button.
   It sits on the page ground, which is within a shade of the carousel's own
   #0E0C0A, so the two read as one continuous dark run.
   ============================================================ */
.startcta { position: relative; padding: clamp(3.5rem, 7vw, 6rem) 0 clamp(4rem, 8vw, 7rem); }
.startcta-inner { display: flex; flex-direction: column; align-items: center; text-align: center; }

/* the line the deck was hanging on, continuing down into the reader's move */
.startcta-thread {
  width: 1px; height: clamp(2.5rem, 6vw, 4.5rem);
  background: linear-gradient(to bottom, rgba(211, 162, 76, 0), var(--gold));
  margin-bottom: clamp(1.6rem, 3vw, 2.2rem);
}
.startcta-stamp {
  font: 500 0.7rem/1 var(--font-mono);
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--tan);
}
.startcta-title {
  margin-top: 1.1rem;
  font-size: clamp(1.9rem, 4.4vw, 3.1rem);
  font-weight: 800; font-stretch: 118%;
  text-transform: uppercase; letter-spacing: -0.025em;
  max-width: 18ch;
}
.startcta-text { margin-top: 1rem; max-width: 46ch; color: var(--ink-2); }
.startcta .btn { margin-top: clamp(1.8rem, 3.5vw, 2.4rem); }

/* ============================================================
   THE BRIEF — start-production.html (2026-08-10)
   Not a contact form. Step 03 of the process is the tech pack, the document
   the factory reads; this is the document that has to exist before step 01
   can happen, so it is set as a document — parts, field codes in the same
   mono voice the hero stamp speaks, and one sticky gold rail that fills as
   the answers land. The rail is .pro-progress's device doing a job it can
   only do here: telling the reader how much of a twelve-question form is
   left. That is the page's one bold element; everything else stays quiet.
   ============================================================ */
.brief-page { padding: clamp(2.5rem, 6vw, 4rem) 0 clamp(4rem, 9vw, 7rem); }
.brief-wrap { max-width: 880px; }

.brief-head { text-align: center; margin-bottom: clamp(2.5rem, 5vw, 3.5rem); }
.brief-head .h-page { font-size: clamp(2.1rem, 5.4vw, 3.9rem); }
.brief-sub {
  margin-top: 0.9rem;
  font-size: clamp(1.25rem, 2.4vw, 1.7rem);
  color: var(--gold);
}
.brief-lede { margin: 1.1rem auto 0; max-width: 52ch; color: var(--ink-2); }

/* Three surfaces, and they have to stay three: the document is LIFTED off
   the page ground, every field is CUT INTO the document, and an open panel
   FLOATS over both. --cream alone cannot do it - the card and the fields
   would land on the same #121116 and a text input would read as loose
   placeholder text with no box around it. */
.brief {
  --doc: #15141A;                    /* the sheet */
  --slot: #0C0B0F;                   /* a field cut into it */
  display: block;
  background: var(--doc);
  border: 1px solid var(--line);
  border-radius: var(--r-big);
  overflow: clip;                    /* the rail's corners follow the card */
  box-shadow: var(--shadow-card);
}
.brief .field input,
.brief .field textarea,
.brief .dsel-trigger { background: var(--slot); border-color: var(--line); }
.brief .field input:focus,
.brief .field textarea:focus { background: var(--slot); border-color: var(--gold); }

/* ---- completion rail. Sticks under the fixed nav and stays with the reader
   for the whole form; the fill is the process bar's own gradient. */
.brief-rail {
  position: sticky; top: var(--nav-h); z-index: 20;
  background: rgba(10, 10, 11, 0.86);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}
.brief-rail-track { display: block; height: 3px; background: rgba(211, 162, 76, 0.14); }
.brief-rail-fill {
  display: block; height: 100%;
  background: linear-gradient(90deg, var(--gold-deep), var(--gold-bright));
  transform: scaleX(0); transform-origin: left center;
  transition: transform 0.5s var(--ease);
}
.brief-rail-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  padding: 0.75rem clamp(1.25rem, 3.5vw, 2.5rem);
  font: 500 0.7rem/1 var(--font-mono);
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--ink-2);
}
.brief-rail-count { color: var(--gold); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* the rail's label turns into the alert line on a blocked submit. #9F2F2D is
   the field-error red, which is legible on the pale form plate but not against
   this near-black strip - so the alert takes a lighter tint of the same hue. */
.brief-rail-row [data-form-alert].is-alert { color: #E8736B; }

/* ---- parts. The code in the gutter is the reader's position in a long
   document, which is the one thing a long document owes them. */
.brief-part { padding: clamp(2rem, 4vw, 2.8rem) clamp(1.25rem, 3.5vw, 2.5rem); }
.brief-part + .brief-part { border-top: 1px solid var(--line); }
.brief-part-code {
  font: 500 0.7rem/1 var(--font-mono);
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--tan);
}
.brief-part-title {
  margin: 0.75rem 0 0.35rem;
  font-size: clamp(1.25rem, 2.2vw, 1.6rem);
  font-weight: 800; font-stretch: 118%;
  text-transform: uppercase; letter-spacing: -0.015em;
}
.brief-part-note { max-width: 62ch; font-size: 0.95rem; color: var(--ink-2); }
.brief-part-head { margin-bottom: 1.6rem; }
.brief-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 1.15rem 1.1rem; }

/* the label for a control that is not an <input>: same voice as .field label,
   but a span, because a <label for> cannot own a listbox */
.field-label { font-size: 0.85rem; font-weight: 700; letter-spacing: 0.02em; color: var(--ink); }
.field-note { margin-top: -0.15rem; font-size: 0.82rem; color: var(--ink-2); }
.brief .field textarea { min-height: 130px; }

/* ---- dropdown: the stacked panel from the mockup, in the site's black and
   gold. The trigger matches .field input to the pixel, because it IS a field. */
.dsel { position: relative; }
.dsel-trigger {
  width: 100%;
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  font: 500 1rem/1.5 "Archivo", sans-serif;
  text-align: left; color: var(--ink);
  background: var(--cream);
  border: 1.5px solid var(--line);
  border-radius: 14px;
  padding: 0.9rem 1.1rem;
  cursor: pointer;
  transition: border-color 0.25s ease;
}
.dsel-trigger:hover { border-color: rgba(211, 162, 76, 0.42); }
.dsel-trigger:focus-visible { outline: none; border-color: var(--gold); }
.dsel.is-open .dsel-trigger { border-color: var(--gold); }
.dsel-value { color: #9C8F84; }              /* placeholder weight until picked */
.dsel.has-value .dsel-value { color: var(--ink); font-weight: 600; }
.dsel-caret {
  flex: none; width: 16px; height: 16px; color: var(--gold);
  transition: transform 0.35s var(--ease);
}
.dsel-caret svg { display: block; width: 100%; height: 100%; }
.dsel.is-open .dsel-caret { transform: rotate(180deg); }

.dsel-list {
  position: absolute; z-index: 30; left: 0; right: 0; top: calc(100% + 0.5rem);
  max-height: 21rem; overflow-y: auto;   /* the six produce options fit whole */
  padding: 0.3rem;
  background: #211F29;               /* floats above the sheet, not level with it */
  border: 1px solid rgba(211, 162, 76, 0.3);
  border-radius: 16px;
  box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.85);
}
.dsel-list[hidden] { display: none; }
/* The search sets `hidden` on the rows it filters out, and an author `display`
   beats the UA's `[hidden] { display: none }` no matter the specificity - so
   with `display: flex` below and nothing here, every one of the 233 countries
   stayed on screen and typing "morocco" appeared to do nothing at all. This
   has to sit next to the rule it is correcting, and win it. */
.dsel-opt[hidden] { display: none; }
.dsel-opt {
  display: flex; align-items: center; gap: 0.7rem;
  padding: 0.75rem 0.85rem;
  border-radius: 11px;
  font-size: 0.98rem; font-weight: 600;
  color: var(--ink);
  cursor: pointer;
}
/* the mockup rules its options apart; a hairline between siblings keeps that
   read without boxing the first and last */
.dsel-opt + .dsel-opt { box-shadow: 0 -1px 0 var(--line); }
/* the "nothing found" line: same padding as a row so the panel keeps its
   shape, but quiet and unclickable - it is an answer, not an option */
.dsel-empty {
  padding: 0.85rem;
  font-size: 0.94rem; font-weight: 500;
  color: var(--ink-2);
  cursor: default;
}
.dsel-empty[hidden] { display: none; }
.dsel-opt:hover, .dsel-opt.is-active {
  background: rgba(211, 162, 76, 0.12);
  color: var(--gold-bright);
  box-shadow: none;
}
.dsel-opt:hover + .dsel-opt, .dsel-opt.is-active + .dsel-opt { box-shadow: none; }
.dsel-opt[aria-selected="true"] { color: var(--gold); }
/* the options take real DOM focus, so the ring has to stay - as a gold inset
   rather than the UA's white box, which fought everything around it */
.dsel-opt:focus { outline: none; }
.dsel-opt:focus-visible { outline: none; box-shadow: inset 0 0 0 1.5px var(--gold); }

/* the mark */
.dsel-mark {
  flex: none; width: 15px; height: 15px; border-radius: 50%;
  border: 1.5px solid rgba(211, 162, 76, 0.45);
  display: grid; place-items: center;
}
.dsel-mark::after {
  content: ""; width: 7px; height: 7px; border-radius: inherit;
  background: var(--gold-bright);
  transform: scale(0); transition: transform 0.25s var(--ease);
}
.dsel-opt[aria-selected="true"] .dsel-mark { border-color: var(--gold); }
.dsel-opt[aria-selected="true"] .dsel-mark::after { transform: scale(1); }

/* .field.has-error only knows about input and textarea */
.field.has-error .dsel-trigger { border-color: #B4443F; }

/* ---- SPOTLIGHT (2026-08-10, from the mockup): while a list is open, every
   question that is not the one being answered fades back, so the only thing
   lit on the page is the choice in front of you.
   Faded PER FIELD, never on an ancestor: opacity on a parent cannot be undone
   by a child, so dimming the card would take the open question down with it. */
form .field,
form .brief-rail,
form .brief-part-head,
form .brief-send,
form .form-actions { transition: opacity 300ms var(--ease); }

form.is-selecting .field:not(:has(.dsel.is-open)),
form.is-selecting .brief-rail,
form.is-selecting .brief-part-head,
form.is-selecting .brief-send,
form.is-selecting .form-actions { opacity: 0.18; }

/* and the lit question rides above the rest, panel and all */
form.is-selecting .field:has(.dsel.is-open) { position: relative; z-index: 40; }

/* ---- PHONE: dialling code and number as ONE control (2026-08-10). The two
   halves give up their own chrome to the wrapper, which owns the border and
   takes the focus ring for either of them. */
.tel {
  display: flex; align-items: stretch;
  background: var(--cream);
  border: 1.5px solid var(--line);
  border-radius: 14px;
  transition: border-color 0.25s ease;
}
.brief .tel { background: var(--slot); }
.tel:focus-within { border-color: var(--gold); }
.field.has-error .tel { border-color: #B4443F; }

.tel .dsel { flex: none; }
.tel .dsel-trigger,
.brief .tel .dsel-trigger {
  width: auto;
  gap: 0.4rem;
  padding: 0.9rem 0.8rem 0.9rem 1.1rem;
  background: transparent;
  border: 0; border-radius: 14px 0 0 14px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.tel .tel-num,
.brief .tel .tel-num {
  flex: 1 1 auto; min-width: 0;
  font: 500 1rem/1.5 "Archivo", sans-serif;
  color: var(--ink);
  background: transparent;
  border: 0; border-left: 1px solid var(--line);
  border-radius: 0 14px 14px 0;
  padding: 0.9rem 1.1rem;
}
/* the wrapper's ring says "this control"; the tint says WHICH HALF you are in */
.tel .tel-num:focus { outline: none; background: rgba(211, 162, 76, 0.06); }
.tel .dsel-trigger:focus-visible { background: rgba(211, 162, 76, 0.10); }

/* ---- the searchable panel. Two hundred countries is past the point where a
   list alone is usable, so this variant wraps the list with a search box —
   which is why the popup is .dsel-pop here and the bare list everywhere else. */
.dsel-pop {
  position: absolute; z-index: 30;
  left: 0; top: calc(100% + 0.5rem);
  width: min(360px, 84vw);
  background: #211F29;
  border: 1px solid rgba(211, 162, 76, 0.3);
  border-radius: 16px;
  box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.85);
  overflow: hidden;
}
.dsel-pop[hidden] { display: none; }
.dsel-pop .dsel-list {
  position: static;
  max-height: 15rem;
  background: transparent;
  border: 0; border-radius: 0; box-shadow: none;
  padding: 0.3rem;
}
.dsel-search { padding: 0.6rem 0.6rem 0.5rem; border-bottom: 1px solid var(--line); }
.dsel-search input {
  width: 100%;
  /* 1rem, not 0.95: iOS Safari zooms the whole page in on any focused control
     set under 16px, and this one opens inside a popover - the zoom left the
     reader somewhere else entirely with the country list off screen */
  font: 500 1rem/1.4 "Archivo", sans-serif;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.05);
  border: 1.5px solid transparent;
  border-radius: 10px;
  padding: 0.55rem 0.75rem;
}
.dsel-search input::placeholder { color: #9C8F84; }
.dsel-search input:focus { outline: none; border-color: var(--gold); }

.dsel-opt--tel { gap: 0.6rem; }
.dsel-opt-name {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dsel-opt-code {
  flex: none; font-weight: 500; color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.dsel-opt[aria-selected="true"] .dsel-opt-code,
.dsel-opt.is-active .dsel-opt-code,
.dsel-opt:hover .dsel-opt-code { color: var(--gold); }

/* ---- send */
.brief-send {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.2rem 1.5rem; flex-wrap: wrap;
  padding: clamp(1.5rem, 3vw, 2rem) clamp(1.25rem, 3.5vw, 2.5rem);
  border-top: 1px solid var(--line);
  background: rgba(211, 162, 76, 0.04);
}
.brief-send .form-status { margin-right: auto; }
.brief-send .ph-tag { white-space: normal; }   /* a sentence, not a tag */
.brief-send .btn { font-size: 1rem; padding: 1.05rem 2.2rem; }

@media (max-width: 640px) {
  .brief-fields { grid-template-columns: 1fr; }
  .brief-send { justify-content: stretch; }
  .brief-send .btn { width: 100%; justify-content: center; }
  .brief-send .form-status { margin-right: 0; }
}
