/* ==========================================================================
   Akash Panwar — Portfolio
   --------------------------------------------------------------------------
   Organisation
     1.  Design tokens (:root)
     2.  Reset & base elements
     3.  Layout primitives (section, shell, grids)
     4.  Typography utilities (eyebrow, headings, prose)
     5.  Components
     6.  Sections (hero, work, skills, process, journey, …)
     7.  Responsive
     8.  Reduced motion

   Note on inline styles: the page's JS (see the `data-dc-script` block in
   index.html) writes transform / box-shadow / border-color directly onto
   elements for hover + focus feedback, and resets them to the values declared
   here. Keep the resting values in this file in sync with those handlers.
   ========================================================================== */

/* ==========================================================================
   1. Design tokens
   ========================================================================== */

:root {
  /* ── Palette ─────────────────────────────────────────────── */
  /* page background, and cards sitting on tinted bands */
  --color-bone: #faf9f6;
  /* tinted section bands, pill fills */
  --color-linen: #f2eee8;
  /* image placeholders */
  --color-clay: #e8e1d8;
  /* primary text, and the dark band's background */
  --color-ink: #2a2a2a;
  /* strong body text */
  --color-ink-soft: #3f3a33;
  /* body text */
  --color-graphite: #544e46;
  /* muted body text */
  --color-stone: #6f685e;
  /* meta text */
  --color-pebble: #8a8177;
  /* faintest text, footer legal */
  --color-ash: #a59c8f;
  /* nav links at rest */
  --color-slate: #5a544c;

  /* primary accent — links, headings */
  --color-walnut: #6b4f3b;
  /* gradient partners for walnut */
  --color-walnut-deep: #4a3628;
  --color-walnut-light: #7a5c44;
  /* text on linen pills */
  --color-cocoa: #5a4230;
  /* secondary accent — rules, numerals */
  --color-brass: #b08d57;
  /* brass on dark backgrounds */
  --color-brass-light: #d9b784;
  /* tertiary accent — eyebrows, success */
  --color-sage: #5f6f52;
  /* error state */
  --color-rust: #a5544c;

  /* On the dark process band */
  --color-on-dark: #faf9f6;
  --color-on-dark-muted: #b9b2a7;
  --color-on-dark-faint: #a59c8f;

  /* ── Alpha derivatives ───────────────────────────────────── */
  --line-hairline: rgba(107, 79, 59, 0.08);
  --line-faint: rgba(107, 79, 59, 0.1);
  --line-soft: rgba(107, 79, 59, 0.12);
  --line: rgba(107, 79, 59, 0.14);
  --line-strong: rgba(107, 79, 59, 0.16);
  --line-bold: rgba(107, 79, 59, 0.18);
  --line-heavy: rgba(107, 79, 59, 0.28);

  --brass-a10: rgba(176, 141, 87, 0.1);
  --brass-a14: rgba(176, 141, 87, 0.14);
  --brass-a40: rgba(176, 141, 87, 0.4);
  --brass-a45: rgba(176, 141, 87, 0.45);
  --brass-a50: rgba(176, 141, 87, 0.5);
  --brass-a55: rgba(176, 141, 87, 0.55);
  --brass-a90: rgba(176, 141, 87, 0.9);
  --brass-a00: rgba(176, 141, 87, 0);
  /* lit process node: brass warmed into --color-ink, not laid over it */
  --node-lit-fill: #4a3d2b;

  /* header at rest */
  --glass: rgba(250, 249, 246, 0.72);
  /* mobile menu sheet */
  --glass-solid: rgba(250, 249, 246, 0.98);
  /* text-shadow on light bands */
  --highlight: rgba(255, 255, 255, 0.6);

  /* ── Typography ──────────────────────────────────────────── */
  --font-display: 'Instrument Serif', Georgia, serif;
  --font-body: 'IBM Plex Sans', system-ui, sans-serif;

  --weight-light: 300;
  --weight-normal: 400;
  --weight-medium: 500;

  /* Fluid type scale */
  /* Capped at 88px, down from 104px. The buyer-facing headline runs longer
     per line than the philosophical one it replaced ("I build and ship
     production SaaS — solo." vs "I build thoughtful digital products"), and
     at 104px its three lines pushed the CTA row and availability line below
     the fold on a 1000px-tall viewport — burying the two things the hero
     exists to surface. */
  --text-hero: clamp(46px, 6.4vw, 88px);
  --text-h2: clamp(38px, 5vw, 60px);
  --text-h2-wide: clamp(38px, 5.5vw, 68px);
  --text-h2-contact: clamp(40px, 5.5vw, 66px);
  --text-h2-center: clamp(36px, 4.5vw, 56px);
  --text-h3: clamp(30px, 3.4vw, 42px);
  --text-lead: clamp(17px, 1.5vw, 20px);

  --tracking-tight: -0.01em;
  --tracking-wide: 0.02em;
  --tracking-eyebrow: 0.18em;
  --tracking-label: 0.14em;
  --tracking-caps: 0.16em;

  /* ── Spacing & rhythm ────────────────────────────────────── */
  /* horizontal page padding */
  --gutter: clamp(24px, 5vw, 64px);
  /* standard section rhythm */
  --section-y: clamp(90px, 12vw, 160px);
  /* tighter rhythm for tinted / dark bands */
  --section-y-tight: clamp(70px, 9vw, 120px);
  /* scroll target offset — also read by navClick() in index.html */
  --header-offset: 68px;

  /* the shared content measure — the hero and every section align to this */
  --shell: 1240px;
  /* header bar, which runs slightly wider than the content */
  --shell-wide: 1340px;
  /* readable prose measure for the timeline */
  --shell-prose: 900px;

  /* ── Radii ───────────────────────────────────────────────── */
  --radius-sm: 9px;
  --radius-md: 12px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-2xl: 22px;
  --radius-3xl: 26px;
  --radius-card: 24px;
  --radius-pill: 100px;
  --radius-round: 50%;

  /* ── Elevation ───────────────────────────────────────────── */
  --shadow-chip: 0 2px 6px rgba(74, 54, 40, 0.05);
  --shadow-chip-hover: 0 8px 18px rgba(74, 54, 40, 0.14);
  --shadow-card: 0 10px 30px rgba(74, 54, 40, 0.06);
  --shadow-card-hover: 0 28px 60px rgba(74, 54, 40, 0.15);
  --shadow-raised: 0 14px 30px rgba(74, 54, 40, 0.12);
  --shadow-panel: 0 18px 44px rgba(74, 54, 40, 0.08);
  --shadow-menu: 0 18px 44px rgba(74, 54, 40, 0.16);
  --shadow-note: 0 16px 34px rgba(74, 54, 40, 0.1);
  --shadow-portrait:
    0 30px 70px rgba(74, 54, 40, 0.32), inset 0 1px 2px rgba(255, 255, 255, 0.22);
  --shadow-frame:
    0 0 0 1px var(--line-soft), 0 0 20px var(--brass-a10),
    0 12px 26px -12px rgba(74, 54, 40, 0.1);
  --shadow-inset: inset 0 2px 4px rgba(74, 54, 40, 0.08);
  --shadow-focus:
    inset 0 2px 4px rgba(74, 54, 40, 0.08), 0 0 0 3px var(--brass-a10);

  --shadow-btn: 0 8px 22px rgba(42, 42, 42, 0.18);
  --shadow-btn-sm: 0 4px 14px rgba(42, 42, 42, 0.16);
  --shadow-btn-strong: 0 8px 22px rgba(42, 42, 42, 0.2);
  --shadow-btn-hover: 0 12px 28px rgba(42, 42, 42, 0.24);
  --shadow-btn-press: 0 3px 10px rgba(42, 42, 42, 0.2);

  /* ── Motion ──────────────────────────────────────────────── */
  --ease-out-soft: cubic-bezier(0.2, 0.8, 0.2, 1);
  --duration-fast: 0.12s;
  --duration-base: 0.3s;
  --duration-slow: 0.4s;
  --duration-card: 0.5s;
  --duration-accordion: 0.55s;
  --duration-glow: 0.6s;
}

