/* Leeway Design Studio — generated bundle. Do not edit.
   Source sheets: assets/css/*.css, order set in
   tools/build.py STYLESHEETS. Rebuild with build.py. */

/* ===== fonts.css ===== */
/* ============================================================
   Self-hosted webfonts (SIL Open Font License 1.1)

   Playfair Display  — headings. A high-contrast display serif with a
                       tall x-height, so titles read large and composed.
   Manrope           — body, labels, navigation. Chosen for its generous
                       x-height: at the same font-size it looks notably
                       larger and clearer than a display serif, which is
                       what keeps small text legible rather than dainty.

   Both are variable fonts — one file per subset covers the whole weight
   range (Playfair 400-900, Manrope 400-800), so every weight the CSS
   asks for is a real cut, never a browser-synthesised fake bold.

   Plus Noto Sans JP / Arabic / Devanagari, subset to the exact intro
   greetings — neither Latin face carries those scripts.

   Served from assets/fonts/ so the site has no external dependency.
   ============================================================ */

/* latin-ext */
@font-face {
  font-family: 'Playfair Display';
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url(../fonts/playfair-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Playfair Display';
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url(../fonts/playfair-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* latin-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url(../fonts/manrope-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url(../fonts/manrope-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Noto Sans JP';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url(../fonts/noto-sans-jp-500-0.woff2) format('woff2');
  unicode-range: U+3053, U+3061, U+306b, U+306f, U+3093;
}

@font-face {
  font-family: 'Noto Sans Arabic';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url(../fonts/noto-sans-arabic-500-0.woff2) format('woff2');
  unicode-range: U+627-628, U+62d, U+631, U+645, U+64b;
}

@font-face {
  font-family: 'Noto Sans Devanagari';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url(../fonts/noto-sans-devanagari-500-0.woff2) format('woff2');
  unicode-range: U+924, U+928, U+92e, U+938, U+947, U+94d;
}

/* ===== variables.css ===== */
/* ============================================================
   LEEWAY DESIGN STUDIO — Portfolio Website
   Design tokens
   ============================================================ */

:root {
  /* Colors */
  --charcoal:      #2F2F2F;
  --charcoal-deep: #262626;
  --charcoal-soft: #383838;
  --charcoal-line: #454545;
  --purple:        #885DCE;
  --purple-soft:   #A184DD;
  --yellow:        #FDE962;
  --ink:           #F5F4F2;
  --ink-muted:     #B7B4AF;
  --ink-faint:     #8A8783;

  /* Foreground for saturated accent surfaces (primary button).
     Deliberately NOT --ink: the purple background stays dark in both
     themes, so a token that inverts with the theme produced dark-on-dark
     text (2.3:1) in light mode. Pure white clears WCAG AA against both
     the dark-theme purple (4.86:1) and the light-theme purple (8.4:1). */
  --on-accent:     #FFFFFF;

  /* Type */
  --font-display: 'Playfair Display', Georgia, serif;
  --font-body:    'Manrope', 'Segoe UI', system-ui, sans-serif;

  /* Media
     ONE ratio for every image and video box on the site — case films,
     live site previews and portfolio thumbnails alike. Because the box
     is fixed, the rendered size never depends on the file you upload,
     so nothing can arrive taller or shorter than its neighbours.

       Export everything at 1920 x 1080 (16:9).
       Minimum 1280 x 720. Videos: MP4 / H.264.

     Anything not 16:9 still fits — object-fit: cover fills the box and
     trims the overflow from the centre — but it is trimmed, so art you
     care about should be exported at the ratio. */
  --media-ar: 16 / 9;

  /* Layout */
  --max-width: 1180px;
  --gutter: 6vw;
  --radius-sm: 10px;
  --radius-md: 18px;
  --radius-lg: 28px;

  /* Motion */
  --ease: cubic-bezier(.4, 0, .2, 1);

  /* Matte surface.
     One noise tile shared by every charcoal surface so the whole site
     reads as a single material. Strength is deliberately high enough to
     see: below ~0.06 it disappears entirely on most displays and the
     charcoal just looks like flat digital fill. */
  /* Semantic surfaces — these are what the light theme swaps. */
  --surface-raised:  #333333;              /* cards, network nodes */
  --surface-sunken:  rgba(20, 20, 20, 0.6);/* inset wells, icon discs */
  --hairline:        rgba(255, 255, 255, 0.10);
  --hairline-strong: rgba(255, 255, 255, 0.18);

  /* Scroll rail. Read by JS, which paints inline styles. */
  --rail-base: 255, 255, 255;
  --rail-fill: 253, 233, 98;

  --grain-strength: 0.10;
  --grain-tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* ===== base.css ===== */
/* ============================================================
   Reset
   ============================================================ */

* { margin: 0; padding: 0; box-sizing: border-box; }

html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }
}

body {
  background: var(--charcoal);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* Site-wide matte finish.
   One shared grain tile, reused by every charcoal surface below. Fixed
   and never animated, so the compositor paints it once and reuses it on
   every frame — it costs nothing to scroll past. Sitting behind content
   means it textures the charcoal without laying noise over client work. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--grain-strength);
  background-image: var(--grain-tile);
  background-size: 150px 150px;
}

/* The alt sections paint their own opaque charcoal, which would cover the
   fixed layer above — so they carry the same grain themselves. Without
   this, only two sections on the page had any texture at all. */
.section-alt::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: var(--grain-strength);
  background-image: var(--grain-tile);
  background-size: 150px 150px;
}

.section-alt > * {
  position: relative;
  z-index: 1;
}

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

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

ul { list-style: none; }

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

::selection {
  background: var(--yellow);
  color: var(--charcoal);
}

:focus-visible {
  outline: 2px solid var(--yellow);
  outline-offset: 3px;
  border-radius: 4px;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.15;
}

.container {
  max-width: var(--max-width);
  margin: 0 auto;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

.eyebrow {
  display: inline-block;
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--purple-soft);
  margin-bottom: 14px;
}

/* Hero eyebrow. The breathing purple bloom is gone: an animated
   text-shadow repaints the text every frame for the life of the page,
   and a pulsing glow undercuts the matte direction. Flat tone instead. */
.eyebrow-glow {
  color: #C9B6F2;
}


.section-heading {
  font-size: clamp(1.8rem, 3.4vw, 2.6rem);
  color: var(--ink);
  margin-bottom: 18px;
}

/* ===== animations.css ===== */
/* ============================================================
   Section headings — letter-by-letter scroll-scrubbed reveal
   Each letter of the word flies in one-by-one from the left and
   stacks into the centered word. Bound to scroll progress (see
   initScrollHeadings()); scrolling up reverses it. Only
   transform / opacity animate (GPU-friendly). Purple type.
   ============================================================ */
.scroll-heading-clip {
  overflow: hidden;              /* contain the off-canvas start, no layout shift */
  margin-bottom: 16px;
}

.scroll-heading {
  display: flex;
  justify-content: center;       /* stack/anchor the word in the center */
  align-items: baseline;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.6rem, 3vw, 2.4rem);   /* medium, not oversized */
  line-height: 1.15;
  color: var(--purple);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  margin: 0;
  text-align: center;
}

.scroll-heading .svc-letter {
  display: inline-block;
  opacity: 0;
  transform: translate3d(-60vw, 0, 0);   /* starts off-canvas left */
  /* 42 letters were held on their own layers permanently. transform and
     opacity composite fine without the hint; the browser promotes them
     for the duration of the scrub and releases them afterwards. */
}

/* Reduced motion / no-JS: show the finished word immediately. */
.scroll-heading.is-static .svc-letter {
  opacity: 1;
  transform: none;
  will-change: auto;
}

.section-lede {
  color: var(--ink-muted);
  max-width: 560px;
  font-size: 1.02rem;
  margin-bottom: 48px;
}

section {
  padding: 110px 0;
  position: relative;
}

.section-alt {
  background: var(--charcoal-deep);
}

/* ===== components.css ===== */
/* ============================================================
   Scroll Rail — editorial capsule progress indicator
   A slim column of thin capsules on the far left that fill
   from gray to yellow in step with scroll. Landmarks every
   ~9 capsules give the rail rhythm. Desktop only.
   ============================================================ */
.scroll-rail {
  position: fixed;
  left: 22px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 90;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  pointer-events: none;   /* purely decorative navigation cue */
}

.rail-cap {
  width: 14px;
  height: 2px;
  border-radius: 999px;
  background: var(--hairline-strong);
  /* keep transitions short & non-elastic; color/width only */
  transition: background-color 160ms linear, width 160ms linear,
              box-shadow 160ms linear;
  /* No will-change here: background-color gains nothing from the hint,
     and 50 promoted capsules cost GPU memory for the whole session. */
}

/* Landmark capsules — slightly longer, act as visual anchors. */
.rail-cap.is-landmark {
  width: 22px;
}

/* Filled state (driven per-frame via inline background-color for a
   continuous gray→yellow blend; this class handles the discrete bits). */
.rail-cap.is-active-landmark {
  width: 24px;                                   /* +2px emphasis only */
  box-shadow: 0 0 0 1px #885DCE;                 /* thin 1px outline */
}

@media (prefers-reduced-motion: reduce) {
  .rail-cap { transition: none; }
}

@media (max-width: 768px) {
  .scroll-rail { display: none; }
}

/* The old fixed header (.site-header / .header-inner / .brand / .main-nav)
   has been deleted outright, styles included — navigation now lives in
   the hero composition. See assets/css/masthead.css. */

/* ============================================================
   Buttons
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 14px 30px;
  border-radius: 999px;
  font-size: 0.95rem;
  font-weight: 600;
  font-family: var(--font-body);
  transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease), background 0.3s var(--ease);
}

.btn-primary {
  background: var(--purple);
  color: var(--on-accent);
}

.btn-primary:hover {
  background: var(--purple-soft);
  transform: translateY(-2px);
  box-shadow: 0 10px 30px -10px rgba(136, 93, 206, 0.55);
}

.btn-ghost {
  border: 1px solid var(--charcoal-line);
  color: var(--ink);
}

.btn-ghost:hover {
  border-color: var(--yellow);
  color: var(--yellow);
  transform: translateY(-2px);
}

/* ============================================================
   Placeholder / future-section marker
   (used only for the scaffold preview — remove when building
   real sections)
   ============================================================ */

.scaffold-note {
  border: 1px dashed var(--charcoal-line);
  border-radius: var(--radius-md);
  padding: 40px var(--gutter);
  text-align: center;
  color: var(--ink-faint);
  font-size: 0.9rem;
}

/* ============================================================
   Footer
   ============================================================ */

.site-footer {
  padding: 50px var(--gutter) 34px;
  text-align: center;
  color: var(--ink-faint);
  font-size: 0.85rem;
  border-top: 1px solid var(--charcoal-line);
}

.site-footer .footer-brand {
  color: var(--yellow);
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: 0.08em;
}

/* ===== sections.css ===== */
/* ============================================================
   Hero
   ============================================================ */

.hero {
  min-height: 86vh;
  display: flex;
  align-items: center;
  padding-top: 80px;
  padding-bottom: 80px;
  position: relative;
}

.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(600px circle at 15% 20%, rgba(136, 93, 206, 0.14), transparent 60%),
    radial-gradient(500px circle at 85% 75%, rgba(253, 233, 98, 0.08), transparent 60%);
  pointer-events: none;
}

.hero-inner {
  position: relative;
  max-width: 760px;
}


.hero-title {
  font-size: clamp(2rem, 6vw, 4.2rem);
  color: var(--ink);
  margin-bottom: 26px;
  line-height: 1.08;
  overflow-wrap: break-word;
  hyphens: none;
}

.hero-line {
  display: block;
}

/* Word wrapper: keeps every word on a single line so the heading
   only ever breaks at spaces between words, never mid-letter.
   The character reveal still animates each .char inside it. */
.hero-title .word {
  display: inline-block;
  white-space: nowrap;
}

.text-yellow { color: var(--yellow); }
.text-purple-highlight { color: var(--purple-soft); }

/* --- Premium character-reveal entrance ---
   Each character is wrapped in a .char span by script.js.
   Words (and the whole heading) still wrap normally because
   only individual characters are spans; the space characters
   between them remain plain text nodes. */
.hero-title .char {
  display: inline-block;
  opacity: 0;
  transform: translateY(14px) scale(0.98);
  filter: blur(10px);
  will-change: transform, opacity, filter;
}

.hero-title.animate .char {
  animation: charReveal 0.85s cubic-bezier(0.22, 1, 0.36, 1) forwards;
  animation-delay: calc(var(--i) * 0.045s);
}

@keyframes charReveal {
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
    filter: blur(0px);
  }
}

/* Once the reveal is done, hand the characters back to the browser.
   Without this, 46 spans keep a filter and a will-change hint for the
   life of the page — each one pinned to its own compositing layer for
   an animation that finished seconds ago. */
.hero-title.chars-settled .char {
  filter: none;
  will-change: auto;
  animation: none;
  opacity: 1;
  transform: none;
}

.hero-lede,
.hero-actions {
  opacity: 0;
  transform: translateY(16px);
}

.hero-lede {
  font-size: 1.1rem;
  color: var(--ink-muted);
  max-width: 540px;
  margin-bottom: 40px;
}

.hero-lede.animate-in {
  animation: fadeUp 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

.hero-actions {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  transform: translateY(18px) scale(0.98);
}

.hero-actions.animate-in {
  animation: fadeUpScale 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

@keyframes fadeUp {
  to { opacity: 1; transform: translateY(0); }
}

@keyframes fadeUpScale {
  to { opacity: 1; transform: translateY(0) scale(1); }
}

/* Reduced motion: show everything immediately, no blur/stagger/motion */
@media (prefers-reduced-motion: reduce) {
  .hero-title .char {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    animation: none !important;
  }
  .hero-lede,
  .hero-actions {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }
}

.hero-scroll-cue {
  position: absolute;
  z-index: 2;
  bottom: 30px;
  left: 50%;
  transform: translateX(-50%);
  width: 18px;
  height: 29px;
  border: 1.2px solid var(--charcoal-line);
  border-radius: 16px;
}

.hero-scroll-cue span {
  position: absolute;
  top: 5px;
  left: 50%;
  transform: translateX(-50%);
  width: 3px;
  height: 6px;
  border-radius: 2px;
  background: var(--yellow);
  animation: scrollcue 1.8s ease-in-out infinite;
}

/* Driven by transform, not `top`. Animating `top` re-ran layout on every
   frame of an infinite loop; translateY is composited and costs nothing. */
@keyframes scrollcue {
  0%   { transform: translate(-50%, 0);    opacity: 1; }
  60%  { transform: translate(-50%, 10px); opacity: 0.3; }
  100% { transform: translate(-50%, 0);    opacity: 0; }
}

/* ============================================================
   About
   ============================================================ */

.about-grid {
  display: grid;
  grid-template-columns: 1.5fr 1fr;
  gap: 64px;
  align-items: start;
}

.about-text {
  color: var(--ink-muted);
  margin-bottom: 18px;
  max-width: 560px;
}

.about-subhead {
  font-family: var(--font-display);
  font-size: 1rem;
  color: var(--ink);
  margin: 36px 0 16px;
}

.experience-list {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.experience-list li {
  display: flex;
  flex-direction: column;
  padding: 16px 20px;
  background: var(--charcoal-soft);
  border-radius: var(--radius-sm);
  border-left: 3px solid var(--purple);
  transition: transform 0.3s var(--ease), border-color 0.3s var(--ease);
}

.experience-list li:hover {
  transform: translateX(6px);
  border-color: var(--yellow);
}

.exp-role {
  font-weight: 600;
  color: var(--ink);
  font-size: 0.95rem;
}

.exp-org {
  color: var(--ink-faint);
  font-size: 0.85rem;
  margin-top: 2px;
}

.about-side {
  display: flex;
  flex-direction: column;
  gap: 18px;
  position: sticky;
  top: 140px;
}

.about-card {
  background: var(--charcoal);
  border: 1px solid var(--charcoal-line);
  border-radius: var(--radius-md);
  padding: 24px 26px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease);
}

.about-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 18px 40px -20px rgba(0,0,0,0.5);
}

.about-card-label {
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--purple-soft);
  font-weight: 600;
}

.about-card-value {
  color: var(--ink);
  font-size: 0.98rem;
  font-weight: 500;
}

/* ============================================================
   Services — split layout with LEEWAY network visualization
   Left: editorial service list. Right: an SVG/DOM network where
   five service nodes feed energy particles into a central
   frosted-glass LEEWAY node. Ambient mesh + grain + vignette.
   ============================================================ */

.services-split {
  position: relative;
  display: grid;
  grid-template-columns: 45% 55%;
  gap: 40px;
  align-items: center;
  min-height: 560px;
}

/* --- ambient background layers (scoped to this section) --- */
#services {
  position: relative;
  overflow: hidden;
}

.services-mesh,
.services-grain,
.services-vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
}

/* Matte charcoal. Depth comes from tonal shift alone — no colour bloom,
   no blur, no motion. A blurred drifting gradient reads as a screensaver;
   a still, evenly-lit surface reads as material. Static layers are
   composited once and never repaint. */
/* Base is --charcoal, NOT --charcoal-deep. The sections either side of
   this one (about, portfolio) are both alt/deep; using deep here made
   three consecutive sections paint the identical colour and the whole
   run read as one flat slab with no background at all. The lighter base
   restores the alternation and lets this surface be seen. */
.services-mesh {
  background:
    linear-gradient(168deg, rgba(255, 255, 255, 0.032) 0%, transparent 48%),
    radial-gradient(92% 72% at 78% 108%, rgba(0, 0, 0, 0.30), transparent 64%),
    var(--charcoal);
}

/* Fine static grain. This is what gives the surface its matte, papery
   finish instead of a flat digital fill. No blend mode: plain alpha is
   cheaper to composite and reads the same at this opacity. */
.services-grain {
  opacity: var(--grain-strength);
  background-image: var(--grain-tile);
  background-size: 150px 150px;
}

.services-vignette {
  background: radial-gradient(120% 120% at 50% 42%, transparent 55%, rgba(14, 14, 14, 0.42) 100%);
}

#services .container { position: relative; z-index: 1; }

/* --- left column: editorial list --- */
.services-intro { max-width: 440px; }

.services-lede {
  color: var(--ink-muted);
  font-size: 1.02rem;
  margin: 4px 0 34px;
  max-width: 400px;
}

.services-list {
  display: flex;
  flex-direction: column;
}

.svc-row {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: 14px;
  padding: 16px 0;
  border-top: 1px solid var(--hairline);
}
.svc-row:last-child { border-bottom: 1px solid var(--hairline); }

.svc-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  align-self: center;
  box-shadow: 0 0 0 4px rgba(136, 93, 206, 0.08);
}

.svc-row-text { display: flex; flex-direction: column; gap: 2px; }
.svc-name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.02rem;
  color: var(--ink);
  letter-spacing: -0.01em;
}
.svc-note { font-size: 0.86rem; color: var(--ink-faint); }

/* --- right column: network stage --- */
.services-network {
  position: relative;
  width: 100%;
  height: 520px;
}

.services-network svg.net-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.net-pipe {
  fill: none;
  stroke: rgba(136, 93, 206, 0.35);
  stroke-width: 1.5;
  stroke-linecap: round;
}

/* particles layer sits above the lines, below the nodes */
.net-particles {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: visible;
}

.net-particle {
  position: absolute;
  top: 0;
  left: 0;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--purple-soft);
  will-change: transform, opacity;
}

.net-particle.warm { background: var(--yellow); }

/* --- nodes --- */
.net-node {
  position: absolute;
  transform: translate(-50%, -50%);
  padding: 12px 18px;
  border-radius: 14px;
  background: var(--surface-raised);
  border: 1px solid var(--hairline);
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 0.86rem;
  color: var(--ink);
  white-space: nowrap;
  cursor: default;
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease);
  z-index: 2;
  opacity: 0;              /* revealed on scroll entrance */
}
.net-node:hover {
  border-color: rgba(161, 132, 221, 0.9);
  background: rgba(56, 56, 56, 0.7);
  cursor: pointer;
}
.net-node .node-dot {
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 50%;
  margin-right: 8px;
  vertical-align: middle;
}

/* --- center LEEWAY node --- */
.net-center {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 132px;
  height: 132px;
  border-radius: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  background: var(--surface-raised);
  border: 1px solid var(--purple);
  box-shadow: inset 0 0 24px rgba(136, 93, 206, 0.16);
  z-index: 3;
  opacity: 0;              /* revealed on scroll entrance */
}
.net-center .center-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.02rem;
  letter-spacing: 0.14em;
  color: var(--ink);
}
.net-center .center-caption {
  font-family: var(--font-body);
  font-size: 0.62rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* breathing halo — expands 2-3px when a particle arrives */
.net-halo {
  position: absolute;
  top: 50%; left: 50%;
  width: 132px;
  height: 132px;
  margin: -66px 0 0 -66px;
  border-radius: 50%;
  border: 1px solid rgba(136, 93, 206, 0.4);
  opacity: 0.5;
  pointer-events: none;
  will-change: transform, opacity;
}

/* --- responsive --- */
@media (max-width: 900px) {
  .services-split { grid-template-columns: 1fr; gap: 24px; }
  .services-network { height: 460px; margin-top: 8px; }
  .net-node { font-size: 0.8rem; padding: 10px 14px; }
}

/* --- full service catalogue (card grid below the network) --- */
.services-catalogue {
  margin-top: 88px;
}

.catalogue-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.3rem, 2.2vw, 1.7rem);
  color: var(--ink);
  letter-spacing: -0.01em;
}
.catalogue-lede {
  color: var(--ink-muted);
  font-size: 0.98rem;
  max-width: 460px;
  margin: 8px 0 34px;
}

.services-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

.svc-cat-card {
  background: var(--surface-raised);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  padding: 26px;
  cursor: pointer;
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease), transform 0.3s var(--ease);
}
.svc-cat-card:hover {
  transform: translateY(-4px);
  border-color: rgba(161, 132, 221, 0.85);
  background: rgba(48, 48, 48, 0.62);
}
.svc-cat-card.open {
  border-color: var(--purple);
  background: rgba(40, 34, 56, 0.6);
}

.svc-cat-head {
  display: flex;
  align-items: center;
  gap: 14px;
}
.svc-cat-icon {
  width: 44px; height: 44px;
  border-radius: 50%;
  background: var(--surface-sunken);
  border: 1px solid var(--hairline);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.2rem;
  flex-shrink: 0;
}
.svc-cat-title {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--ink);
  flex: 1;
  line-height: 1.2;
}
.svc-cat-toggle {
  width: 28px; height: 28px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.16);
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-muted);
  font-size: 1.05rem;
  flex-shrink: 0;
  transition: transform 0.35s var(--ease), color 0.3s var(--ease), border-color 0.3s var(--ease);
}
.svc-cat-card.open .svc-cat-toggle {
  transform: rotate(45deg);
  color: var(--purple-soft);
  border-color: var(--purple);
}
.svc-cat-desc {
  color: var(--ink-muted);
  font-size: 0.9rem;
  margin-top: 16px;
}
.svc-cat-body {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.45s var(--ease);
}
.svc-cat-card.open .svc-cat-body { max-height: 260px; }
.svc-cat-list {
  margin-top: 16px;
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.svc-cat-list li {
  display: flex; align-items: center; gap: 10px;
  font-size: 0.86rem;
  color: var(--ink);
}
.svc-cat-list li::before {
  content: "";
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--purple-soft);
  flex-shrink: 0;
}

@media (max-width: 900px) {
  .services-grid { grid-template-columns: repeat(2, 1fr); }
  .services-catalogue { margin-top: 56px; }
}
@media (max-width: 640px) {
  .services-grid { grid-template-columns: 1fr; }
}


/* ============================================================
   Industries — tag cloud
   ============================================================ */

.industries-track {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
}

.industry-tag {
  padding: 14px 24px;
  border-radius: 999px;
  background: var(--charcoal-soft);
  border: 1px solid var(--charcoal-line);
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--ink);
  transition: background 0.3s var(--ease), color 0.3s var(--ease), transform 0.3s var(--ease), border-color 0.3s var(--ease);
}

.industry-tag:hover {
  background: var(--yellow);
  color: var(--purple);
  border-color: var(--yellow);
  transform: translateY(-3px) scale(1.03);
}

/* ============================================================
   Portfolio
   ============================================================ */

.portfolio-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
}

.portfolio-card {
  position: relative;
  border-radius: var(--radius-md);
  overflow: hidden;
  aspect-ratio: 4 / 3;
  background: var(--charcoal-soft);
  display: block;
}

.portfolio-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s var(--ease), filter 0.6s var(--ease);
}

.portfolio-card:hover img {
  transform: scale(1.08);
  filter: brightness(0.5);
}

.portfolio-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 22px;
  background: linear-gradient(to top, rgba(38,38,38,0.92) 0%, rgba(38,38,38,0.15) 55%, transparent 100%);
}

.portfolio-title {
  color: var(--ink);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  margin-bottom: 8px;
  transform: translateY(6px);
  opacity: 0.9;
  transition: transform 0.4s var(--ease), opacity 0.4s var(--ease);
}

.portfolio-card:hover .portfolio-title {
  transform: translateY(0);
  opacity: 1;
}

.portfolio-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--yellow);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.35s var(--ease) 0.05s, transform 0.35s var(--ease) 0.05s;
}

.portfolio-card:hover .portfolio-cta {
  opacity: 1;
  transform: translateY(0);
}

/* ============================================================
   Contact
   ============================================================ */

/* Ferrofluid backdrop. Sits above the section's charcoal (and its grain
   layer) but below the content column, which carries z-index 1 already
   via `.section-alt > *`. Left clickable so the shader can track the
   cursor over empty background; the copy and form sit on top and keep
   their own hit areas. */
.contact-fluid,
.section-fluid {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
}

/* Fades the churn out behind the text so nothing competes with it. The
   same falloff on every section keeps the four backdrops reading as one
   effect rather than four separate treatments. */
.contact-fluid,
.section-fluid {
  -webkit-mask-image: radial-gradient(120% 100% at 50% 100%, #000 30%, transparent 78%);
          mask-image: radial-gradient(120% 100% at 50% 100%, #000 30%, transparent 78%);
}

.contact-fluid canvas,
.section-fluid canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Industries paints no charcoal of its own, so — unlike the `.section-alt`
   sections — its content column needs lifting above the backdrop by hand. */
#industries > .container {
  position: relative;
  z-index: 1;
}

/* Portfolio runs several thousand pixels tall, so the bottom-anchored
   radial the short sections use would pool everything at the very end and
   leave the heading bare. A tall section instead gets an even wash with
   soft fades at both ends. Blob size is held to the same scale as the
   short sections by `data-fluid-fit-height` (see contact-fluid.js). */
.section-fluid--tall {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 10%, #000 90%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, #000 10%, #000 90%, transparent 100%);
}

.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1.1fr;
  gap: 60px;
  align-items: start;
}

.contact-links {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-top: 36px;
}

.contact-link {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 20px;
  background: var(--charcoal-soft);
  border: 1px solid var(--charcoal-line);
  border-radius: var(--radius-sm);
  font-size: 0.95rem;
  font-weight: 500;
  transition: transform 0.3s var(--ease), border-color 0.3s var(--ease), background 0.3s var(--ease);
}

.contact-link:hover {
  transform: translateX(6px);
  border-color: var(--purple);
  background: var(--charcoal);
}

.contact-link-icon {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--purple);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  flex-shrink: 0;
}

.contact-form {
  background: var(--charcoal);
  border: 1px solid var(--charcoal-line);
  border-radius: var(--radius-lg);
  padding: 40px;
  position: relative;
  overflow: hidden;
}

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

.form-row label {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ink-muted);
  letter-spacing: 0.02em;
}

.form-row input,
.form-row select,
.form-row textarea {
  background: var(--charcoal-soft);
  border: 1px solid var(--charcoal-line);
  border-radius: var(--radius-sm);
  padding: 13px 16px;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 0.95rem;
  resize: vertical;
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease);
}

.form-row input::placeholder,
.form-row textarea::placeholder {
  color: var(--ink-faint);
}

.form-row input:focus,
.form-row select:focus,
.form-row textarea:focus {
  border-color: var(--purple);
  background: var(--charcoal);
  outline: none;
}

.form-submit {
  justify-content: center;
  margin-top: 6px;
  position: relative;
  transition: transform 0.25s var(--ease), background 0.25s var(--ease);
}

.form-submit .btn-spinner {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid rgba(245, 244, 242, 0.35);
  border-top-color: var(--ink);
  display: none;
  animation: spin 0.7s linear infinite;
}

.form-submit.is-loading .btn-label { opacity: 0.6; }
.form-submit.is-loading .btn-spinner { display: inline-block; }
.form-submit.is-loading { pointer-events: none; transform: scale(0.98); }

@keyframes spin {
  to { transform: rotate(360deg); }
}

.form-note {
  font-size: 0.85rem;
  color: var(--ink-faint);
  min-height: 1.2em;
  text-align: center;
}