/* ==========================================================================
   2. Reset & base
   ========================================================================== */

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

body {
  font-family: var(--font-body);
  background: var(--color-bone);
  color: var(--color-ink);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

::selection {
  background: rgba(107, 79, 59, 0.18);
  color: var(--color-ink);
}

a {
  color: var(--color-walnut);
  text-decoration: none;
}

a:hover {
  color: var(--color-sage);
}

input,
textarea,
button {
  font-family: inherit;
}

/* ==========================================================================
   3. Layout primitives
   ========================================================================== */

/* ── ScrollSmoother scaffolding ────────────────────────────────
   The wrapper is the viewport; #smooth-content is transformed as you scroll.
   Both need to exist in CSS even when ScrollSmoother is off (mobile, reduced
   motion, CDN failure) — in that state they are inert boxes and the page
   scrolls natively, which is exactly the fallback we want. */

#smooth-wrapper {
  overflow: hidden;
}

/* Only ScrollSmoother sets this class, so a page without it keeps normal
   document flow and normal native scrolling. */
html.has-smooth-scroll #smooth-wrapper {
  position: fixed;
  height: 100%;
  width: 100%;
  top: 0;
  left: 0;
  overflow: hidden;
}

/* Root wrapper — clips the decorative overflow of hero/contact glows.

   `clip`, not `hidden`: both clip identically, but `hidden` creates a scroll
   container, and any scrollable ancestor breaks ScrollTrigger's pinning
   (position:sticky stops resolving against the viewport). `clip` does not,
   so the pinned process section works while the glows stay clipped. */
.page {
  position: relative;
  overflow-x: clip;
  background: var(--color-bone);
}

.section {
  position: relative;
  padding: var(--section-y) var(--gutter);
}

/* Tinted / dark bands sit on a tighter vertical rhythm. */
.section--band {
  padding: var(--section-y-tight) var(--gutter);
  background: var(--color-linen);
}

.section--dark {
  padding: var(--section-y-tight) var(--gutter);
  background: var(--color-ink);
  color: var(--color-on-dark);
}

/* When ScrollTrigger pins the process section it wraps it in a .pin-spacer
   and adds padding-bottom equal to the pin distance — here 900px against a
   635px section. The spacer is transparent by default, so that padding
   showed the page's cream background through the dark band for the whole
   pinned sequence: the section looked like it ended halfway down.
   Painting the spacer the same ink keeps the band continuous. ScrollTrigger
   puts the spacer immediately around the section, so this targets it without
   affecting the pin-spacer of any other trigger. */
.pin-spacer:has(> .section--dark) {
  background: var(--color-ink);
}

.section--clip {
  overflow: hidden;
}

/* The one centred container every section uses, so all content shares a
   single left edge. Sections that need a narrower measure (the timeline,
   the hero copy) constrain their own inner element rather than the shell. */
.shell {
  max-width: var(--shell);
  margin: 0 auto;
}

/* Two-column splits that collapse to one column on tablet.
   `.stack-mobile` is the shared collapse hook (see §7). */
.split {
  position: relative;
  display: grid;
  align-items: start;
  gap: clamp(40px, 7vw, 96px);
}

.split--about {
  grid-template-columns: 0.9fr 1.1fr;
}

/* Width comes from .shell, as in every other section — the split only owns
   the column ratio. */
.split--contact {
  grid-template-columns: 1fr 1fr;
  gap: clamp(40px, 6vw, 80px);
}

/* Auto-fitting card grids (skills, recognition, testimonials). */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: clamp(20px, 3vw, 36px);
}

.card-grid--wide {
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: clamp(20px, 3vw, 32px);
}

/* ==========================================================================
   4. Typography utilities
   ========================================================================== */

/* Small caps label that opens each section: "02 — Selected Work". */
.eyebrow {
  font-size: 12.5px;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--color-sage);
  margin-bottom: 18px;
}

.eyebrow--brass {
  color: var(--color-brass);
}

.display {
  font-family: var(--font-display);
  font-weight: var(--weight-normal);
  color: var(--color-ink);
  letter-spacing: var(--tracking-tight);
  margin: 0;
}

.display--hero {
  font-size: var(--text-hero);
  /* Serif descenders (the "g" in "digital") overrun a sub-1 line box and get
     clipped by the following line, so keep this at/above 1. */
  line-height: 1.06;
  margin-bottom: 26px;
}

.display--section {
  font-size: var(--text-h2);
  line-height: 1.04;
}

.display--section-wide {
  font-size: var(--text-h2-wide);
  line-height: 1.02;
}

.display--section-center {
  font-size: var(--text-h2-center);
  line-height: 1.05;
  letter-spacing: normal;
}

.display--contact {
  font-size: var(--text-h2-contact);
  line-height: 1.02;
  margin-bottom: 22px;
}

.display--on-dark {
  color: var(--color-on-dark);
}

/* Italic brass phrase inside the hero headline. */
.display__accent {
  font-style: italic;
  color: var(--color-walnut);
}

/* Trailing paragraph under a section heading. */
.section-intro {
  font-size: 17px;
  line-height: 1.7;
  color: var(--color-stone);
  font-weight: var(--weight-light);
  margin: 0;
}

.section-intro--on-dark {
  color: var(--color-on-dark-muted);
}

/* Header block wrappers — control the measure of intro copy. */
.section-head {
  max-width: 600px;
  margin-bottom: clamp(48px, 6vw, 80px);
}

.section-head--wide {
  max-width: 640px;
}

.section-head--full {
  max-width: none;
}

.section-head--center {
  max-width: 560px;
  margin: 0 auto clamp(48px, 6vw, 72px);
  text-align: center;
}

/* Split heading row used by the Work section (title left, blurb right). */
.section-head-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
  margin-bottom: clamp(48px, 6vw, 80px);
}

.section-head-row__note {
  font-size: 16px;
  line-height: 1.7;
  color: var(--color-stone);
  max-width: 340px;
  font-weight: var(--weight-light);
  margin: 0;
}

.prose {
  font-size: 17px;
  line-height: 1.8;
  color: var(--color-graphite);
  font-weight: var(--weight-light);
  margin: 0 0 24px;
}

.prose--lead {
  font-size: 19px;
  line-height: 1.75;
  color: var(--color-ink-soft);
}