/* --- Fields <-> Success swap --- */
.form-fields {
  display: flex;
  flex-direction: column;
  gap: 20px;
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease);
}

.js-form.is-submitted .form-fields {
  opacity: 0;
  transform: translateY(-8px) scale(0.98);
  pointer-events: none;
  position: absolute;
  visibility: hidden;
}

.form-success {
  display: none;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 20px 0 8px;
  opacity: 0;
  transform: translateY(10px) scale(0.97);
}

.js-form.is-submitted .form-success {
  display: flex;
  animation: successIn 0.55s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

@keyframes successIn {
  to { opacity: 1; transform: translateY(0) scale(1); }
}

.success-check {
  width: 68px;
  height: 68px;
  margin-bottom: 18px;
}

.success-check-circle {
  stroke: var(--yellow);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-dasharray: 145;
  stroke-dashoffset: 145;
}

.js-form.is-submitted .success-check-circle {
  animation: drawCircle 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

.success-check-mark {
  stroke: var(--yellow);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 36;
  stroke-dashoffset: 36;
}

.js-form.is-submitted .success-check-mark {
  animation: drawCheck 0.4s cubic-bezier(0.22, 1, 0.36, 1) 0.55s forwards;
}

@keyframes drawCircle {
  to { stroke-dashoffset: 0; }
}

@keyframes drawCheck {
  to { stroke-dashoffset: 0; }
}

.success-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.25rem;
  color: var(--ink);
  margin-bottom: 8px;
}

.success-sub {
  color: var(--ink-muted);
  font-size: 0.92rem;
  max-width: 320px;
  margin-bottom: 26px;
}

@media (prefers-reduced-motion: reduce) {
  .form-fields,
  .form-success,
  .success-check-circle,
  .success-check-mark {
    animation: none !important;
    transition: none !important;
  }
  .js-form.is-submitted .form-success { opacity: 1; transform: none; }
  .js-form.is-submitted .success-check-circle,
  .js-form.is-submitted .success-check-mark {
    stroke-dashoffset: 0;
  }
}

/* ===== modal.css ===== */
/* ============================================================
   Privacy Confirmation Modal
   Glassmorphism popup shown once per session when the Contact
   section scrolls into view. Locks the contact form until the
   visitor accepts (or explicitly dismisses) the privacy notice.
   ============================================================ */

.privacy-modal-overlay {
  position: fixed;
  inset: 0;
  /* Above the sticky bar (1000) and the floating toggles (1200), below
     the intro overlay. Previously 1000, which let those controls float
     on top of an open modal. */
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(20, 19, 18, 0.55);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s var(--ease), visibility 0s linear 0.35s;
}

.privacy-modal-overlay.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.35s var(--ease);
}

/* Blur the page content behind the modal without touching the
   modal panel itself, which lives outside the blurred element. */
body.privacy-modal-active .sticky-nav,
body.privacy-modal-active main,
body.privacy-modal-active .site-footer {
  filter: blur(6px) saturate(0.9);
  transition: filter 0.35s var(--ease);
}

/* The floating controls step aside rather than blurring — a blurred
   button still reads as clickable. */
body.privacy-modal-active .theme-toggle,
body.privacy-modal-active .nav-toggle {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s var(--ease);
}

body.privacy-modal-active {
  overflow: hidden;
}

.privacy-modal {
  position: relative;
  width: 100%;
  max-width: 460px;
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  background: linear-gradient(180deg, rgba(56, 56, 56, 0.72), rgba(38, 38, 38, 0.82));
  -webkit-backdrop-filter: blur(24px) saturate(1.4);
  backdrop-filter: blur(24px) saturate(1.4);
  border: 1px solid rgba(245, 244, 242, 0.12);
  border-radius: var(--radius-lg);
  padding: 36px 32px 28px;
  box-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.03) inset;
  transform: scale(0.92) translateY(14px);
  opacity: 0;
  transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.35s var(--ease);
}

.privacy-modal-overlay.is-open .privacy-modal {
  transform: scale(1) translateY(0);
  opacity: 1;
}

.privacy-modal-icon {
  width: 52px;
  height: 52px;
  margin: 0 auto 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(136, 93, 206, 0.16);
  border: 1px solid rgba(136, 93, 206, 0.35);
}

.privacy-modal-icon svg {
  width: 26px;
  height: 26px;
  stroke: var(--purple-soft);
}

.privacy-modal-title {
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 600;
  text-align: center;
  color: var(--ink);
  margin-bottom: 8px;
}

.privacy-modal-subtitle {
  font-size: 0.92rem;
  line-height: 1.5;
  text-align: center;
  color: var(--ink-muted);
  margin-bottom: 20px;
}

.privacy-modal-notice {
  font-size: 0.85rem;
  line-height: 1.65;
  color: var(--ink-muted);
  background: rgba(0, 0, 0, 0.18);
  border: 1px solid rgba(245, 244, 242, 0.08);
  border-radius: var(--radius-sm);
  padding: 16px 18px;
  max-height: 180px;
  overflow-y: auto;
  margin-bottom: 20px;
}

.privacy-modal-checkbox {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 24px;
  cursor: pointer;
}

.privacy-modal-checkbox input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  margin-top: 1px;
  border-radius: 6px;
  border: 1.5px solid var(--charcoal-line);
  background: var(--charcoal-soft);
  cursor: pointer;
  position: relative;
  transition: border-color 0.25s var(--ease), background 0.25s var(--ease);
}

.privacy-modal-checkbox input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--yellow);
  outline-offset: 2px;
}

.privacy-modal-checkbox input[type="checkbox"]:checked {
  background: var(--purple);
  border-color: var(--purple);
}

.privacy-modal-checkbox input[type="checkbox"]:checked::after {
  content: "";
  position: absolute;
  left: 6px;
  top: 2px;
  width: 5px;
  height: 10px;
  border: solid var(--ink);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.privacy-modal-checkbox span {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--ink-muted);
}

.privacy-modal-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.privacy-modal-actions .btn {
  width: 100%;
  justify-content: center;
}

.privacy-modal-actions .btn-primary:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.privacy-modal-actions .btn-primary:disabled:hover {
  background: var(--purple);
  transform: none;
  box-shadow: none;
}

@media (max-width: 480px) {
  .privacy-modal { padding: 28px 22px 22px; }
  .privacy-modal-title { font-size: 1.2rem; }
}

/* --- Locked contact form state --- */
.contact-form.is-locked .form-fields {
  position: relative;
}

.contact-form.is-locked .form-fields fieldset {
  opacity: 0.4;
  pointer-events: none;
  filter: grayscale(0.3);
  transition: opacity 0.3s var(--ease), filter 0.3s var(--ease);
}

.privacy-required-notice {
  display: none;
  align-items: center;
  gap: 8px;
  font-size: 0.85rem;
  color: var(--yellow);
  background: rgba(253, 233, 98, 0.08);
  border: 1px solid rgba(253, 233, 98, 0.25);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  margin-top: 4px;
}

.privacy-required-notice.is-visible {
  display: flex;
  animation: fadeUp 0.4s var(--ease) forwards;
}

.privacy-required-notice button {
  background: none;
  border: none;
  color: var(--yellow);
  font-weight: 600;
  text-decoration: underline;
  cursor: pointer;
  font-size: 0.85rem;
  padding: 0;
  flex-shrink: 0;
  margin-left: auto;
}

@media (prefers-reduced-motion: reduce) {
  .privacy-modal-overlay,
  .privacy-modal,
  body.privacy-modal-active .sticky-nav,
  body.privacy-modal-active main,
  body.privacy-modal-active .site-footer {
    transition: none !important;
  }
}

/* (An orphaned `overflow: hidden;` block sat here with no selector. Its
   stray brace made the CSS parser swallow the next rule whole. The modal
   scroll-lock it duplicated already exists above.) */

.newsletter-inner {
  background: var(--charcoal-soft);
  border: 1px solid var(--charcoal-line);
  border-radius: var(--radius-lg);
  padding: 56px clamp(28px, 5vw, 72px);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 40px;
  align-items: center;
  /* Clips the ferrofluid canvas to the card's rounded corners. */
  position: relative;
  overflow: hidden;
}

/* Copy and form ride above the fluid backdrop. */
.newsletter-copy,
.newsletter-inner .newsletter-form {
  position: relative;
  z-index: 1;
}

.newsletter-copy .eyebrow { margin-bottom: 12px; }
.newsletter-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.5rem, 2.8vw, 2.1rem);
  color: var(--ink);
  letter-spacing: -0.01em;
  margin-bottom: 12px;
}
.newsletter-sub {
  color: var(--ink-muted);
  font-size: 1rem;
  max-width: 420px;
}

.newsletter-form { position: relative; }

.newsletter-form .form-fields {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.newsletter-form .nl-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.newsletter-form label {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.newsletter-form input {
  width: 100%;
  background: var(--charcoal);
  border: 1px solid var(--charcoal-line);
  border-radius: var(--radius-sm);
  padding: 13px 16px;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 0.95rem;
  transition: border-color 0.25s var(--ease);
}
.newsletter-form input::placeholder { color: var(--ink-faint); }
.newsletter-form input:focus {
  outline: none;
  border-color: var(--purple);
}
.newsletter-form .form-submit { margin-top: 4px; width: 100%; }
.newsletter-form .form-note {
  font-size: 0.78rem;
  color: var(--ink-faint);
  margin-top: 2px;
}

.newsletter-form .form-success { padding: 8px 0; }

@media (max-width: 820px) {
  .newsletter-inner { grid-template-columns: 1fr; gap: 26px; padding: 40px 26px; }
}

/* (An orphaned `opacity/transform` block sat here with no selector. Its
   stray brace consumed the whole @media (max-width: 980px) block that
   follows, which is why the mobile layout never collapsed to one column.) */

/* ===== responsive.css ===== */
/* ============================================================
   Responsive
   ============================================================ */

/* ============================================================
   Responsive — tablet → mobile
   Breakpoints: 980px (tablet), 768px (large mobile), 480px (small mobile)
   ============================================================ */

/* ── Tablet (≤ 980px) ── */
@media (max-width: 980px) {
  /* Grids → single column */
  .about-grid,
  .contact-grid {
    grid-template-columns: 1fr;
    gap: 36px;
  }
  .about-side { position: static; }

  /* Portfolio 2-col */
  .portfolio-grid { grid-template-columns: repeat(2, 1fr); }

  /* Services network smaller */
  .services-split { grid-template-columns: 1fr; gap: 24px; }
  .services-network { height: 460px; margin-top: 8px; }
  .net-node { font-size: 0.8rem; padding: 10px 14px; }

  /* Catalogue 2-col */
  .services-grid { grid-template-columns: repeat(2, 1fr); }
  .services-catalogue { margin-top: 56px; }

  /* Newsletter */
  .newsletter-inner { grid-template-columns: 1fr; gap: 26px; padding: 40px 26px; }
}

/* ── Large mobile (≤ 768px) ── */
@media (max-width: 768px) {
  /* Section spacing */
  section { padding: 72px 0; }

  /* Container breathing room */
  :root { --gutter: 5vw; }

  /* Hero */
  .hero { min-height: 92vh; padding-top: 96px; padding-bottom: 56px; }
  .hero-inner { max-width: 100%; }
  .hero-title { font-size: clamp(2rem, 9vw, 3rem); line-height: 1.1; margin-bottom: 20px; }
  .hero-lede { font-size: 1rem; margin-bottom: 28px; }
  .hero-actions { flex-direction: column; gap: 12px; }
  .hero-actions .btn { width: 100%; justify-content: center; }

  /* Scroll-heading animated word — smaller on mobile */
  .scroll-heading { font-size: clamp(1.3rem, 6vw, 2rem); }

  /* About */
  .about-grid { gap: 28px; }
  .about-text { max-width: 100%; }

  /* Services list */
  .services-intro { max-width: 100%; }
  .services-lede { font-size: 0.96rem; }
  .svc-name { font-size: 0.96rem; }

  /* Services network — more compact */
  .services-network { height: 380px; }
  .net-node { font-size: 0.75rem; padding: 8px 12px; border-radius: 10px; }
  .net-center { width: 108px; height: 108px; }
  .center-title { font-size: 0.86rem; }
  .net-halo { width: 108px; height: 108px; margin: -54px 0 0 -54px; }

  /* Services catalogue → 1-col */
  .services-grid { grid-template-columns: 1fr; }

  /* Industries */
  .industries-track { gap: 10px; }
  .industry-tag { font-size: 0.88rem; padding: 11px 18px; }

  /* Portfolio → 1-col */
  .portfolio-grid { grid-template-columns: 1fr; }

  /* Contact */
  .contact-grid { gap: 28px; }
  .contact-form { padding: 24px; border-radius: var(--radius-md); }
  .contact-links { flex-direction: column; gap: 10px; }

  /* Newsletter */
  .newsletter { padding: 0; }
  .newsletter-inner { padding: 32px 20px; border-radius: var(--radius-md); }
  .newsletter-title { font-size: clamp(1.3rem, 5vw, 1.7rem); }

  /* Footer */
  .site-footer { font-size: 0.82rem; padding: 22px var(--gutter); text-align: center; }
}

/* ── Small mobile (≤ 480px) ── */
@media (max-width: 480px) {
  section { padding: 60px 0; }
  :root { --gutter: 4.5vw; }

  /* Hero */
  .hero { padding-top: 84px; }
  .hero-title { font-size: clamp(1.8rem, 10vw, 2.4rem); }
  .hero-lede { font-size: 0.94rem; }

  /* Buttons full-width */
  .btn { padding: 13px 22px; font-size: 0.88rem; }

  /* Services network minimal on tiny screens */
  .services-network { height: 320px; }
  .net-node { font-size: 0.7rem; padding: 7px 10px; }
  .net-center { width: 90px; height: 90px; }
  .center-title { font-size: 0.78rem; letter-spacing: 0.1em; }
  .center-caption { font-size: 0.55rem; }
  .net-halo { width: 90px; height: 90px; margin: -45px 0 0 -45px; }

  /* Catalogue cards */
  .svc-cat-card { padding: 20px; }
  .svc-cat-title { font-size: 0.96rem; }

  /* Contact form */
  .contact-form { padding: 20px; }
  form input, form select, form textarea {
    font-size: 16px;  /* prevents iOS auto-zoom on focus */
  }

  /* Newsletter */
  .newsletter-inner { padding: 26px 16px; }
  .newsletter-title { font-size: 1.3rem; }
  .newsletter-form input { font-size: 16px; } /* prevent iOS zoom */

  /* Section headings smaller */
  .section-heading { font-size: 1.4rem; }
}

/* Liquid blob cursor removed: it morphed border-radius every frame
   (a paint, not a composite) behind a blur(30px) layer, which was the
   single largest source of scroll jank on the page. */

/* ===== intro.css ===== */
/* ============================================================
     INTRO MODULE — stylesheet
     Self-contained cinematic welcome overlay.
     Every selector is namespaced `lw-`. Nothing here touches the
     existing site. To remove the intro entirely, delete this block,
     the #leewayIntro markup, and the intro script block.
     ============================================================ */

/* ------------------------------------------------------------
   Tokens — scoped to the overlay only
------------------------------------------------------------ */
#leewayIntro {
  --lw-black: #000000;
  --lw-ink:   #F5F4F2;          /* the site's own ink — not pure white, which glares on black */
  --lw-font:  'Manrope', 'Segoe UI', system-ui, sans-serif;

  /* Motion — expo-out for arrivals (decisive, no overshoot),
     ease-in for departures (the word is lifted away). */
  --lw-ease-in-view: cubic-bezier(0.22, 1, 0.28, 1);
  --lw-ease-out-view: cubic-bezier(0.55, 0, 0.85, 0.35);
}

/* ------------------------------------------------------------
   Overlay shell
   Hidden by default. Only an armed session reveals it, so a
   refresh (or a browser with no storage entry) never paints black.
------------------------------------------------------------ */
#leewayIntro {
  position: fixed;
  inset: 0;
  z-index: 2147483000;          /* above the site's highest layer (9999, the liquid cursor) */
  display: none;
  place-items: center;
  background: var(--lw-black);
  overflow: hidden;
  cursor: default;
  contain: strict;              /* isolate paint + layout from the page beneath */
}