.prose--last {
  margin-bottom: 24px;
}

/* Education line in About. Deliberately the quietest text in the section —
   it is present for completeness, not as a credential to lean on. Carries
   the 40px that .prose--last used to hold before the stat row. */
.about__footnote {
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--color-pebble);
  font-weight: var(--weight-light);
  margin: 0 0 40px;
}

/* ==========================================================================
   5. Components
   ========================================================================== */

/* ── Site header ───────────────────────────────────────────── */

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  padding: 18px 0;
  background: var(--glass);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line-hairline);
  transition:
    padding var(--duration-slow) ease,
    background var(--duration-slow) ease,
    border-color var(--duration-slow) ease;
}

/* Condensed state past ~24px of scroll. JS toggles the class and nothing
   else — the values that used to be written as inline styles on every
   scroll frame now live here with the rest of the header's design. */
.site-header--condensed {
  padding: 11px 0;
  background: rgba(250, 249, 246, 0.9);
  border-bottom-color: rgba(107, 79, 59, 0.14);
}

.site-header__inner {
  max-width: var(--shell-wide);
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--font-display);
  font-size: 23px;
  color: var(--color-ink);
  letter-spacing: 0.01em;
}

.brand__mark {
  width: 26px;
  height: 26px;
  border-radius: 8px;
  background: linear-gradient(140deg, var(--color-walnut), var(--color-walnut-deep));
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.25),
    0 2px 6px rgba(74, 54, 40, 0.25);
  display: inline-block;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: clamp(14px, 2.4vw, 34px);
}

.nav-links__link {
  font-size: 14px;
  letter-spacing: var(--tracking-wide);
  color: var(--color-slate);
  text-shadow: 0 1px 0 var(--highlight);
  transition:
    color var(--duration-base) ease,
    opacity var(--duration-base) ease;
}

/* Scrollspy: JS marks the in-view section's link. Siblings only dim once
   something is actually active, so the resting header stays at full strength.
   `:not(.is-active)` keeps the dim off the active link regardless of source
   order — without it this rule outranks `.nav-links__link.is-active`. */
.nav-links:has(.is-active) .nav-links__link:not(.is-active) {
  opacity: 0.7;
}

.nav-links__link.is-active {
  color: var(--color-ink);
  opacity: 1;
}

/* Hamburger — hidden until the mobile breakpoint (see §7). */
.nav-toggle {
  display: none;
}

/* ── Buttons ───────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 15px 26px;
  border-radius: var(--radius-lg);
  font-size: 15px;
  letter-spacing: 0.01em;
  transition:
    transform var(--duration-base) ease,
    box-shadow var(--duration-base) ease;
}

.btn--primary {
  background: var(--color-ink);
  color: var(--color-bone);
  box-shadow: var(--shadow-btn);
}

.btn--ghost {
  background: transparent;
  color: var(--color-ink);
  border: 1px solid var(--line-heavy);
  transition:
    transform var(--duration-base) ease,
    box-shadow var(--duration-base) ease,
    background var(--duration-base) ease;
}

/* Compact primary button in the header. */
.btn--nav {
  font-size: 14px;
  letter-spacing: var(--tracking-wide);
  color: var(--color-bone);
  background: var(--color-ink);
  padding: 9px 18px;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-btn-sm);
  transition:
    transform var(--duration-base) ease,
    box-shadow var(--duration-base) ease;
}

.btn__arrow {
  font-size: 17px;
}

/* Full-width submit inside the contact form. */
.btn--submit {
  margin-top: 4px;
  background: var(--color-ink);
  color: var(--color-bone);
  border: none;
  border-radius: var(--radius-lg);
  padding: 16px;
  font-size: 15px;
  letter-spacing: var(--tracking-wide);
  cursor: pointer;
  box-shadow: var(--shadow-btn-strong);
  transition:
    transform var(--duration-fast) ease,
    box-shadow var(--duration-fast) ease;
}

/* Text-only disclosure button on project cards. */
.btn-plain {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-size: 14px;
  color: var(--color-ink);
  letter-spacing: 0.01em;
}

.btn-row {
  display: flex;
  align-items: center;
  gap: 22px;
  flex-wrap: wrap;
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
}

/* ── Pills & chips ─────────────────────────────────────────── */

/* Static tech tag on a project card. */
.tag {
  font-size: 13px;
  padding: 6px 13px;
  border-radius: var(--radius-sm);
  background: var(--color-linen);
  border: 1px solid var(--line);
  color: var(--color-cocoa);
}

/* Interactive skill chip — JS lifts it on hover. */
.chip {
  font-size: 14px;
  padding: 9px 15px;
  border-radius: 11px;
  background: var(--color-bone);
  border: 1px solid var(--line);
  color: var(--color-ink-soft);
  box-shadow: var(--shadow-chip);
  transition:
    transform var(--duration-base) ease,
    box-shadow var(--duration-base) ease;
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 24px;
}

.chip-list {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
}

/* Status pill above the hero headline. */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 7px 14px;
  border: 1px solid var(--line-bold);
  border-radius: var(--radius-pill);
  background: rgba(250, 249, 246, 0.6);
  margin-bottom: 30px;
}

.status-pill__dot {
  width: 7px;
  height: 7px;
  border-radius: var(--radius-round);
  background: var(--color-sage);
  box-shadow: 0 0 0 3px rgba(95, 111, 82, 0.15);
}

.status-pill__label {
  font-size: 12.5px;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--color-walnut);
}

/* ── Stat blocks ───────────────────────────────────────────── */

.stat-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
  padding-top: 34px;
  border-top: 1px solid var(--line);
}

.stat__value {
  font-family: var(--font-display);
  font-size: 40px;
  color: var(--color-walnut);
  line-height: 1;
}

.stat__label {
  font-size: 13px;
  color: var(--color-pebble);
  margin-top: 8px;
  line-height: 1.5;
}

/* ── Column card (skills / recognition) ────────────────────── */

.column-card {
  border-top: 1px solid var(--line-strong);
  padding-top: 24px;
}

.column-card__title {
  font-family: var(--font-display);
  font-weight: var(--weight-normal);
  font-size: 24px;
  margin: 0 0 20px;
  color: var(--color-walnut);
}

.entry-list {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.entry__title {
  font-size: 16px;
  color: var(--color-ink);
  margin-bottom: 5px;
}

.entry__meta {
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--color-pebble);
  font-weight: var(--weight-light);
}

/* ── Service cards (engagement models) ─────────────────────────
   Built from the tokens already in use elsewhere: bone card on the page
   ground, hairline border, the same radius and shadow as the project
   panels. The featured card is distinguished by a walnut border and a
   badge rather than by size or colour, so the row keeps one baseline and
   the grid stays even. */

.service-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(20px, 2.4vw, 30px);
  align-items: stretch;
}

.service {
  display: flex;
  flex-direction: column;
  background: var(--color-bone);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: clamp(26px, 2.6vw, 36px);
  box-shadow: var(--shadow-card);
  transition:
    transform var(--duration-card) var(--ease-out-soft),
    box-shadow var(--duration-card) var(--ease-out-soft);
}

.service:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-card-hover);
}

/* The lead offer. Walnut border plus a badge — deliberately not a larger
   card or a different background, both of which break the row's baseline
   and make the other two read as rejected options rather than as
   alternatives. */
.service--featured {
  border-color: var(--color-walnut);
  box-shadow: var(--shadow-panel);
}

/* Every card reserves the badge row, whether or not it has a badge, so the
   three service names sit on one line. Without this the featured card's
   title is pushed down by the badge and the row reads as misaligned. The
   unbadged cards get the space via .service__name's margin-top below. */
.service__badge {
  align-self: flex-start;
  font-size: 11.5px;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-bone);
  background: var(--color-walnut);
  padding: 5px 11px;
  border-radius: var(--radius-pill);
  margin-bottom: 16px;
}

.service__badge[hidden] {
  display: none;
}

/* The badge's reserved height (badge line-height + its 16px margin) applied
   as top margin on cards that have no badge, so all three names align.
   :first-child only matches when no badge precedes it. */
.service__name {
  font-family: var(--font-display);
  font-weight: var(--weight-normal);
  font-size: 27px;
  line-height: 1.15;
  color: var(--color-ink);
  margin: 0 0 8px;
}

.service__name:first-child {
  margin-top: 43px;
}

.service__one-liner {
  font-size: 15px;
  line-height: 1.5;
  color: var(--color-walnut);
  margin: 0 0 16px;
}

.service__body {
  font-size: 15px;
  line-height: 1.7;
  color: var(--color-graphite);
  font-weight: var(--weight-light);
  margin: 0 0 22px;
}

.service__includes {
  list-style: none;
  margin: 0 0 26px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.service__includes li {
  position: relative;
  padding-left: 20px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--color-stone);
  font-weight: var(--weight-light);
}

/* Brass tick, drawn in CSS rather than shipping an icon per line. */
.service__includes li::before {
  content: '';
  position: absolute;
  left: 2px;
  top: 6px;
  width: 5px;
  height: 9px;
  border: solid var(--color-brass);
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg);
}

/* margin-top:auto pins the price and CTA to the bottom of every card, so
   the three CTAs line up regardless of how much copy sits above them. */
.service__foot {
  margin-top: auto;
  padding-top: 22px;
  border-top: 1px solid var(--line-faint);
}

/* How the engagement is structured, above the CTA — where a price used to
   sit. Set in the body face at label size rather than the 29px display
   serif the figure used: "Fixed scope · 6–10 weeks" is a phrase, and at
   display size it wrapped to two lines and read as a headline competing
   with the service name above it. Brass, uppercase and tracked, so it
   still anchors the foot of the card. */
.service__shape {
  font-size: 12.5px;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-brass);
  margin-bottom: 14px;
}

.service__cta {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 14.5px;
  color: var(--color-ink);
  letter-spacing: 0.01em;
  transition: gap var(--duration-base) var(--ease-out-soft);
}

.service__cta:hover {
  gap: 12px;
}

/* Booking button in the contact column, above the email/social list.
   [hidden] needs the explicit display:none because .btn sets
   display:inline-flex, which would otherwise win. */
.contact__book {
  margin-bottom: 30px;
}

.contact__book[hidden] {
  display: none;
}

.service-note {
  margin: clamp(32px, 4vw, 44px) 0 0;
  text-align: center;
  font-size: 16px;
  line-height: 1.7;
  color: var(--color-stone);
  font-weight: var(--weight-light);
}

.service-note__link {
  color: var(--color-walnut);
  border-bottom: 1px solid var(--brass-a45);
  transition: border-color var(--duration-base) ease;
}

.service-note__link:hover {
  border-bottom-color: var(--color-walnut);
}

/* ── Media frame ───────────────────────────────────────────── */

.frame {
  border-radius: var(--radius-xl);
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background: var(--color-clay);
  box-shadow: var(--shadow-frame);
}

/* ==========================================================================
   6. Sections
   ========================================================================== */

/* ── Hero ──────────────────────────────────────────────────── */

/* Padding is fluid rather than fixed at 160/130: the hero now carries a CTA
   row AND an availability line under a three-line headline, and fixed
   padding pushed both off-screen on shorter laptops. */
.hero {
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: center;
  padding: clamp(120px, 13vh, 160px) var(--gutter) clamp(90px, 11vh, 130px);
  overflow: hidden;
}

.hero__inner {
  position: relative;
  width: 100%;
  max-width: var(--shell);
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: clamp(32px, 6vw, 88px);
  align-items: center;
}

/* Decorative layers — all non-interactive. */
.hero__grain,
.hero__wash,
.hero__glow,
.hero__dust,
.contact__wash {
  pointer-events: none;
}

.hero__grain {
  position: absolute;
  inset: 0;
  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='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  opacity: 0.05;
  mix-blend-mode: multiply;
}

/* Static warm bloom anchored to the top-left of the hero. */
.hero__wash {
  position: absolute;
  top: -10%;
  left: -5%;
  width: 60%;
  height: 70%;
  background: radial-gradient(closest-side,
      rgba(176, 141, 87, 0.16),
      var(--brass-a00));
}

/* Cursor-tracked glow — JS moves it and fades it in. */
.hero__glow {
  position: absolute;
  width: 520px;
  height: 520px;
  left: 0;
  top: 0;
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side, var(--brass-a14), var(--brass-a00));
  opacity: 0;
  transition: opacity var(--duration-glow) ease;
}

.hero__dust {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* The single subheading paragraph. The 36px below it used to be split
   across two stacked paragraphs (body + body--secondary); with the second
   one gone the gap to the CTA row lives here. */
.hero__body {
  font-size: var(--text-lead);
  line-height: 1.7;
  color: var(--color-graphite);
  max-width: 520px;
  margin: 0 0 36px;
  font-weight: var(--weight-light);
}

/* Availability line under the hero CTAs. Deliberately quiet — it is a
   scarcity signal, and shouting it undercuts the rest of the page's tone.
   The dot reuses the status-pill's sage treatment so the two read as the
   same system. Text comes from data.js → availability. */
.hero__availability {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 14px;
  color: var(--color-stone);
  font-weight: var(--weight-light);
  margin: 22px 0 0;
}

.hero__availability-dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: var(--radius-round);
  background: var(--color-sage);
  box-shadow: 0 0 0 3px rgba(95, 111, 82, 0.15);
}

/* Booked-out variant, set by applyData() when availability.taking is false. */
.hero__availability--closed .hero__availability-dot {
  background: var(--color-brass);
  box-shadow: 0 0 0 3px var(--brass-a14);
}

.hero__portrait-wrap {
  position: relative;
}

/* The portrait is a photo clipped to an organic blob whose outline undulates
   and ripples away from the cursor — setupPortrait() in site.js drives it.

   Square by construction: the blob path lives in a 0–100 viewBox and is
   near-radial, so a non-square box would stretch the lobes into ovals. The
   photo inside keeps its own aspect via preserveAspectRatio="slice". */
.portrait-blob {
  position: relative;
  aspect-ratio: 1;
  max-width: 460px;
  margin-inline: auto;
  /* The drop shadow has to follow the blob's silhouette, not a bounding box —
     a box-shadow here would paint a square halo around a round shape. */
  filter: drop-shadow(0 30px 55px rgba(74, 54, 40, 0.28));
}