.lw-intro-armed #leewayIntro { display: grid; }

/* Failsafe. The overlay is opened by an inline <head> script but closed by
   the module at the end of <body>. If that module never runs — a blocked
   script, a throw — the black screen would sit over the site forever. This
   retires it after 12s no matter what. The normal sequence finishes around
   7.9s and removes the node, so this never fires in practice. It animates
   `visibility` only, which the JS dissolve doesn't touch, so the two cannot
   fight over a property. */
@keyframes lwIntroFailsafe {
  from { visibility: visible; }
  to   { visibility: hidden; }
}

.lw-intro-armed #leewayIntro {
  animation: lwIntroFailsafe 0.1s linear 12s forwards;
}

/* Quiet the site's decorative cursor while the intro owns the screen —
   the brief asks for typography and nothing else. */
/* ------------------------------------------------------------
   Stage
   Greeting layer and brand layer share one grid cell, so swapping
   between them is a pure cross-cut with zero layout movement.
------------------------------------------------------------ */
.lw-stage {
  display: grid;
  place-items: center;
  width: 100%;
  padding: 0 6vw;
}

.lw-stage > * { grid-area: 1 / 1; }

/* ------------------------------------------------------------
   Greetings
   Each word sits in its own overflow mask and rises through it.
   All words stack in the same cell, so every one is optically
   centred regardless of its width.
------------------------------------------------------------ */
.lw-greetings {
  display: grid;
  place-items: center;
}

.lw-greetings > .lw-mask { grid-area: 1 / 1; }

.lw-mask {
  display: block;
  overflow: hidden;
  /* The mask height is the line box, so the line box must contain the
     font's full ink bounds or the overflow clips live glyphs. The
     Tight needs ~1.21em (the descender on "Bonjour"); Noto Devanagari
     needs ~1.36em for matras, and Arabic more again for the tanween.
     Per-script values below; this is the Latin floor. */
  line-height: 1.45;
}

/* Optical centring across scripts.
   Every script parks its ink at a different height inside its line box,
   so without this the sequence visibly bobs as it swaps languages
   (measured: ~18px of drift at desktop size). The nudge is a percentage
   of the mask's own height, which is proportional to font-size, so one
   value stays correct at every breakpoint. Applied to the mask, not the
   word, because the word's transform belongs to the animation.
   Latin is the reference and is deliberately left alone, so words with
   descenders still sit naturally rather than being forced to match. */
.lw-mask[lang="ja"] { transform: translate3d(0, 15.67%, 0); }
.lw-mask[lang="ar"] { transform: translate3d(0, -6.86%, 0); }
.lw-mask[lang="hi"] { transform: translate3d(0,   1.23%, 0); }

.lw-word {
  display: block;
  font-family: var(--lw-font);
  font-weight: 500;
  font-size: clamp(2.2rem, 8.8vw, 7rem);
  line-height: 1.45;
  letter-spacing: -0.035em;
  color: var(--lw-ink);
  white-space: nowrap;
  opacity: 0;                    /* JS drives visibility; hidden until animated */
  transform: translate3d(0, 110%, 0);
  will-change: transform, opacity;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Per-script font stacks and optical size correction. CJK, Arabic and
   Devanagari all read larger than Latin at the same em, so they come
   down slightly to keep the sequence visually even. */
.lw-word[lang="ja"] {
  font-family: 'Noto Sans JP', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', Meiryo, sans-serif;
  font-size: clamp(1.9rem, 7.2vw, 5.8rem);
  line-height: 1.5;
  letter-spacing: 0.01em;
}

/* Arabic and Devanagari carry marks well above and below the baseline —
   they need the tallest line box of the set to stay uncut. */
.lw-word[lang="ar"] {
  font-family: 'Noto Sans Arabic', 'Segoe UI', Tahoma, sans-serif;
  font-size: clamp(2rem, 8vw, 6.4rem);
  line-height: 1.8;
  letter-spacing: 0;
}

.lw-word[lang="hi"] {
  font-family: 'Noto Sans Devanagari', 'Nirmala UI', 'Kohinoor Devanagari', sans-serif;
  font-size: clamp(2rem, 8vw, 6.4rem);
  line-height: 1.75;
  letter-spacing: 0;
}

/* ------------------------------------------------------------
   Brand lockup — the reveal
   Heavier weight and open tracking against the greetings' lighter,
   tighter setting: many voices, then one name.
------------------------------------------------------------ */
.lw-brand {
  display: flex;
  flex-wrap: nowrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.42em;
  will-change: transform;
}

.lw-brand .lw-mask { line-height: 1.35; }

.lw-brand-word {
  display: block;
  font-family: var(--lw-font);
  font-weight: 700;
  font-size: clamp(1rem, 3.7vw, 3rem);
  line-height: 1.35;
  letter-spacing: 0.16em;
  text-indent: 0.16em;           /* balance the trailing letter-space so the lockup optically centres */
  text-transform: uppercase;
  color: var(--lw-ink);
  white-space: nowrap;
  opacity: 0;
  transform: translate3d(0, 110%, 0);
  will-change: transform, opacity;
  -webkit-font-smoothing: antialiased;
}

/* Narrow screens: stack the three words rather than shrink them past
   the point where the tracking stops reading as deliberate. */
@media (max-width: 640px) {
  .lw-brand {
    flex-direction: column;
    align-items: center;
    gap: 0.18em;
  }
  .lw-brand-word {
    font-size: clamp(1.3rem, 7vw, 2.2rem);
    letter-spacing: 0.2em;
    text-indent: 0.2em;
  }
}

/* Ultra-wide: cap the type so it never sprawls across the viewport.
   The per-script rules are repeated here because `[lang]` outranks a
   bare `.lw-word` — without them the non-Latin words escape the cap and
   the optical size correction inverts. */
@media (min-aspect-ratio: 21/9) and (min-width: 1600px) {
  .lw-word { font-size: clamp(2.2rem, 5.9vw, 6.3rem); }
  .lw-word[lang="ja"] { font-size: clamp(1.9rem, 4.8vw, 5.2rem); }
  .lw-word[lang="ar"],
  .lw-word[lang="hi"] { font-size: clamp(2rem, 5.4vw, 5.8rem); }
  .lw-brand-word { font-size: clamp(1rem, 2.2vw, 2.5rem); }
}

/* Short viewports (landscape phones) — keep everything on screen.
   Same specificity note as above: the per-script sizes must be
   restated or they overflow the height budget. */
@media (max-height: 420px) {
  .lw-word { font-size: clamp(1.5rem, 9.2vh, 2.9rem); }
  .lw-word[lang="ja"] { font-size: clamp(1.3rem, 7.6vh, 2.4rem); }
  .lw-word[lang="ar"],
  .lw-word[lang="hi"] { font-size: clamp(1.35rem, 8.4vh, 2.6rem); }
  .lw-brand-word { font-size: clamp(0.8rem, 3.9vh, 1.35rem); }
}

/* ===== case-study.css ===== */
/* ============================================================
     CASE STUDY MODULE — stylesheet
     Scoped to the Portfolio section. Namespaced `case-` / `cs-`.
     Nothing here touches the rest of the site.
     ============================================================ */


/* ------------------------------------------------------------
   Rhythm
   Both films sit on one line. One width token drives every
   breakpoint, capped so the pair never outgrows the page.
------------------------------------------------------------ */
.cases {
  --cases-w: min(92vw, 1280px);
  --case-gap: clamp(20px, 2.4vw, 44px);
  --case-radius: 20px;
  /* Sized for a half-width column, not the old full-bleed film. */
  --case-title-size: clamp(1.15rem, 2.1vw, 2rem);
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  /* Two named rows — heading, then film — shared by both articles via
     the subgrid below. Without this the longer title wraps to a second
     line and drags only its own film down, breaking the top edge. */
  grid-template-rows: auto auto;
  align-items: start;
  column-gap: var(--case-gap);
  width: var(--cases-w);
  margin: clamp(48px, 7vh, 96px) auto clamp(88px, 13vh, 150px);
}

.case {
  display: grid;
  grid-row: span 2;
  grid-template-rows: subgrid;
  justify-items: center;
  width: 100%;
}

/* ------------------------------------------------------------
   Case study titling
   Title and index label both take the site face — one typeface runs
   the whole site, separated here by size, weight and tracking only.
------------------------------------------------------------ */
.case-head {
  text-align: center;
  margin-bottom: clamp(20px, 2.6vh, 34px);
  /* Half-width columns can't spare a full site gutter. */
  padding: 0 clamp(8px, 1.4vw, 22px);
}

/* Fallback for engines without subgrid: reserve two title lines so both
   heads occupy the same height and the films still start on one line.
   2.08 = two lines at the 1.04 line-height set on .case-title; the 2.6em
   covers the index label above it and its margin. */
@supports not (grid-template-rows: subgrid) {
  .case-head { min-height: calc(2.08 * var(--case-title-size) + 2.6em); }
}

.case-index {
  display: block;
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: clamp(14px, 2vh, 22px);
}

.case-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--case-title-size);
  line-height: 1.04;
  /* Playfair caps need air, not negative tracking. */
  letter-spacing: 0.005em;
  text-transform: uppercase;
  color: var(--ink);
  margin: 0;
  text-wrap: balance;
}

/* ------------------------------------------------------------
   The frame
   aspect-ratio reserves the exact box before any video byte
   arrives, so the section never shifts (both films are 16:9).
   Black underneath means no flash between poster and first frame.
------------------------------------------------------------ */
.case-frame {
  position: relative;
  width: 100%;
  /* The shared site media ratio, not each film's own. Side by side, two
     frames of different heights read as a mistake; one ratio means both
     films are always the same box. object-fit: cover below fills it. */
  aspect-ratio: var(--media-ar);
  border-radius: var(--case-radius);
  overflow: hidden;
  background: #000;
  box-shadow: 0 40px 90px -34px rgba(0, 0, 0, 0.78);
  /* Safari: keep the video's corners inside the rounded frame. */
  transform: translateZ(0);
}

.case-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity 0.6s var(--ease), filter 0.6s var(--ease);
}

/* When the film ends, settle it back so the replay control leads. */
.case-frame.is-ended .case-video {
  opacity: 0.38;
}

/* ------------------------------------------------------------
   Controls — replay + sound only. No seek, no fullscreen, no PiP.
------------------------------------------------------------ */
.case-frame button {
  -webkit-tap-highlight-color: transparent;
}

.case-replay {
  position: absolute;
  inset: 0;
  margin: auto;
  /* Scaled to the half-width frame — the old size was set against a
     film twice this wide and now reads as an oversized button. */
  width: clamp(50px, 4.4vw, 68px);
  height: clamp(50px, 4.4vw, 68px);
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--ink);
  background: rgba(0, 0, 0, 0.42);
  border: 1px solid rgba(245, 244, 242, 0.3);
  opacity: 0;
  visibility: hidden;
  transform: scale(0.94);
  transition: opacity 0.55s var(--ease), transform 0.55s var(--ease),
              background 0.3s var(--ease), border-color 0.3s var(--ease);
}

.case-frame.is-ended .case-replay {
  opacity: 1;
  visibility: visible;
  transform: scale(1);
}

.case-replay:hover,
.case-replay:focus-visible {
  background: rgba(0, 0, 0, 0.62);
  border-color: rgba(245, 244, 242, 0.6);
}

.case-replay svg { width: 38%; height: 38%; }

.case-sound {
  position: absolute;
  right: clamp(12px, 1.1vw, 18px);
  bottom: clamp(12px, 1.1vw, 18px);
  width: clamp(36px, 2.4vw, 42px);
  height: clamp(36px, 2.4vw, 42px);
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--ink);
  background: rgba(0, 0, 0, 0.5);
  opacity: 0;
  transform: translate3d(0, 8px, 0);
  transition: opacity 0.5s var(--ease), transform 0.5s var(--ease),
              background 0.3s var(--ease);
}

/* The control appears once the film is live, not before. */
.case-frame.is-active .case-sound {
  opacity: 1;
  transform: none;
}

.case-sound:hover,
.case-sound:focus-visible {
  background: rgba(0, 0, 0, 0.72);
}

.case-sound svg { width: 46%; height: 46%; }
.case-sound .ico-on  { display: none; }
.case-sound.is-on .ico-off { display: none; }
.case-sound.is-on .ico-on  { display: block; }

/* Keyboard users must always be able to reach the controls. */
.case-frame button:focus-visible {
  opacity: 1;
  transform: none;
  visibility: visible;
}

/* ------------------------------------------------------------
   Entrance
   Scoped deliberately: the site ships a `.reveal` / `.in-view`
   pair with no CSS behind it, and defining that globally here
   would silently animate (and briefly hide) unrelated sections.
------------------------------------------------------------ */
.has-js .cs-reveal {
  opacity: 0;
  transform: translate3d(0, 26px, 0);
  transition: opacity 0.95s var(--ease), transform 0.95s var(--ease);
  will-change: opacity, transform;
}