.portrait-blob__svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* Clay backstop while the photo decodes, matching the old mount's inner. */
.portrait-blob__image {
  background: var(--color-clay);
}

/* Brass rim on the same path as the clip, so it wobbles in lockstep.
   vector-effect keeps the hairline at 1px however the SVG is scaled. */
.portrait-blob__rim {
  fill: none;
  stroke: var(--brass-a55);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* Floating stat card at the blob's lower-left.

   Pulled inward rather than hung off a corner: the blob has no corner to
   overlap, and its widest point at that height is roughly 8% of the box in
   from the edge. Sitting flush at 0 would leave the card floating in the gap
   beside the curve instead of reading as attached to it. */
.portrait-badge {
  position: absolute;
  bottom: 6%;
  left: 0;
  background: var(--color-bone);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 14px 18px;
  box-shadow: var(--shadow-raised);
}

.portrait-badge__value {
  font-family: var(--font-display);
  font-size: 30px;
  line-height: 1;
  color: var(--color-ink);
}

.portrait-badge__label {
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-pebble);
  margin-top: 4px;
}

.scroll-cue {
  position: absolute;
  bottom: 42px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  color: var(--color-ash);
}

.scroll-cue__label {
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

.scroll-cue__line {
  width: 1px;
  height: 34px;
  background: linear-gradient(var(--color-ash), rgba(165, 156, 143, 0));
}

/* ── About ─────────────────────────────────────────────────── */

/* Heading column that pins while the prose column scrolls past. */
.about__aside {
  position: sticky;
  top: 120px;
}

/* ── Work / project cards ──────────────────────────────────── */

/* Stacked panels. Each project is a full-viewport panel that holds at the top
   of the screen while the next scrolls up and covers it — panels dealing over
   each other like cards, with the alternating bone/ink surfaces making every
   arrival read as a new screen.

   The holding is done by ScrollTrigger pins in animations.js (stickyWork()),
   NOT by `position: sticky`. That is not a preference — CSS sticky cannot work
   on this page. ScrollSmoother makes `#smooth-wrapper` a `position: fixed;
   overflow: hidden` box and translates `#smooth-content` inside it, so there
   is no scrolling ancestor for a sticky element to resolve against: the panels
   simply scrolled away, tops going negative, holding nothing. Same root cause
   as the note on `.page` about scroll containers and pinning.

   What must hold here in CSS:
     - `display: block`, not flex. ScrollTrigger's pin wraps each panel in a
       `.pin-spacer`; a flex column would stretch and reorder those spacers.
     - no `gap`. A gap shows the page background in the seam between a held
       panel and the one covering it.
     - the panels stay in normal flow so the pin-spacers stack naturally. */
.project-list {
  display: block;
}

/* Break out of the .shell measure so each surface reaches the viewport edges
   while the content inside stays on the shared column. Without this the band
   colour shows down both sides and the panel reads as a wide card. */
.project-list {
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  /* Cancel the section's bottom padding beneath the stack. `#work` is a linen
     band, and once the panels became full-bleed opaque surfaces that padding
     was the only linen still visible — a stray grey strip between the last
     panel and the next section, reading as an unfinished edge rather than a
     deliberate band. The panels supply their own vertical rhythm now. */
  margin-bottom: calc(var(--section-y-tight) * -1);
}

.project {
  position: relative;
  /* The full viewport, and pinned at top 0 (not at the header offset) so the
     surface runs *behind* the translucent header. Held back to
     `100vh - header` it stopped short, and the panel underneath showed as a
     pale seam along the top edge of every dark panel — small, but it broke
     the illusion of one colour replacing another edge to edge.
     Fixed height, not min-height: every panel must be identical for the pin
     hand-off to be seamless; the body takes any overflow instead. */
  height: 100vh;
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: clamp(28px, 4vw, 56px);
  align-items: center;
  background: var(--color-bone);
  /* Horizontal: hold the content on the shared shell measure even though the
     surface is full-bleed. Vertical: clear the header so the top line of copy
     is never tucked under it. */
  padding: calc(var(--header-offset) + clamp(20px, 3vw, 44px))
    max(var(--gutter), calc(50vw - var(--shell) / 2))
    clamp(28px, 4vw, 64px);
  /* No border, radius or shadow. These are full-bleed surfaces now, not cards
     on a band: the effect is one colour replacing another edge to edge, and a
     rounded corner or drop shadow announces "panel sliding over panel"
     instead. The card treatment comes back below the stacking breakpoint,
     where they are cards again. */
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* Stacking order is set in JS (stickyWork), not here. It has to be: a
   `:nth-of-type` ladder cannot work once ScrollTrigger wraps each pinned
   panel in its own `.pin-spacer`, because every panel then becomes the only
   child of its wrapper and all of them match `:nth-of-type(1)`. That is the
   same reason the alternating surface below is a class rather than
   `:nth-of-type(even)` — the first version of this was, and every panel
   silently rendered bone. */

/* Alternating surface. Odd panels stay on bone; even panels invert to ink and
   carry their own on-dark text treatment, since none of the project child
   elements inherit colour from the panel. The `--ink` class is applied in JS,
   in DOM order, before the pins are created. */
.project--ink {
  background: var(--color-ink);
  border-color: rgba(250, 249, 246, 0.12);
}

.project--ink .project__title {
  color: var(--color-on-dark);
}

.project--ink .project__summary {
  color: var(--color-on-dark-muted);
}

.project--ink .project__kicker {
  color: var(--color-on-dark-faint);
}

.project--ink .project__index {
  color: var(--color-brass-light);
}

/* The 34px divider between index and kicker. Brass at 40% reads as almost
   nothing against ink, so it steps up to the light brass the numerals use. */
.project--ink .project__rule {
  background: var(--color-brass-light);
}

.project--ink .detail__text {
  color: var(--color-on-dark-muted);
}

.project--ink .detail__label {
  color: var(--color-brass-light);
}

.project--ink .tag,
.project--ink .chip {
  background: rgba(250, 249, 246, 0.06);
  border-color: rgba(250, 249, 246, 0.16);
  color: var(--color-on-dark-muted);
  box-shadow: none;
}

.project--ink .btn-plain,
.project--ink .link-external {
  color: var(--color-on-dark);
}

/* Alternating layout: text first in source, image pulled to the right. */
.project--reversed {
  grid-template-columns: 0.95fr 1.05fr;
}

.project--reversed .project__body {
  order: 1;
}

.project--reversed .frame {
  order: 2;
}

.project__body {
  padding: clamp(6px, 1.5vw, 26px) clamp(6px, 1.5vw, 20px);
  /* The panel is a fixed viewport height, so the text column takes any
     overflow rather than pushing the panel taller (which would break sticky —
     see the height note on `.project`). `min-height: 0` is the grid-item
     escape hatch: without it a grid child refuses to shrink below its content
     and `overflow-y` never engages.
     Only bites when a case-detail accordion is open on a short viewport; at
     rest there is nothing to scroll. */
  min-height: 0;
  overflow-y: auto;
}

/* The image keeps the shared `.frame` 16/9 ratio. An earlier version forced
   `height: 100%; aspect-ratio: auto` here to guard against overflowing the
   fixed-height panel — but the panel has room to spare (a 731px frame in a
   900px panel), so all that did was stretch every cover image to the panel's
   full height and distort it. `min-height: 0` stays: it is the grid-item
   escape hatch that lets the frame shrink on short viewports rather than
   pushing the panel taller. */
.project .frame {
  min-height: 0;
  align-self: center;
}

/* "01 —— SaaS · Founding Engineer · 2025" */
.project__meta {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

.project__index {
  font-family: var(--font-display);
  font-size: 15px;
  color: var(--color-brass);
}

.project__rule {
  height: 1px;
  width: 34px;
  background: var(--brass-a40);
}

.project__kicker {
  font-size: 12px;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-pebble);
}

.project__title {
  font-family: var(--font-display);
  font-weight: var(--weight-normal);
  font-size: var(--text-h3);
  line-height: 1.05;
  margin: 0 0 14px;
  color: var(--color-ink);
}

.project__summary {
  font-size: 16.5px;
  line-height: 1.7;
  color: var(--color-graphite);
  margin: 0 0 20px;
  font-weight: var(--weight-light);
}

/* Collapsible case-study panel — JS animates max-height. */
.project__detail {
  max-height: 0;
  overflow: hidden;
  transition: max-height var(--duration-accordion) var(--ease-out-soft);
}

.project__detail-inner {
  display: grid;
  gap: 16px;
  padding: 4px 0 22px;
}

.detail__label {
  font-size: 11.5px;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-sage);
  margin-bottom: 5px;
}

/* The hard result, above the outcome prose. Set in the display serif at
   walnut so it carries the same weight as the stat figures in About — a
   number a buyer can hold on to, rather than another line of description.

   Rendered only when data.js carries a real figure. While the metric is
   still a TODO, applyData() sets [hidden] and the outcome falls back to
   prose alone, so unfilled scaffolding never reaches a visitor. */
.detail__metric {
  font-family: var(--font-display);
  font-size: 30px;
  line-height: 1.1;
  color: var(--color-walnut);
  margin-bottom: 6px;
}

.detail__metric[hidden] {
  display: none;
}

.project--ink .detail__metric {
  color: var(--color-brass-light);
}

.detail__text {
  margin: 0;
  font-size: 14.5px;
  line-height: 1.65;
  color: var(--color-stone);
  font-weight: var(--weight-light);
}

/* Chevron in the disclosure button — JS rotates it when open. */
.chevron {
  display: inline-block;
  transition: transform var(--duration-slow) ease;
}

.link-external {
  font-size: 14px;
  color: var(--color-walnut);
}

/* ── Process: workbench rail stepper ───────────────────────── */

/* Four stations. Must match the station count in index.html — benchScrub()
   in animations.js reads stations.length and adapts on its own, but this
   column count does not, and a mismatch leaves an empty column with the
   rail drawing past the last node into nothing. */
.bench {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(16px, 2.5vw, 40px);
}

.bench-rail {
  position: absolute;
  top: 22px;
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(90deg,
      var(--brass-a00),
      var(--brass-a45) 9%,
      var(--brass-a45) 91%,
      var(--brass-a00));
  z-index: 0;
}

/* The rail's bright overlay — GSAP scrubs scaleX 0→1 as the pinned bench
   advances, so the line appears to draw itself left-to-right. Separate from
   .bench-rail so the dim base track stays visible ahead of the draw.
   Deliberately left at full scale here rather than scaleX(0): if GSAP never
   loads, the rail must still read as a finished line. GSAP collapses it to 0
   itself once it is confirmed live, so the drawn state is the safe default
   and the animated state is the enhancement. */
.bench-rail__draw {
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,
      var(--color-brass) 0%,
      var(--color-brass-light) 50%,
      var(--color-brass) 100%);
  transform-origin: left center;
  /* Doubled halo: the tight pass gives the line an edge, the wide one spills
     onto the dark ground so the draw is visible in peripheral vision. */
  box-shadow:
    0 0 6px var(--brass-a90),
    0 0 16px var(--brass-a55);
}

/* Stations dim until the rail draw reaches them, then settle to full
   strength. Only the JS path ever sets .is-lit; without GSAP the station
   keeps its normal appearance and nothing looks unfinished. */
.station__text {
  transition:
    opacity var(--duration-card) var(--ease-out-soft),
    transform var(--duration-card) var(--ease-out-soft);
}

/* GSAP owns the icon's opacity during the sweep; stroke and glow are left to
   CSS so they ride the .is-lit toggle. The inline stroke on each <svg> is the
   base brass — override it here rather than editing seven markup blocks. */
.station svg {
  transition:
    opacity var(--duration-card) var(--ease-out-soft),
    transform var(--duration-card) var(--ease-out-soft),
    stroke var(--duration-card) var(--ease-out-soft),
    filter var(--duration-card) var(--ease-out-soft);
}

.station.is-lit svg {
  stroke: var(--color-brass-light);
  filter: drop-shadow(0 0 6px var(--brass-a55));
}

/* Lit copy steps up from muted to full-strength on-dark. */
.station.is-lit .st-title {
  color: var(--color-on-dark);
}

.station.is-lit .st-desc {
  color: var(--color-on-dark-muted);
}

.st-title,
.st-desc {
  transition: color var(--duration-card) var(--ease-out-soft);
}

/* The lit node has to read as a lamp coming on, not a slightly warmer ring.
   Three things carry that: a brass-tinted fill so the disc itself glows, a
   two-stop halo (tight + wide) since a single 18px shadow disappears against
   --color-ink, and a brighter numeral. The 6px --color-ink ring stays first
   in the stack so the halo still starts outside the rail. */
.station.is-lit .node {
  border-color: var(--color-brass-light);
  background: var(--node-lit-fill);
  color: var(--color-on-dark);
  box-shadow:
    0 0 0 6px var(--color-ink),
    0 0 0 7px var(--brass-a45),
    0 0 14px var(--brass-a90),
    0 0 34px var(--brass-a55),
    0 6px 16px rgba(0, 0, 0, 0.4);
}

.station {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 13px;
}

/* The ring shadow punches the node out of the rail behind it. */
.node {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-round);
  background: var(--color-ink);
  border: 1px solid var(--brass-a55);
  box-shadow:
    0 0 0 6px var(--color-ink),
    0 6px 16px rgba(0, 0, 0, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 18px;
  color: var(--color-brass-light);
  transition:
    transform var(--duration-slow) ease,
    border-color var(--duration-slow) ease,
    background-color var(--duration-slow) ease,
    color var(--duration-slow) ease,
    box-shadow var(--duration-slow) ease;
}

.station:hover .node {
  transform: translateY(-3px);
  border-color: var(--brass-a90);
}

.st-title {
  font-size: 16px;
  color: var(--color-on-dark);
  margin-bottom: 5px;
}

.st-desc {
  margin: 0;
  font-size: 13px;
  line-height: 1.55;
  color: var(--color-on-dark-faint);
  font-weight: var(--weight-light);
  max-width: 20ch;
}

/* ── Journey timeline ──────────────────────────────────────── */