.has-js .cs-reveal.is-in {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* ------------------------------------------------------------
   Responsive
------------------------------------------------------------ */
@media (max-width: 1024px) {
  .cases { --cases-w: 94vw; --case-radius: 16px; }
}

/* Below this the pair is too narrow to read as two films — a half of
   700px is a 340px-wide movie. One per row, full width, instead. */
@media (max-width: 700px) {
  .cases {
    --cases-w: calc(100vw - 2 * var(--gutter));
    --case-radius: 18px;
    --case-title-size: clamp(1.5rem, 6.4vw, 2.4rem);
    grid-template-columns: 1fr;
    grid-template-rows: none;
    row-gap: clamp(56px, 9vh, 92px);
  }
  .case {
    display: flex;
    flex-direction: column;
    align-items: center;
    grid-row: auto;
  }
  .case-head {
    min-height: 0;                       /* the subgrid fallback is moot here */
    margin-bottom: clamp(22px, 3.2vh, 40px);
  }
  .case-index { letter-spacing: 0.24em; font-size: 0.66rem; }
}

/* Ultra-wide: the cap above already holds the pair at 1280px, so it
   stays a composed object rather than stretching across the display. */

@media (prefers-reduced-motion: reduce) {
  .cs-reveal { opacity: 1; transform: none; transition: none; }
  .case-video, .case-replay, .case-sound { transition: none; }
}

/* ===== live-work.css ===== */
/* ============================================================
     FEATURED LIVE PROJECTS — stylesheet
     Three live sites as floating previews. Reveal animates the stage;
     hover animates the inner viewport — kept on separate elements so the
     scroll tween and the hover transition can never fight over transform.
     ============================================================ */


/* Two per row. The third simply flows into the next row's first column,
   so its left edge lines up with the item directly above it — a plain
   aligned grid, not a centred odd one out. */
.live-work {
  --flp-radius: 18px;
  --flp-scale: 0.62;            /* JS refines this from the real width */
  --flp-gap: clamp(20px, 2.4vw, 44px);
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  align-items: start;
  justify-items: stretch;
  column-gap: var(--flp-gap);
  row-gap: clamp(44px, 6vh, 84px);
  width: min(92vw, 1280px);
  margin: clamp(30px, 4vw, 54px) auto clamp(86px, 12vh, 150px);
}

.live-work-heading { margin-bottom: 0; }

.flp-item {
  display: block;
  width: 100%;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  /* Pre-reveal state. JS clears these; without JS the CSS fallback below
     restores them so the section is never left invisible. */
  opacity: 0;
  transform: scale(0.96);
  will-change: opacity, transform;
}

.flp-item.is-revealed {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* No JS: show everything immediately rather than an empty section. */
html:not(.has-js) .flp-item { opacity: 1; transform: none; }

/* ------------------------------------------------------------
   The floating preview
------------------------------------------------------------ */
.flp-viewport {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: var(--media-ar);   /* matches the 1440x810 render below */
  overflow: hidden;
  border-radius: var(--flp-radius);
  background: var(--charcoal-deep);
  box-shadow: 0 34px 80px -32px rgba(0, 0, 0, 0.72);
  transform: translateZ(0);
  transition: transform 450ms var(--ease), box-shadow 450ms var(--ease);
}

.flp-item:hover .flp-viewport,
.flp-item:focus-visible .flp-viewport {
  transform: scale(1.03);
  box-shadow: 0 46px 110px -30px rgba(0, 0, 0, 0.82);
}

/* ------------------------------------------------------------
   Fallback plate — sits under the frame, always rendered.
   Doubles as the loading state, so there is never a blank area, and
   remains if a site is unreachable or refuses to be framed.
------------------------------------------------------------ */
.flp-plate {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  background:
    linear-gradient(168deg, rgba(255, 255, 255, 0.035) 0%, transparent 52%),
    var(--charcoal-deep);
}

.flp-plate-name {
  font-family: var(--font-body);
  font-weight: 700;
  /* Halved column, halved plate type. */
  font-size: clamp(1rem, 1.8vw, 1.6rem);
  letter-spacing: -0.025em;
  text-transform: uppercase;
  color: var(--ink);
}

.flp-plate-cue {
  font-family: var(--font-body);
  font-size: 0.64rem;
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Cover variant: a real project image filling the frame edge to edge.
   No inset, no border, no overlay — it reads as the preview itself. */
.flp-plate--cover {
  padding: 0;
  gap: 0;
  background: var(--charcoal-deep);
}

.flp-plate--cover img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  border: 0;
}

/* ------------------------------------------------------------
   The live frame
   Rendered at a true desktop viewport and scaled down, so each site
   serves its desktop layout rather than a squeezed tablet breakpoint.
   pointer-events:none keeps every click on the anchor and stops the
   embedded page from capturing scroll.
------------------------------------------------------------ */
.flp-frame {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  /* 16:9 at the same 1440 logical width, so scaling by width alone lands
     the render exactly on the frame — no gap at the bottom, no overspill. */
  width: 1440px;
  height: 810px;
  border: 0;
  pointer-events: none;
  transform: scale(var(--flp-scale));
  transform-origin: top left;
  opacity: 0;
  transition: opacity 0.7s var(--ease);
}

/* Only revealed once the frame has genuinely loaded. */
.flp-frame.is-live { opacity: 1; }

/* ------------------------------------------------------------
   Caption
------------------------------------------------------------ */
.flp-caption {
  display: flex;
  align-items: baseline;
  gap: 0.75em;
  margin-top: clamp(14px, 1.4vw, 20px);
  font-family: var(--font-body);
  font-size: clamp(0.9rem, 1.05vw, 1.08rem);
  font-weight: 600;
  letter-spacing: -0.012em;
  color: var(--ink);
}

.flp-num {
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  color: var(--purple-soft);
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------
   Responsive
------------------------------------------------------------ */
@media (max-width: 900px) {
  .live-work {
    --flp-radius: 14px;
    width: 94vw;
    row-gap: clamp(36px, 5vh, 62px);
  }
}

/* Below this a half column is under 340px, at which point a whole desktop
   site scaled into it is a texture rather than a preview. One per row. */
@media (max-width: 700px) {
  .live-work {
    --flp-radius: 16px;
    grid-template-columns: 1fr;
    width: 88vw;
    row-gap: clamp(44px, 7vh, 72px);
  }
  .flp-caption { font-size: 0.88rem; }
}

/* ------------------------------------------------------------
   Mobile refinements
   Desktop is untouched — every rule here is inside a phone-width query.
------------------------------------------------------------ */
@media (max-width: 560px) {
  /* Network labels ran past the right edge because they were pinned by
     percentage but sized by their text. Cap the width and let long
     labels wrap instead of pushing out of the viewport. */
  .net-node {
    max-width: 40vw;
    white-space: normal;
    text-align: center;
    line-height: 1.3;
  }

  /* Below 560px the diagram has no room to breathe — five pinned labels
     plus a hub in under 300px reads as clutter, not craft. It is purely
     decorative (aria-hidden), and the service list beside it carries the
     same information, so the section is stronger without it on a phone.
     The particle loop is IntersectionObserver-gated, so a hidden network
     never starts and costs nothing. */
  .services-network { display: none; }

  /* Give the stacked columns real air now that they are single-file. */
  .about-grid,
  .contact-grid { gap: 30px; }

  /* Comfortable reading measure and rhythm on a narrow column. */
  .about-text,
  .section-lede { max-width: 100%; }

  /* Buttons go full width and stack — no more side-by-side squeeze. */
  .hero-actions {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
  }
  .hero-actions .btn { width: 100%; text-align: center; }

  /* Form controls are already full width; make the tap targets generous. */
  form input,
  form select,
  form textarea { padding: 14px 16px; }
}

@media (prefers-reduced-motion: reduce) {
  .flp-item { opacity: 1; transform: none; }
  .flp-viewport,
  .flp-frame { transition: none; }
  .flp-item:hover .flp-viewport,
  .flp-item:focus-visible .flp-viewport { transform: none; }
}

/* ===== portfolio-gallery.css ===== */
/* ============================================================
     PORTFOLIO GALLERY — stylesheet
     A uniform grid. Every thumbnail is the site media ratio, so the
     rows line up top and bottom no matter what shape the source file
     happens to be, and one export size covers every slot.
     ============================================================ */


/* Equal columns, equal boxes. Nothing spans, nothing is offset — every
   left and right edge lines up down the whole gallery. */
/* Two columns at the same field width and gap as Featured live projects,
   so a thumbnail here is exactly the size of a preview there. The width
   overrides the parent .container measure to match that section, which
   sits outside it. */
.pf-gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  column-gap: clamp(20px, 2.4vw, 44px);
  row-gap: clamp(44px, 6vh, 84px);
  align-items: start;
  --pf-field: min(92vw, 1280px);
  width: var(--pf-field);
  /* Centre on the viewport, not on the parent .container's narrower
     measure — the section being matched sits outside that container, so
     staying inside it would both shrink the field and overflow right. */
  margin: clamp(28px, 3vw, 48px) 0 0 calc((100% - var(--pf-field)) / 2);
}

.pf-item {
  display: block;
  color: inherit;
  text-decoration: none;
}

/* The fixed box. aspect-ratio reserves it before the file arrives, so
   the gallery never shifts as images stream in — and because the box no
   longer depends on the image, a stray upload cannot break the row. */
.pf-media {
  display: block;
  aspect-ratio: var(--media-ar);
  overflow: hidden;                 /* contains the hover scale */
  border-radius: 10px;
  background: var(--charcoal-soft); /* neutral while the file loads */
}

.pf-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;                /* fills the box, trims the excess */
  object-position: center;
  transition: transform 0.8s cubic-bezier(0.22, 1, 0.28, 1);
}

.pf-item:hover .pf-media img,
.pf-item:focus-visible .pf-media img {
  transform: scale(1.03);
}

/* Caption: an index line, then the title. Numbering is legitimate here —
   this is a curated sequence, so the numbers carry real information
   about order rather than decorating it. */
.pf-caption {
  margin-top: clamp(12px, 1.1vw, 18px);
}

.pf-index {
  display: flex;
  align-items: baseline;
  gap: 0.7em;
  margin-bottom: 0.42em;
  font-family: var(--font-body);
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.pf-num {
  color: var(--purple-soft);
  font-variant-numeric: tabular-nums;   /* numerals stay in column */
}

.pf-rule {
  flex: 1;
  height: 1px;
  background: var(--charcoal-line);
  transform: scaleX(1);
  transform-origin: left;
}

.pf-title {
  display: block;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: clamp(0.92rem, 1.05vw, 1.1rem);
  line-height: 1.32;
  letter-spacing: -0.014em;
  color: var(--ink);
  text-wrap: balance;
}

/* Closing block: a way out to the full archive, sitting in the last
   columns so the final row resolves instead of trailing off. */
.pf-all {
  align-self: center;
  display: block;
  padding-bottom: clamp(4px, 0.8vw, 12px);
  color: inherit;
  text-decoration: none;
}

.pf-all-label {
  display: block;
  font-family: var(--font-body);
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 0.6em;
}

.pf-all-link {
  display: inline-flex;
  align-items: baseline;
  gap: 0.55em;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: clamp(1.1rem, 1.8vw, 1.6rem);
  letter-spacing: -0.02em;
  color: var(--ink);
  border-bottom: 1px solid var(--charcoal-line);
  padding-bottom: 0.28em;
  transition: border-color 0.4s var(--ease);
}

.pf-all:hover .pf-all-link,
.pf-all:focus-visible .pf-all-link { border-bottom-color: var(--purple-soft); }

/* ------------------------------------------------------------
   Responsive
   The column count drops; the boxes stay the same shape throughout.
------------------------------------------------------------ */
@media (max-width: 1024px) {
  .pf-gallery { --pf-field: 94vw; row-gap: clamp(36px, 5vh, 62px); }
}

/* Mobile: one work per row, full width, tight rhythm. */
@media (max-width: 700px) {
  .pf-gallery {
    grid-template-columns: 1fr;
    --pf-field: 88vw;
    row-gap: clamp(44px, 7vh, 72px);
  }
  .pf-all { align-self: start; margin-top: 4px; }
  .pf-media { border-radius: 8px; }
}

@media (prefers-reduced-motion: reduce) {
  .pf-media img { transition: none; }
  .pf-item:hover .pf-media img,
  .pf-item:focus-visible .pf-media img { transform: none; }
}

/* ===== theme-light.css ===== */
/* ============================================================
     LIGHT THEME — stylesheet
     Set by data-theme on <html>. One selector re-points the same tokens
     the whole site already uses, so nothing downstream needs to know a
     theme exists. Warm off-white rather than pure white: #FFFFFF against
     charcoal type is glary, and the warmth keeps the charcoal-violet
     brand family intact.
     ============================================================ */


:root[data-theme="light"] {
  /* True white. Every neutral below is on the grey axis — no warm bias
     anywhere, so the page reads as genuinely white rather than cream. */
  --charcoal:      #FFFFFF;   /* page */
  --charcoal-deep: #F8F8F8;   /* alt sections — separation only */
  --charcoal-soft: #FFFFFF;   /* raised cards */
  --charcoal-line: #E8E8E8;   /* dividers */

  --ink:       #111111;
  --ink-muted: #5A5A5A;
  --ink-faint: #737373;

  /* Accents unchanged — these are the light-mode brand values, darkened
     only so the display purple and pale yellow clear contrast on a light
     ground. Hue is untouched. */
  --purple:      #5B36A8;
  --purple-soft: #6E45C0;
  --yellow:      #9A7405;

  --surface-raised:  #FFFFFF;
  --surface-sunken:  rgba(0, 0, 0, 0.035);
  --hairline:        rgba(0, 0, 0, 0.08);
  --hairline-strong: rgba(0, 0, 0, 0.16);

  /* Neutral, not the warm near-black used before. */
  --rail-base: 17, 17, 17;
  --rail-fill: 154, 116, 5;

  /* Eased right back: visible grain is what made the old palette read as
     paper stock. On true white it has to be almost imperceptible. */
  --grain-strength: 0.026;
}

/* ------------------------------------------------------------
   Light-mode component overrides
   These paint with literal colours rather than tokens, so the theme
   cannot reach them. Each carried a dark or warm cast that would sit
   badly on white.
------------------------------------------------------------ */

/* Services ambient layers: the dark radial produced a grey smudge on
   white. Re-cast as a near-invisible neutral lift. */
:root[data-theme="light"] .services-mesh {
  background:
    linear-gradient(168deg, rgba(0, 0, 0, 0.014) 0%, transparent 48%),
    radial-gradient(92% 72% at 78% 108%, rgba(0, 0, 0, 0.032), transparent 64%),
    var(--charcoal);
}

:root[data-theme="light"] .services-vignette {
  background: radial-gradient(120% 120% at 50% 42%, transparent 64%, rgba(0, 0, 0, 0.045) 100%);
}

/* Live-project fallback plate. */
:root[data-theme="light"] .flp-plate {
  background:
    linear-gradient(168deg, rgba(0, 0, 0, 0.016) 0%, transparent 52%),
    var(--charcoal-deep);
}

/* Glassmorphism preserved — same blur and saturation, warm dark tint
   swapped for a neutral light one. */
:root[data-theme="light"] .privacy-modal {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.86), rgba(248, 248, 248, 0.93));
  border-color: rgba(0, 0, 0, 0.08);
}

:root[data-theme="light"] .privacy-modal-overlay {
  background: rgba(17, 17, 17, 0.40);
}

/* Depth kept, weight adjusted: the dark-mode shadows are tuned against
   charcoal and read as grey mud on white. Same geometry, lighter ink. */
:root[data-theme="light"] .flp-viewport {
  box-shadow: 0 34px 80px -32px rgba(17, 17, 17, 0.22);
}

:root[data-theme="light"] .flp-item:hover .flp-viewport,
:root[data-theme="light"] .flp-item:focus-visible .flp-viewport {
  box-shadow: 0 46px 110px -30px rgba(17, 17, 17, 0.28);
}

:root[data-theme="light"] .case-frame {
  box-shadow: 0 40px 90px -34px rgba(17, 17, 17, 0.26);
}

/* The hero is exempt.
   It carries a bright video behind a dark scrim, and the logo's sub-line
   is near-white, so a light hero would break both. Re-declaring the dark
   token values here means every descendant inherits them regardless of
   the active theme — no per-element overrides needed. */
.hero {
  --ink:       #F5F4F2;
  --ink-muted: #B7B4AF;
  --ink-faint: #8A8783;
  --yellow:    #FDE962;
  --purple:      #885DCE;
  --purple-soft: #A184DD;
  --charcoal-line: #454545;
  --hairline: rgba(255, 255, 255, 0.10);
}

/* Smooth the swap so it reads as a considered transition rather than a
   flash. Colour only — nothing here moves or reflows. */
:root[data-theme] body,
:root[data-theme] section,
:root[data-theme] .pf-media,
:root[data-theme] .sticky-nav,
:root[data-theme] .mobile-menu {
  transition: background-color 0.45s var(--ease), color 0.45s var(--ease),
              border-color 0.45s var(--ease);
}

/* The floating controls sit over the dark hero at first and over themed
   content afterwards, so they follow the scroll state, not the theme. */
:root.is-scrolled .theme-toggle,
:root.is-scrolled .nav-toggle {
  background: var(--charcoal-soft);
  border-color: var(--charcoal-line);
  color: var(--ink);
}

@media (prefers-reduced-motion: reduce) {
  :root[data-theme] body,
  :root[data-theme] section,
  :root[data-theme] .pf-media,
  :root[data-theme] .sticky-nav,
  :root[data-theme] .mobile-menu { transition: none; }
}

/* ===== masthead.css ===== */
/* ============================================================
     MASTHEAD — stylesheet
     The navigation is part of the hero composition, not a bar above it.
     Logo and nav sit in normal flow at the top of the hero so the whole
     stack (logo → nav → eyebrow → headline → lede) scrolls as one unit
     and shares a single vertical rhythm. A separate slim bar, fixed and
     out of flow, takes over once the hero is behind you.
     ============================================================ */


/* ------------------------------------------------------------
   Hero becomes a single vertical composition
------------------------------------------------------------ */
.hero {
  min-height: 100vh;
  flex-direction: column;
  align-items: stretch;
  padding-top: clamp(34px, 5.5vh, 68px);
  padding-bottom: clamp(56px, 9vh, 96px);
}

/* Masthead pins to the top; the hero copy takes the remaining space and
   optically centres inside it. auto margins do the work — no absolute
   positioning, so nothing can overlap at any viewport height. */
.hero-inner {
  margin-top: auto;
  margin-bottom: auto;
  /* Guarantees clear air between the nav and the headline no matter how
     tall the viewport is; auto margins alone can close this up. */
  padding-top: clamp(18px, 4vh, 52px);
}

.hero-masthead {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Tightened with the smaller logo so the pair reads as one lockup;
     the breathing room now falls between the nav and the headline. */
  /* Balanced: close enough to read as one lockup, open enough to breathe. */
  gap: clamp(15px, 2.1vh, 24px);
  padding: 0 var(--gutter);
}

/* ------------------------------------------------------------
   Logo
------------------------------------------------------------ */
.hero-logo {
  display: block;
  /* Stepped down twice from the original 286px: 214px, now 172px.
     That is 40% below where it started. */
  width: clamp(104px, 12vw, 172px);
  /* width/height on the tag reserve the box before the file loads,
     so the masthead never reflows. */
  animation: mastheadIn 1s cubic-bezier(0.22, 1, 0.28, 1) both;
}

.hero-logo img {
  display: block;
  width: 100%;
  height: auto;
}

/* ------------------------------------------------------------
   Navigation — type only. No pill, no box, no background.
------------------------------------------------------------ */
.hero-nav ul {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: clamp(20px, 2.6vw, 42px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.hero-nav a,
.sticky-links a {
  position: relative;
  display: inline-block;
  font-family: var(--font-body);
  font-size: clamp(0.68rem, 0.78vw, 0.78rem);
  font-weight: 500;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  white-space: nowrap;
  color: rgba(245, 244, 242, 0.78);
  transition: color 0.35s var(--ease);
}

.hero-nav a { animation: mastheadIn 0.9s cubic-bezier(0.22, 1, 0.28, 1) both; }

/* Hairline that draws from the centre. transform only. */
.hero-nav a::after,
.sticky-links a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -7px;
  height: 1px;
  background: currentColor;
  opacity: 0.55;
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 0.45s var(--ease);
}

.hero-nav a:hover,
.hero-nav a:focus-visible,
.hero-nav a.active { color: #F5F4F2; }

.hero-nav a:hover::after,
.hero-nav a:focus-visible::after,
.hero-nav a.active::after,
.sticky-links a:hover::after,
.sticky-links a:focus-visible::after,
.sticky-links a.active::after { transform: scaleX(1); }

/* Staggered arrival — six links, barely perceptible. */
.hero-nav li:nth-child(1) a { animation-delay: 0.30s; }
.hero-nav li:nth-child(2) a { animation-delay: 0.36s; }
.hero-nav li:nth-child(3) a { animation-delay: 0.42s; }
.hero-nav li:nth-child(4) a { animation-delay: 0.48s; }
.hero-nav li:nth-child(5) a { animation-delay: 0.54s; }
.hero-nav li:nth-child(6) a { animation-delay: 0.60s; }

@keyframes mastheadIn {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to   { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------
   Sticky bar — fixed, so revealing it cannot shift layout
------------------------------------------------------------ */
.sticky-nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  background: var(--charcoal);
  border-bottom: 1px solid var(--charcoal-line);
  transform: translate3d(0, -101%, 0);
  opacity: 0;
  visibility: hidden;
  transition: transform 0.55s var(--ease), opacity 0.4s var(--ease),
              visibility 0s linear 0.55s;
}

.sticky-nav.is-visible {
  transform: translate3d(0, 0, 0);
  opacity: 1;
  visibility: visible;
  transition: transform 0.55s var(--ease), opacity 0.4s var(--ease),
              visibility 0s linear 0s;
}

.sticky-inner {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 13px var(--gutter);
  display: flex;
  align-items: center;
  gap: 28px;
}

.sticky-logo {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.94rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
}

.sticky-links { margin-left: auto; }

.sticky-links ul {
  display: flex;
  align-items: center;
  gap: clamp(16px, 1.9vw, 32px);
  margin: 0;
  padding: 0;
  list-style: none;
  /* keep clear of the fixed theme toggle */
  padding-right: 54px;
}

.sticky-links a { color: var(--ink-muted); }
.sticky-links a:hover,
.sticky-links a:focus-visible,
.sticky-links a.active { color: var(--ink); }

/* ------------------------------------------------------------
   Theme toggle — fixed top right, serves both states
------------------------------------------------------------ */
.theme-toggle {
  position: fixed;
  top: clamp(14px, 2vh, 22px);
  right: clamp(14px, 2.2vw, 28px);
  z-index: 1200;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--ink);
  background: rgba(20, 20, 22, 0.42);
  border: 1px solid rgba(245, 244, 242, 0.20);
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease),
              color 0.3s var(--ease);
}

.theme-toggle:hover { border-color: rgba(245, 244, 242, 0.45); }
.theme-toggle svg { width: 17px; height: 17px; }
.theme-toggle .ico-moon { display: none; }
:root[data-theme="light"] .theme-toggle .ico-sun  { display: none; }
:root[data-theme="light"] .theme-toggle .ico-moon { display: block; }

/* ------------------------------------------------------------
   Mobile
------------------------------------------------------------ */
.nav-toggle {
  position: fixed;
  top: clamp(14px, 2vh, 22px);
  left: clamp(14px, 2.2vw, 28px);
  z-index: 1200;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: none;          /* becomes flex at the mobile breakpoint below */
  /* Explicit on both axes. `place-items` alone is wrong here: it expands
     to align-items + justify-items, and justify-items has no effect in
     flexbox — so the main (vertical) axis fell back to flex-start and the
     three bars sat hard against the top of the circle. */
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: var(--ink);
  background: rgba(20, 20, 22, 0.42);
  border: 1px solid rgba(245, 244, 242, 0.20);
}

.nav-toggle:hover { border-color: rgba(245, 244, 242, 0.45); }

.nav-toggle span {
  display: block;
  width: 16px;
  height: 1.5px;
  background: currentColor;
  border-radius: 2px;
  transition: transform 0.35s var(--ease), opacity 0.25s var(--ease);
}

.nav-toggle.open span:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
.nav-toggle.open span:nth-child(2) { opacity: 0; }
.nav-toggle.open span:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }

.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 1100;
  background: var(--charcoal);
  display: grid;
  place-items: center;
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}

.mobile-menu.is-open { opacity: 1; }

/* The class sets display:grid, which beats the [hidden] attribute's UA
   display:none on specificity. The closed menu therefore stayed in the
   layer as a full-screen, fully transparent, pointer-events:auto overlay
   at z-index 1100 — swallowing taps on everything beneath it, which is
   why footer links appeared not to open on a phone.

   menu.js clears `hidden` before adding .is-open, so the fade still runs. */
.mobile-menu[hidden] {
  display: none;
}

.mobile-menu ul {
  list-style: none;
  margin: 0;
  padding: 0;
  text-align: center;
}

.mobile-menu li + li { margin-top: clamp(18px, 3.4vh, 30px); }

.mobile-menu a {
  font-family: var(--font-body);
  font-size: clamp(1.4rem, 6vw, 2.1rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
}

@media (max-width: 860px) {
  .hero-nav,
  .sticky-links { display: none; }
  .nav-toggle { display: flex; }
  .sticky-inner { justify-content: center; padding-right: 0; }
  .sticky-logo { margin: 0 auto; }
  .hero-logo { width: clamp(96px, 28vw, 136px); }
}

@media (min-width: 861px) {
  .mobile-menu { display: none !important; }
}

/* Ultra-wide: hold the logo at a composed size rather than letting it
   grow with the viewport. */
@media (min-width: 1800px) {
  .hero-logo { width: 172px; }
}

@media (prefers-reduced-motion: reduce) {
  .hero-logo,
  .hero-nav a { animation: none; }
  .sticky-nav,
  .mobile-menu,
  .hero-nav a::after,
  .sticky-links a::after { transition: none; }
}

/* ===== hero-video.css ===== */
/* ============================================================
     HERO BACKGROUND VIDEO — stylesheet
     Self-contained. To remove: delete this block and the
     .hero-video-wrap markup in the hero section.
     ============================================================ */


.hero {
  position: relative;
  overflow: hidden;
}

.hero-video-wrap {
  position: absolute;
  inset: 0;
  z-index: 0;
  /* Charcoal, not the light lavender: this hero sits on a dark site, so a
     pale fallback flashed bright for the whole buffering window. Charcoal
     makes the wait invisible. */
  background: var(--charcoal);
}

/* Contrast scrim.
   Every piece of hero type is light — #F5F4F2 title, #B7B4AF lede,
   #C9B6F2 eyebrow — and the clip is bright, so the text was washing out
   into it. This sits between video and copy: heaviest across the left
   two-thirds where the text block lives, easing off to the right so the
   footage still reads. Tinted 26,24,32 rather than neutral black, which
   keeps it in the brand's charcoal-violet family instead of looking like
   a grey wash. Static — no motion, no glow. */
.hero-video-wrap::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* Three layers: a flat base that lifts contrast everywhere, a vertical
     pass for depth at the header and the section seam, and a horizontal
     pass weighted to the left where the text block sits. Past ~90% the
     scrim drops away so the footage reads clearly at the right edge
     rather than being buried under a flat wash. */
  background:
    linear-gradient(180deg,
      rgba(26, 24, 32, 0.34) 0%,
      rgba(26, 24, 32, 0.05) 25%,
      rgba(26, 24, 32, 0.07) 66%,
      rgba(26, 24, 32, 0.52) 100%),
    linear-gradient(98deg,
      rgba(26, 24, 32, 0.62) 0%,
      rgba(26, 24, 32, 0.58) 48%,
      rgba(26, 24, 32, 0.46) 76%,
      rgba(26, 24, 32, 0.14) 92%,
      rgba(26, 24, 32, 0.04) 100%),
    linear-gradient(rgba(26, 24, 32, 0.34), rgba(26, 24, 32, 0.34));
}

/* Narrow screens: the text spans nearly the full width, so the horizontal
   falloff has nowhere to go — even it out instead. */
@media (max-width: 860px) {
  .hero-video-wrap::after {
    background:
      linear-gradient(180deg,
        rgba(26, 24, 32, 0.42) 0%,
        rgba(26, 24, 32, 0.12) 30%,
        rgba(26, 24, 32, 0.16) 64%,
        rgba(26, 24, 32, 0.58) 100%),
      linear-gradient(rgba(26, 24, 32, 0.52), rgba(26, 24, 32, 0.52));
  }
}

/* ------------------------------------------------------------
   Hero palette
   Scoped to .hero only — no other section is affected. The body copy
   and the highlighted word were mid-tones chosen against flat charcoal;
   over moving footage they fell below the 4.5:1 AA threshold. Lifting
   them buys the contrast back without having to black out the video.
------------------------------------------------------------ */
.hero .eyebrow {
  font-size: 0.64rem;
  letter-spacing: 0.2em;
  margin-bottom: 10px;
}