/* Sits in the standard shell so its rail aligns with every other section's
   left edge; the max-width keeps the entry prose at a readable measure
   instead of stretching the full 1160px. */
.timeline {
  position: relative;
  padding-left: 36px;
  max-width: var(--shell-prose);
}

.timeline__line {
  position: absolute;
  left: 7px;
  top: 6px;
  bottom: 6px;
  width: 1px;
  background: linear-gradient(rgba(107, 79, 59, 0.35), rgba(107, 79, 59, 0.08));
}

.timeline__item {
  position: relative;
  padding-bottom: 48px;
}

.timeline__item:last-child {
  padding-bottom: 0;
}

.timeline__dot {
  position: absolute;
  left: -36px;
  top: 6px;
  width: 15px;
  height: 15px;
  border-radius: var(--radius-round);
  background: var(--color-bone);
  border: 2px solid var(--color-brass);
  box-shadow: 0 0 0 4px var(--brass-a14);
}

.timeline__date {
  font-size: 13px;
  letter-spacing: 0.12em;
  color: var(--color-brass);
  margin-bottom: 8px;
}

.timeline__role {
  font-family: var(--font-display);
  font-weight: var(--weight-normal);
  font-size: 26px;
  margin: 0 0 6px;
  color: var(--color-ink);
}

.timeline__org {
  font-size: 14px;
  color: var(--color-pebble);
  margin-bottom: 12px;
}

.timeline__text {
  font-size: 16px;
  line-height: 1.7;
  color: var(--color-graphite);
  font-weight: var(--weight-light);
  margin: 0;
}

/* ── Testimonials ──────────────────────────────────────────── */

/* Pinned notes — each is rotated slightly off-axis in the markup. */
.note {
  background: var(--color-bone);
  border-radius: 16px;
  padding: 32px 30px;
  box-shadow: var(--shadow-note);
  position: relative;
  margin: 0;
  /* Pivot at the pin, not the card's centre — matches .note__pin's
     `top:-9px; left:50%`. A card that rotates about its middle reads as a
     wobbling rectangle; one that rotates about a point above its top edge
     reads as weight hanging off a pin. animations.js sets the same origin
     so the JS settle and this hover share one pivot. */
  transform-origin: 50% -9px;
  transition:
    transform var(--duration-card) var(--ease-out-soft),
    box-shadow var(--duration-card) var(--ease-out-soft);
}

/* Nudge the note off true and let the shadow drop a little further, as if
   the paper has swung forward on the pin and is hanging off the wall. The
   rotation is deliberately small: the card is already tilted, and the pivot
   sits above it, so a couple of degrees moves the bottom corner a long way.

   Guarded on `:hover:hover` so it never latches on touch, where the state
   sticks after a tap and leaves one note permanently askew. */
@media (hover: hover) {

  .note:hover {
    box-shadow: 0 18px 36px rgba(74, 54, 40, 0.16);
  }

  .note--tilt-left:hover {
    transform: rotate(-2.1deg);
  }

  .note--tilt-right:hover {
    transform: rotate(1.9deg);
  }

  .note--tilt-slight:hover {
    transform: rotate(-1.5deg);
  }
}

.note--tilt-left {
  transform: rotate(-1.2deg);
}

.note--tilt-right {
  transform: rotate(0.9deg);
}

.note--tilt-slight {
  transform: rotate(-0.6deg);
}

/* Brass pin at the top edge. */
.note__pin {
  position: absolute;
  top: -9px;
  left: 50%;
  transform: translateX(-50%);
  width: 15px;
  height: 15px;
  border-radius: var(--radius-round);
  background: radial-gradient(circle at 35% 30%, #c9a56a, #8a6a3c);
  box-shadow: 0 3px 6px rgba(74, 54, 40, 0.35);
}

.note__quote {
  font-family: var(--font-display);
  font-size: 21px;
  line-height: 1.5;
  color: var(--color-ink);
  margin: 0 0 22px;
}

.note__cite {
  font-size: 14px;
  color: var(--color-stone);
}

.note__name {
  color: var(--color-ink-soft);
  font-weight: var(--weight-medium);
}

/* ── Contact ───────────────────────────────────────────────── */

.contact__wash {
  position: absolute;
  bottom: -10%;
  right: -5%;
  width: 55%;
  height: 70%;
  background: radial-gradient(closest-side,
      rgba(176, 141, 87, 0.12),
      var(--brass-a00));
}

.contact__intro {
  font-size: 17px;
  line-height: 1.75;
  color: var(--color-graphite);
  font-weight: var(--weight-light);
  margin: 0 0 36px;
  max-width: 400px;
}

.contact-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.contact-link {
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: 16px;
  color: var(--color-ink-soft);
}

.contact-link__icon {
  width: 38px;
  height: 38px;
  border-radius: 11px;
  background: var(--color-linen);
  border: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ── Form ──────────────────────────────────────────────────── */

.form {
  background: var(--color-bone);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-2xl);
  padding: clamp(24px, 3vw, 38px);
  box-shadow: var(--shadow-panel);
}

.form__fields {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.field__label {
  font-size: 13px;
  letter-spacing: 0.06em;
  color: var(--color-stone);
}

/* JS swaps border-color / box-shadow on focus (see `inputFocus`). */
.field__input {
  background: var(--color-linen);
  border: 1px solid var(--line-faint);
  border-radius: 13px;
  padding: 14px 16px;
  font-size: 15px;
  color: var(--color-ink);
  box-shadow: var(--shadow-inset);
  outline: none;
}

.field__input--textarea {
  resize: vertical;
  font-family: inherit;
}

/* Submit feedback line — JS toggles opacity and colour. */
.form__status {
  margin: 0;
  font-size: 14px;
  color: var(--color-sage);
  opacity: 0;
  transition: opacity var(--duration-card) ease;
  text-align: center;
}

/* ── Footer ────────────────────────────────────────────────── */

.site-footer {
  position: relative;
  padding: clamp(48px, 6vw, 72px) var(--gutter) 40px;
  background: var(--color-linen);
  border-top: 1px solid var(--line-soft);
}

.site-footer__top {
  max-width: var(--shell);
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
}

.site-footer__tagline {
  font-family: var(--font-display);
  font-size: 22px;
  color: var(--color-walnut);
  text-shadow: 0 1px 0 var(--highlight);
}

.site-footer__links {
  display: flex;
  gap: 24px;
  align-items: center;
}

.site-footer__link {
  font-size: 14px;
  color: var(--color-stone);
}

.site-footer__legal {
  max-width: var(--shell);
  margin: 32px auto 0;
  padding-top: 24px;
  border-top: 1px solid var(--line-faint);
  display: flex;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  font-size: 13px;
  color: var(--color-ash);
}

/* ==========================================================================
   7. Responsive
   ========================================================================== */

@media (max-width: 900px) {

  /* Collapse every two-column layout to a single column. */
  .stack-mobile,
  .hero__inner,
  .split,
  .project {
    grid-template-columns: 1fr;
  }

  /* Reversed cards: put the image back below the text once stacked. */
  .project--reversed .frame {
    order: 0;
  }

  /* Drop the sticky stack below the two-column breakpoint. Stacked to one
     column, a project's image + prose + tags already exceed a phone
     viewport, so a 100vh panel would scroll internally *and* stick — the
     content would sit half-read while the panel refused to move. Back to a
     plain scrolling list, with the gap restored so the panels read as
     separate cards again. The alternating bone/ink stays: it survives
     without the stacking.

     Flex again here, not block: `gap` has no effect on a block container, so
     restoring the spacing means restoring the flex column with it. */
  .project-list {
    display: flex;
    flex-direction: column;
    gap: clamp(40px, 6vw, 88px);
    /* Back inside the shell: the full-bleed break-out and the cancelled
       section padding only make sense for edge-to-edge stacked surfaces. */
    margin-left: 0;
    margin-right: 0;
    margin-bottom: 0;
  }

  /* Cards again, not surfaces — the border, radius and shadow come back with
     the normal card padding. */
  .project {
    position: static;
    height: auto;
    padding: clamp(18px, 2vw, 26px);
    border: 1px solid var(--line-faint);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-card);
  }

  /* Undo the fixed-height companion: the text column scrolls with the page. */
  .project__body {
    overflow-y: visible;
  }

  /* Services drop to two columns. The featured card leads the row, so it
     keeps the top-left position where reading starts. */
  .service-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 820px) {

  /* Mobile nav: hide the inline links, show the hamburger. */
  .nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    padding: 0;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-md);
    background: rgba(250, 249, 246, 0.6);
    color: var(--color-ink);
    cursor: pointer;
  }

  .nav-links {
    position: absolute;
    top: calc(100% + 10px);
    left: var(--gutter);
    right: var(--gutter);
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    padding: 12px;
    background: var(--glass-solid);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 1px solid var(--line-soft);
    border-radius: 16px;
    box-shadow: var(--shadow-menu);
    display: none;
  }

  .nav-links>a {
    padding: 12px 14px;
    border-radius: 10px;
  }

  .site-header.menu-open .nav-links {
    display: flex;
  }
}