.hero .hero-lede {
  color: #EDEBE7;
}

.hero .text-purple-highlight {
  color: #C9B2F5;
}

.hero .eyebrow-glow {
  color: #D8C9FA;
}

/* 130% height anchored to the top edge: the clip overflows downward and
   keeps its focal point in view rather than centre-cropping it away. */
.hero-video-wrap video {
  width: 100%;
  height: 130%;
  object-fit: cover;
  object-position: top;
  display: block;
}

/* Existing hero children stack above the video. */
.hero > *:not(.hero-video-wrap) {
  position: relative;
  z-index: 10;
}

/* The scroll cue is anchored with position:absolute (bottom:30px,
   left:50%). The rule above would force it to static-flow relative
   positioning and tear it out of place, so its own positioning is
   restored here — it still gets the z-index it needs. */
.hero > .hero-scroll-cue {
  position: absolute;
}

/* ===== wholepage-button.css ===== */
/* ============================================================
     WHOLEPAGE BUTTON — floating WhatsApp CTA
     A vanilla port of the React "subtle border button". This project is
     static HTML/CSS/JS with no build step, so the .tsx and its Tailwind
     utilities could not be used directly; every utility is written out
     as real CSS here.

     Pill outline, a sheen that sweeps across on hover, an amber glow,
     and an amber dot. The dot pulses and rings continuously — it does
     not wait for the cursor.

     Fixed bottom-right on every page. Namespaced `wpb-`: deleting this
     file and its script removes the button with nothing left behind.
     ============================================================ */

.wpb {
  position: fixed;
  /* Clear of the iOS home indicator and any on-screen chrome. */
  right: calc(24px + env(safe-area-inset-right, 0px));
  bottom: calc(24px + env(safe-area-inset-bottom, 0px));
  z-index: 9000;

  display: flex;
  justify-content: center;
  align-items: center;
  gap: 12px;
  height: 48px;
  padding: 0 22px;
  border: 2px solid rgba(255, 255, 255, .7);
  border-radius: 999px;
  overflow: hidden;
  text-decoration: none;
  cursor: pointer;
  background: rgba(0, 0, 0, .35);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  transition: border-color .5s ease, box-shadow .5s ease, transform .5s ease;
}

/* The sheen. Parked off the left edge, swept across on hover. */
.wpb::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .05), transparent);
  transform: translateX(-100%);
  transition: transform .7s ease;
}

/* Amber wash + a second inner ring, both revealed on hover. */
.wpb::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 2px solid rgba(253, 230, 138, 0);
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(253, 230, 138, 0), rgba(253, 230, 138, .1), rgba(253, 230, 138, 0));
  opacity: 0;
  transition: opacity .5s ease, border-color .5s ease;
}

.wpb:hover,
.wpb:focus-visible {
  border-color: #fff;
  box-shadow: 0 10px 15px -3px rgba(255, 255, 255, .2);
  transform: scale(1.05);
}

.wpb:hover::before,
.wpb:focus-visible::before { transform: translateX(100%); }

.wpb:hover::after,
.wpb:focus-visible::after {
  opacity: 1;
  border-color: rgba(253, 230, 138, .3);
}

.wpb:active { transform: scale(.95); }

.wpb__text {
  position: relative;
  z-index: 10;
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: .02em;
  color: #fff;
  white-space: nowrap;
  transition: color .3s ease;
}

.wpb:hover .wpb__text,
.wpb:focus-visible .wpb__text { color: #fffbeb; }

/* ---------- the amber dot ----------
   Always alive: the core breathes and a ring expands out of it on a
   loop, so the button reads as active without the cursor being on it. */
.wpb__dot {
  position: relative;
  z-index: 10;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fde68a;
  animation: wpb-breathe 2s ease-in-out infinite;
  transition: background .5s ease, box-shadow .5s ease, transform .5s ease;
}

/* The expanding ring. */
.wpb__dot::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: #fde68a;
  animation: wpb-ping 2s cubic-bezier(0, 0, .2, 1) infinite;
}

.wpb:hover .wpb__dot,
.wpb:focus-visible .wpb__dot {
  background: #fcd34d;
  box-shadow: 0 10px 15px -3px rgba(252, 211, 77, .5);
  transform: scale(1.1);
}

.wpb:active .wpb__dot { transform: scale(.9); }

@keyframes wpb-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: .75; }
}

@keyframes wpb-ping {
  0%        { transform: scale(1);   opacity: .75; }
  75%, 100% { transform: scale(2.2); opacity: 0; }
}

/* ---------- responsive ---------- */
@media (max-width: 640px) {
  .wpb {
    right: calc(16px + env(safe-area-inset-right, 0px));
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    height: 44px;
    padding: 0 18px;
    gap: 10px;
  }
  .wpb__text { font-size: 13px; }
  .wpb__dot { width: 14px; height: 14px; }
}

@media (prefers-reduced-motion: reduce) {
  .wpb, .wpb::before, .wpb::after,
  .wpb__text, .wpb__dot { transition: none; }
  .wpb__dot { animation: none; }
  .wpb__dot::before { animation: none; opacity: 0; }
  .wpb:hover, .wpb:focus-visible { transform: none; }
}

@media print { .wpb { display: none; } }

/* ===== editorial.css ===== */
/* ============================================================
   EDITORIAL
   Layout for the pages the build generates — service pages, the
   services hub, journal entries, archives and the long-form core
   pages.

   Everything here reads from the existing tokens in
   variables.css and theme-light.css, so these pages inherit the
   homepage's surface, type and colour automatically. Nothing in
   this sheet touches an existing selector, so the homepage
   composition is unaffected.
   ============================================================ */

/* ---- Skip link --------------------------------------------------
   First focusable element on every page. Off-screen until focused,
   which is the accessible pattern — `display:none` would remove it
   from the tab order entirely and defeat the purpose. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 1000;
  padding: 12px 20px;
  background: var(--purple);
  color: #fff;
  font-family: var(--font-body);
  font-size: 0.95rem;
  border-radius: 0 0 var(--radius-sm) 0;
  text-decoration: none;
}
.skip-link:focus {
  left: 0;
}

/* ---- Breadcrumb -------------------------------------------------- */
.breadcrumb {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 104px var(--gutter) 0;
  font-family: var(--font-body);
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
}
.breadcrumb ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.breadcrumb li + li::before {
  content: "/";
  margin-right: 8px;
  color: var(--ink-faint);
  opacity: 0.5;
}
.breadcrumb a {
  color: var(--ink-muted);
  text-decoration: none;
}
.breadcrumb a:hover,
.breadcrumb a:focus-visible {
  color: var(--yellow);
  text-decoration: underline;
}
.breadcrumb [aria-current="page"] {
  color: var(--ink);
}

/* ---- Page head --------------------------------------------------- */
.page-head {
  padding: 40px 0 24px;
}
.breadcrumb + .page-head {
  padding-top: 24px;
}
.page-title {
  font-family: var(--font-display);
  font-size: clamp(2.1rem, 5.4vw, 3.6rem);
  line-height: 1.08;
  letter-spacing: -0.015em;
  color: var(--ink);
  margin: 10px 0 20px;
  max-width: 20ch;
}
.page-lede {
  font-family: var(--font-body);
  font-size: clamp(1.02rem, 1.5vw, 1.18rem);
  line-height: 1.65;
  color: var(--ink-muted);
  max-width: 62ch;
  margin: 0 0 26px;
}
.cta-inline {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 4px;
}

/* ---- Prose ------------------------------------------------------- */
.prose,
.prose-block {
  max-width: 68ch;
}
.prose h2,
.prose-block .section-heading,
.post h2 {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 2.6vw, 2rem);
  line-height: 1.22;
  color: var(--ink);
  margin: 56px 0 18px;
  letter-spacing: -0.01em;
}
.prose h3 {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--ink);
  margin: 30px 0 10px;
}
.prose p,
.prose-block p {
  font-family: var(--font-body);
  font-size: 1.02rem;
  line-height: 1.78;
  color: var(--ink-muted);
  margin: 0 0 18px;
  max-width: 68ch;
}
.prose a,
.prose-block a,
.faq-answer a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--purple-soft);
  text-decoration-thickness: 1px;
}
.prose a:hover,
.prose-block a:hover,
.prose a:focus-visible {
  color: var(--yellow);
  text-decoration-color: var(--yellow);
}
.prose strong {
  color: var(--ink);
  font-weight: 600;
}
.prose section {
  scroll-margin-top: 90px;
}

/* ---- What's included --------------------------------------------- */
.includes,
.techlist,
.faq,
.related,
.cta-band {
  padding: 18px 0;
}
.includes-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 18px;
}
.includes-grid li {
  background: var(--surface-raised);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  padding: 22px 24px;
}
.includes-grid h3 {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 1rem;
  color: var(--ink);
  margin: 0 0 8px;
}
.includes-grid p {
  font-family: var(--font-body);
  font-size: 0.92rem;
  line-height: 1.6;
  color: var(--ink-muted);
  margin: 0;
}

/* ---- Technology tags --------------------------------------------- */
.tech-tags {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.tech-tags li {
  font-family: var(--font-body);
  font-size: 0.86rem;
  letter-spacing: 0.02em;
  color: var(--ink-muted);
  background: var(--surface-sunken);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: 7px 15px;
}

/* ---- FAQ ---------------------------------------------------------
   Native <details>. Keyboard-accessible with no JavaScript, and the
   answer text stays in the DOM when collapsed — which is what makes
   it readable to crawlers and AI retrieval. */
.faq-list {
  max-width: 76ch;
  border-top: 1px solid var(--hairline);
}
.faq-item {
  border-bottom: 1px solid var(--hairline);
}
.faq-item summary {
  cursor: pointer;
  list-style: none;
  padding: 20px 44px 20px 0;
  position: relative;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary h3 {
  display: inline;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 1.02rem;
  line-height: 1.5;
  color: var(--ink);
  margin: 0;
}
.faq-item summary::after {
  content: "";
  position: absolute;
  right: 8px;
  top: 50%;
  width: 11px;
  height: 11px;
  margin-top: -6px;
  border-right: 1.6px solid var(--ink-faint);
  border-bottom: 1.6px solid var(--ink-faint);
  transform: rotate(45deg);
  transition: transform 0.25s var(--ease);
}
.faq-item[open] summary::after {
  transform: rotate(-135deg);
}
.faq-item summary:focus-visible {
  outline: 2px solid var(--yellow);
  outline-offset: 3px;
}
.faq-answer {
  padding: 0 44px 22px 0;
}
.faq-answer p {
  font-family: var(--font-body);
  font-size: 0.98rem;
  line-height: 1.75;
  color: var(--ink-muted);
  margin: 0;
  max-width: 68ch;
}

/* ---- Card grids (services hub, related, sectors, journal) --------- */
.svc-groups { padding-bottom: 20px; }
.svc-group { margin-bottom: 46px; }
.svc-grid,
.related-grid,
.sector-grid {
  list-style: none;
  margin: 22px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(288px, 1fr));
  gap: 18px;
}
.svc-grid li,
.related-grid li,
.sector-grid li {
  background: var(--surface-raised);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  transition: border-color 0.25s var(--ease), transform 0.25s var(--ease);
}
.svc-grid li:hover,
.related-grid li:hover,
.sector-grid li:hover {
  border-color: var(--hairline-strong);
  transform: translateY(-2px);
}
.svc-grid article,
.sector-grid article {
  padding: 24px;
}
.svc-grid h3,
.sector-grid h2 {
  font-family: var(--font-display);
  font-size: 1.22rem;
  line-height: 1.25;
  color: var(--ink);
  margin: 0 0 10px;
}
.svc-grid h3 a,
.sector-grid h2 a {
  color: inherit;
  text-decoration: none;
}
.svc-grid h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
}
.svc-grid li { position: relative; }
.svc-grid p,
.sector-grid p {
  font-family: var(--font-body);
  font-size: 0.92rem;
  line-height: 1.65;
  color: var(--ink-muted);
  margin: 0;
}
.related-grid a {
  display: block;
  padding: 22px 24px;
  text-decoration: none;
}
.rel-name {
  display: block;
  font-family: var(--font-display);
  font-size: 1.15rem;
  color: var(--ink);
  margin-bottom: 7px;
}
.rel-note {
  display: block;
  font-family: var(--font-body);
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--ink-muted);
}

/* ---- CTA band ----------------------------------------------------- */
.cta-band {
  margin: 56px 0 0;
  padding: 54px 0 58px;
  border-top: 1px solid var(--hairline);
}
.cta-actions {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 24px;
}

/* ---- Journal ------------------------------------------------------ */
.post-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 18px;
}
.post-card {
  background: var(--surface-raised);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  padding: 26px 28px;
}
.post-card h2 {
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 2.2vw, 1.6rem);
  line-height: 1.22;
  margin: 8px 0 10px;
}
.post-card h2 a {
  color: var(--ink);
  text-decoration: none;
}
.post-card h2 a:hover { color: var(--yellow); }
.post-card p {
  font-family: var(--font-body);
  font-size: 0.95rem;
  line-height: 1.68;
  color: var(--ink-muted);
  margin: 0 0 10px;
}
.post-meta {
  font-family: var(--font-body);
  font-size: 0.84rem;
  color: var(--ink-faint);
  margin: 0;
}
.post-meta a { color: var(--ink-muted); }
.tag-list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 30px 0 0;
  margin: 30px 0 0;
  border-top: 1px solid var(--hairline);
}
.tag-list a {
  font-family: var(--font-body);
  font-size: 0.84rem;
  color: var(--ink-muted);
  background: var(--surface-sunken);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: 6px 14px;
  text-decoration: none;
}
.tag-list a:hover { color: var(--yellow); border-color: var(--hairline-strong); }

/* ---- Contact block, search form ----------------------------------- */
.contact-block p {
  font-family: var(--font-body);
  font-style: normal;
  font-size: 1rem;
  line-height: 1.8;
  color: var(--ink-muted);
  margin: 0 0 14px;
}
.contact-block a { color: var(--ink); }