@media (max-width: 1040px) {

  /* Tablet: four stations still fit across, so the rail and its draw stay.
     This used to force a horizontal scroll with the rail hidden — necessary
     when there were seven stations at ~158px each, and pure cost now that
     there are four. Keeping the row intact means the signature rail sweep
     survives on tablet instead of being switched off. */
  .bench {
    gap: clamp(10px, 1.8vw, 22px);
  }

  .st-desc {
    font-size: 13px;
  }
}

@media (max-width: 600px) {

  /* Services stack. Order is source order, so the MVP Build card stays
     first — the one offer a phone visitor is most likely to read. */
  .service-grid {
    grid-template-columns: 1fr;
  }

  /* Mobile: the process becomes a vertical connected timeline. */
  .bench {
    grid-template-columns: 1fr;
    gap: 26px;
  }

  .bench-rail {
    top: 22px;
    bottom: 26px;
    left: 21px;
    right: auto;
    width: 1px;
    height: auto;
    background: linear-gradient(180deg,
        var(--brass-a00),
        var(--brass-a40) 6%,
        var(--brass-a40) 94%,
        var(--brass-a00));
  }

  .station {
    display: grid;
    grid-template-columns: 44px 1fr;
    column-gap: 16px;
    row-gap: 8px;
    align-items: start;
    text-align: left;
  }

  .node {
    grid-column: 1;
    grid-row: 1 / span 2;
  }

  .station>svg {
    grid-column: 2;
    grid-row: 1;
    align-self: center;
  }

  .station__text {
    grid-column: 2;
    grid-row: 2;
  }

  .st-desc {
    max-width: none;
  }

  /* The blob is square, so at full column width it eats far more vertical
     space on a phone than the old 4:5 frame did — enough to push the CTAs
     under the fold. Cap it and pull the badge in with it. */
  .portrait-blob {
    max-width: 300px;
  }

  .portrait-badge {
    padding: 11px 14px;
  }

  .portrait-badge__value {
    font-size: 25px;
  }

  /* Stacked, the portrait sits directly above the scroll cue instead of
     beside it, and the hero's bottom padding is the only thing between them
     — which left the cue crowding the blob's lower edge. Push the cue down
     and grow the padding that reserves its space, so the gap comes from the
     layout rather than from the cue overlapping the portrait. */
  .hero {
    padding-bottom: 132px;
  }

  .scroll-cue {
    bottom: 24px;
  }
}

/* ==========================================================================
   7b. Interaction states

   These were previously inline styles written by JS event handlers (chipIn,
   cardIn, liftIn, pressIn, inputFocus and their partners). They are pure
   presentation reacting to hover/active/focus, so CSS owns them now: no
   listeners, no inline-style churn, and they keep working if JS fails.
   The values are unchanged from the handlers they replace.
   ========================================================================== */

.chip {
  box-shadow: var(--shadow-chip);
}

.chip:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-chip-hover);
  border-color: var(--brass-a50);
}

/* No hover lift on the project panels. They are full-viewport sticky surfaces
   now rather than cards in a list, so "lift the card under the cursor" has
   nothing to point at — the pointer is always over one of them. A transform
   here would also create a containing block for the panel's own descendants
   and shift the stacking the sticky stack depends on. */

/* Hover lift for the primary/ghost/nav/submit buttons. */
.btn:hover,
.btn--nav:hover,
.btn--submit:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-btn-hover);
}

/* Press feedback. :active fires for both mouse and touch, which removes the
   four separate mousedown/mouseup/touchstart/touchend handlers. The scale is
   skipped under reduced motion by the block below. */
.btn:active,
.btn--nav:active {
  transform: translateY(-2px) scale(0.97);
  box-shadow: var(--shadow-btn-press);
  transition:
    transform 140ms var(--ease-out-soft),
    box-shadow 140ms var(--ease-out-soft);
}

.btn--submit:active {
  transform: translateY(2px);
  box-shadow: var(--shadow-btn-press);
}

/* Touch has no hover to hold the lift, so the pressed state sits flat. */
@media (hover: none) {

  .btn:active,
  .btn--nav:active {
    transform: scale(0.97);
  }
}

.field__input:focus {
  border-color: var(--brass-a55);
  box-shadow: var(--shadow-focus);
  outline: none;
}

/* ==========================================================================
   8. Reduced motion

   JS gates the reveal, parallax and particle systems; this is the CSS safety
   net for every remaining transition/animation (hover lifts, accordion, etc.).
   Near-zero (not 0s) so state still settles instantly without tripping
   transitionend-dependent logic.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {

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

  /* Unstack the work section. The sticky stack is motion the reader did not
     ask for and cannot opt out of mid-scroll — panels holding still while
     others slide over them is the same vestibular problem the pinned process
     section is gated on. Reverts to a plain scrolling list; the alternating
     bone/ink surfaces still carry the section's rhythm.

     Flex again, since `gap` does nothing on the block container the stacked
     layout uses. */
  .project-list {
    display: flex;
    flex-direction: column;
    gap: clamp(40px, 6vw, 88px);
    margin-left: 0;
    margin-right: 0;
    margin-bottom: 0;
  }

  .project {
    position: static;
    height: auto;
    padding: clamp(18px, 2vw, 26px);
    border: 1px solid var(--line-faint);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-card);
  }

  .project__body {
    overflow-y: visible;
  }
}