.site-search {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  margin: 8px 0 20px;
  max-width: 620px;
}
.site-search label {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
.site-search input[type="search"] {
  flex: 1 1 320px;
  font-family: var(--font-body);
  font-size: 1rem;
  padding: 14px 18px;
  color: var(--ink);
  background: var(--surface-sunken);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
}
.site-search input[type="search"]:focus-visible {
  outline: 2px solid var(--yellow);
  outline-offset: 2px;
}

/* ---- Experience list reuse on /about/ ----------------------------- */
.prose .experience-list {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  display: grid;
  gap: 12px;
}

/* ---- Focus visibility (global uplift) -----------------------------
   The site previously relied on the browser default, which is
   invisible against the charcoal surface in several places. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--yellow);
  outline-offset: 3px;
  border-radius: 3px;
}

/* ---- Responsive ---------------------------------------------------- */
@media (max-width: 720px) {
  .breadcrumb { padding-top: 92px; }
  .page-title { max-width: 100%; }
  .includes-grid,
  .svc-grid,
  .related-grid,
  .sector-grid { grid-template-columns: 1fr; }
  .cta-actions .btn,
  .cta-inline .btn { width: 100%; text-align: center; justify-content: center; }
}

@media (prefers-reduced-motion: reduce) {
  .svc-grid li,
  .related-grid li,
  .sector-grid li,
  .faq-item summary::after { transition: none; }
  .svc-grid li:hover,
  .related-grid li:hover,
  .sector-grid li:hover { transform: none; }
}

/* ============================================================
   PORTFOLIO PAGE
   The gallery is rendered server-side here (unlike the homepage,
   which injects it with JS) so crawlers and AI retrieval can read
   the work without executing anything.
   ============================================================ */

.pf-section { padding: 8px 0 34px; }

.case-list { display: grid; gap: 46px; margin-top: 26px; }

.case-entry header { margin-bottom: 10px; }
.case-entry .case-index {
  display: block;
  font-family: var(--font-body);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 6px;
}
.case-entry h3 {
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 2.6vw, 1.9rem);
  color: var(--ink);
  margin: 0 0 10px;
}
.case-entry > p {
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.7;
  color: var(--ink-muted);
  max-width: 66ch;
  margin: 0 0 20px;
}
.case-frame-static {
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 1px solid var(--hairline);
  background: var(--surface-sunken);
  line-height: 0;
}
.case-frame-static video { width: 100%; height: auto; display: block; }

.live-grid,
.work-grid {
  list-style: none;
  margin: 26px 0 0;
  padding: 0;
  display: grid;
  gap: 26px;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}

.live-entry h3,
.work-entry h3 {
  font-family: var(--font-display);
  font-size: 1.2rem;
  line-height: 1.3;
  color: var(--ink);
  margin: 12px 0 8px;
}
.live-entry h3 a,
.work-entry h3 a { color: inherit; text-decoration: none; }
.live-entry h3 a:hover,
.work-entry h3 a:hover { color: var(--yellow); }
.live-entry p,
.work-entry p {
  font-family: var(--font-body);
  font-size: 0.93rem;
  line-height: 1.68;
  color: var(--ink-muted);
  margin: 0;
}
.work-entry .eyebrow { display: block; margin-top: 14px; }
.live-entry .flp-num {
  font-family: var(--font-body);
  font-size: 0.82rem;
  color: var(--ink-faint);
  margin-right: 8px;
}

.live-thumb,
.work-thumb {
  display: block;
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 1px solid var(--hairline);
  background: var(--surface-sunken);
  line-height: 0;
  transition: border-color 0.25s var(--ease), transform 0.25s var(--ease);
}
.live-thumb:hover,
.work-thumb:hover {
  border-color: var(--hairline-strong);
  transform: translateY(-2px);
}
.live-thumb img,
.work-thumb img {
  width: 100%;
  height: auto;
  display: block;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}

.archive-list {
  list-style: none;
  margin: 22px 0 0;
  padding: 0;
  display: grid;
  gap: 10px;
  max-width: 70ch;
}
.archive-list a {
  font-family: var(--font-body);
  font-size: 0.98rem;
  color: var(--ink-muted);
  text-decoration: none;
  border-bottom: 1px solid var(--hairline);
  display: block;
  padding: 10px 0;
}
.archive-list a:hover { color: var(--yellow); }

/* ============================================================
   CONTACT PAGE
   Reuses the homepage's .contact-grid / .contact-form styling from
   sections.css; only the page-level padding needs setting here.
   ============================================================ */

.contact-page { padding: 10px 0 40px; }
.contact-page .contact-grid { align-items: start; }
.contact-page .section-heading { margin-top: 38px; }
.contact-page .contact-copy > .section-heading:first-child { margin-top: 0; }

/* ============================================================
   ABOUT PAGE
   ============================================================ */

.process-list {
  counter-reset: step;
  list-style: none;
  margin: 20px 0 0;
  padding: 0;
  display: grid;
  gap: 16px;
  max-width: 70ch;
}
.process-list li {
  counter-increment: step;
  position: relative;
  padding: 16px 20px 16px 58px;
  background: var(--surface-raised);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: 0.97rem;
  line-height: 1.7;
  color: var(--ink-muted);
}
.process-list li::before {
  content: counter(step, decimal-leading-zero);
  position: absolute;
  left: 20px;
  top: 16px;
  font-family: var(--font-body);
  font-size: 0.82rem;
  letter-spacing: 0.08em;
  color: var(--purple-soft);
}
.process-list strong { color: var(--ink); font-weight: 600; }

.cover-grid {
  list-style: none;
  margin: 18px 0;
  padding: 0;
  display: grid;
  gap: 10px;
  max-width: 70ch;
}
.cover-grid li {
  font-family: var(--font-body);
  font-size: 0.95rem;
  line-height: 1.8;
  color: var(--ink-faint);
}

.fact-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
  margin-top: 20px;
}
.fact-grid > div {
  background: var(--surface-raised);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  padding: 18px 20px;
}
.fact-label {
  display: block;
  font-family: var(--font-body);
  font-size: 0.74rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 7px;
}
.fact-value {
  display: block;
  font-family: var(--font-body);
  font-size: 0.98rem;
  line-height: 1.5;
  color: var(--ink);
}

@media (max-width: 720px) {
  .live-grid, .work-grid, .fact-grid { grid-template-columns: 1fr; }
  .contact-page .contact-grid { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  .live-thumb, .work-thumb { transition: none; }
  .live-thumb:hover, .work-thumb:hover { transform: none; }
}

/* ============================================================
   MOBILE SPACING
   The desktop rhythm leaves dead vertical space once the type
   scale shrinks. These only apply below the tablet breakpoint.
   ============================================================ */
@media (max-width: 860px) {
  .breadcrumb { padding-top: 84px; font-size: 0.78rem; }
  .page-head { padding: 22px 0 8px; }
  .page-title { margin: 8px 0 14px; }
  .page-lede { margin-bottom: 20px; }
  .cta-inline { margin-top: 0; }

  /* The largest offender: a full section-heading margin stacked on top of
     the page-head padding produced roughly 180px of nothing between the
     hero buttons and the first group. */
  .svc-groups { padding-top: 4px; }
  .svc-group { margin-bottom: 34px; }
  .svc-group:first-child > .section-heading { margin-top: 22px; }
  .svc-group .section-heading { margin-top: 0; margin-bottom: 8px; }
  .svc-group .section-lede { margin-bottom: 14px; }
  .svc-grid, .related-grid, .sector-grid, .includes-grid { margin-top: 16px; gap: 14px; }

  .pf-section { padding: 4px 0 26px; }
  .case-list { gap: 32px; margin-top: 18px; }
  .prose h2, .prose-block .section-heading, .post h2 { margin: 34px 0 12px; }
  .cta-band { margin-top: 34px; padding: 34px 0 38px; }
  .faq, .includes, .techlist, .related { padding: 10px 0; }
}

/* Comfortable hit areas for everything tappable, everywhere.
   44px is the Apple/Android minimum; several of these were 19px. */
@media (pointer: coarse) {
  .faq-item summary { padding-top: 18px; padding-bottom: 18px; }
  .breadcrumb a,
  .archive-list a,
  .tag-list a { min-height: 44px; display: inline-flex; align-items: center; }
  .related-grid a { padding: 18px 20px; }
}

/* .svc-group is a <section>, so it inherits the global section padding
   (60px top). On a phone that reads as a dead gap under the hero buttons
   — the group headings already carry their own rhythm. */
@media (max-width: 860px) {
  .svc-group { padding-top: 0; padding-bottom: 0; }
  .svc-group:first-child > .section-heading { margin-top: 6px; }
}

/* ===== footer-nav.css ===== */
/* ============================================================
   FOOTER NAVIGATION

   The site's internal-linking backbone: it puts all 28 service
   pages one hop from every other page.

   Two things drive the layout decisions here:

   1. Tap targets. The first version used 9px margins and a 19px
      line box, which is close to unhittable on a phone — links
      appeared not to work. Every link is now at least 44px tall
      on touch devices.

   2. The floating "Connect Us" button is position:fixed at
      z-index 9000, 16px off the bottom. It sat directly on top
      of the last footer links, so those were genuinely
      unclickable. The footer now reserves space for it.

   Reads entirely from variables.css tokens, so both themes work
   without a separate light-mode block.
   ============================================================ */

.footer-nav {
  border-top: 1px solid var(--hairline);
  padding: 64px 0 40px;
  background: var(--charcoal-deep);
}

.fnav-grid {
  display: grid;
  grid-template-columns: 1.6fr repeat(4, 1fr);
  gap: 40px 32px;
  align-items: start;
}

/* ---- Brand column ------------------------------------------------ */
.fnav-brand {
  grid-row: span 2;
  min-width: 240px;
}

.fnav-brand h2 {
  font-family: var(--font-display);
  font-size: 1.2rem;
  font-weight: 500;
  color: var(--ink);
  margin: 0 0 12px;
}

.fnav-brand p {
  font-family: var(--font-body);
  font-size: 0.9rem;
  line-height: 1.7;
  color: var(--ink-muted);
  margin: 0 0 18px;
  max-width: 34ch;
}

.fnav-brand address {
  font-style: normal;
  font-family: var(--font-body);
  font-size: 0.92rem;
  line-height: 2;
  color: var(--ink-muted);
  margin: 0 0 16px;
}

/* ---- Column headings --------------------------------------------- */
.fnav-col h2 {
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 4px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--hairline);
}

.fnav-col ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.fnav-col li { margin: 0; }

/* ---- Links -------------------------------------------------------
   Padding rather than margin, so the whole row is the hit area. */
.fnav-col a,
.fnav-brand address a {
  display: block;
  font-family: var(--font-body);
  font-size: 0.9rem;
  line-height: 1.45;
  color: var(--ink-muted);
  text-decoration: none;
  padding: 7px 0;
  transition: color 0.2s var(--ease), padding-left 0.2s var(--ease);
}

.fnav-brand address a { display: inline-block; padding: 2px 0; }

.fnav-col a:hover,
.fnav-col a:focus-visible {
  color: var(--yellow);
  padding-left: 5px;
}

/* ---- Social ------------------------------------------------------- */
.fnav-social {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 4px;
}

.fnav-social li { margin: 0; }

.fnav-social a {
  padding: 9px 15px;
  font-size: 0.84rem;
  letter-spacing: 0.02em;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  color: var(--ink-muted);
}

.fnav-social a:hover,
.fnav-social a:focus-visible {
  padding-left: 15px;
  border-color: var(--hairline-strong);
  color: var(--yellow);
}

/* ============================================================
   TABLET
   ============================================================ */
@media (max-width: 1080px) {
  .fnav-grid { grid-template-columns: 1.4fr repeat(3, 1fr); }
}

@media (max-width: 860px) {
  .fnav-grid { grid-template-columns: repeat(2, 1fr); gap: 30px 24px; }
  .fnav-brand { grid-column: 1 / -1; grid-row: auto; }
  .fnav-brand p { max-width: none; }
}

/* ============================================================
   PHONE

   Two columns, not eight stacked ones — the single-column version
   ran to ~1,500px of footer, which is why it felt like a wall.
   ============================================================ */
@media (max-width: 720px) {
  .footer-nav {
    padding: 40px 0 28px;
  }

  /* Collapsed groups are a single row each, so they stack in one
     column and read as an index rather than a wall. */
  .fnav-grid {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .fnav-studio { margin-bottom: 18px; }

  .fnav-col h2 {
    font-size: 0.68rem;
    letter-spacing: 0.12em;
    padding-bottom: 8px;
  }

  .fnav-col a {
    font-size: 0.88rem;
    /* 44px minimum touch target — the fix for links that "would not open". */
    min-height: 44px;
    display: flex;
    align-items: center;
    padding: 4px 0;
  }

  .fnav-col a:hover { padding-left: 0; }

  .fnav-brand address a { min-height: 40px; display: flex; align-items: center; }
  .fnav-social a { min-height: 40px; display: inline-flex; align-items: center; }
}

/* 375px is the most common phone width, so the single-column
   fallback must sit below it, not above. */
@media (max-width: 340px) {
  .fnav-grid { grid-template-columns: 1fr; }
}

/* ============================================================
   CLEARANCE FOR THE FLOATING BUTTON

   .wpb is position:fixed, z-index 9000, ~44px tall, 16px from the
   bottom. Without this reserve it covers the last footer links and
   the copyright line, and those taps hit the button instead.
   ============================================================ */
.site-footer {
  padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
}

@media (min-width: 861px) {
  /* On desktop the button sits in the right margin, so only a little
     breathing room is needed. */
  .site-footer { padding-bottom: 40px; }
}

@media (prefers-reduced-motion: reduce) {
  .fnav-col a { transition: none; }
  .fnav-col a:hover { padding-left: 0; }
}

/* ============================================================
   COLLAPSIBLE GROUPS

   Service groups ship as <details open>, so every link is in the
   DOM and crawlable with JavaScript disabled. main.js closes them
   below 720px, where 29 expanded links made the footer ~2,400px
   tall on a phone.
   ============================================================ */
.fnav-group { border: 0; }
.fnav-group > summary {
  list-style: none;
  cursor: default;
  display: block;
}
.fnav-group > summary::-webkit-details-marker { display: none; }
.fnav-group > summary::marker { content: ""; }

@media (max-width: 720px) {
  .fnav-group > summary {
    cursor: pointer;
    position: relative;
    min-height: 46px;
    display: flex;
    align-items: center;
  }
  .fnav-group > summary h2 {
    margin: 0;
    padding: 0;
    border: 0;
    flex: 1;
  }
  /* Chevron */
  .fnav-group > summary::after {
    content: "";
    width: 9px;
    height: 9px;
    margin-left: 12px;
    border-right: 1.5px solid var(--ink-faint);
    border-bottom: 1.5px solid var(--ink-faint);
    transform: rotate(45deg) translateY(-2px);
    transition: transform 0.25s var(--ease);
    flex: none;
  }
  .fnav-group[open] > summary::after {
    transform: rotate(-135deg) translateY(-2px);
  }
  .fnav-group { border-bottom: 1px solid var(--hairline); }
  .fnav-group > ul { padding-bottom: 8px; }
}
