/* ============================================================
   VEDA9 — Ahead By Design
   Brand system: Obsidian + Imperial Purple + Ivory, jade for proof.
   Type: Archivo (expanded) display / Inter body / JetBrains Mono utility.
   Fonts: self-hosted variable woff2 (OFL / Apache 2.0).
   Google Fonts CDN removed (see A7 in security audit 2026-06-21).
   ============================================================ */

/* ---- Self-hosted fonts ---------------------------------------- */

/* Archivo Variable (wdth + wght axes: 62-125% stretch, 100-900 weight) */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  font-stretch: 62% 125%;
  src: url('./assets/fonts/archivo-latin-ext-wdth-normal.woff2') format('woff2-variations');
  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;
}
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  font-stretch: 62% 125%;
  src: url('./assets/fonts/archivo-latin-wdth-normal.woff2') format('woff2-variations');
  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;
}

/* Inter Variable (wght axis: 100-900 weight) */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('./assets/fonts/inter-latin-ext-wght-normal.woff2') format('woff2-variations');
  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;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('./assets/fonts/inter-latin-wght-normal.woff2') format('woff2-variations');
  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;
}

/* JetBrains Mono Variable (wght axis: 100-800 weight) */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url('./assets/fonts/jetbrains-mono-latin-ext-wght-normal.woff2') format('woff2-variations');
  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;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url('./assets/fonts/jetbrains-mono-latin-wght-normal.woff2') format('woff2-variations');
  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;
}

/* --------------------------------------------------------------- */

:root {
  --obsidian: #0a0a0a;
  --ink: #121216;
  --ink-2: #17171d;
  /* PURPLE, and the three jobs it does. Ratios are against --obsidian, measured
     with the WCAG 2.1 relative-luminance formula. Formalised into the brand on
     2026-08-15; see Ventures/VEDA9/brand/assets/color/PALETTE.md, which is the
     source of truth for both this site and every document.

     The rule in one line: purple that carries WORDS is --purple-txt. Purple that
     draws a LINE or fills a SHAPE is --purple-lo or --purple. Never the reverse. */
  --purple: #6b3cc7;     /* 2.9:1 — FILL ONLY. Never text, never a hairline. */
  --purple-lo: #8b5cf6;  /* 4.7:1 — borders, icon strokes, glows, 32px+ display. */
  --purple-txt: #a78bfa; /* 7.3:1 AAA — anything that reads as language. */
  --purple-deep: #4b2a8a;/* gradient stop only, far below any text floor. */
  --jade: #79b8a6;       /* 8.7:1 AAA — proof and data. Reserved for results. */
  --ivory: #f5f3ec;      /* 17.8:1 AAA */
  --silver: #8a8a93;     /* 3.9:1 — below AA. Dividers and non-informational
                            marks only. Never text, at any size. */
  --slate: #18181e;
  --grad: linear-gradient(90deg, #4b2a8a 0%, #6b3cc7 45%, #8b5cf6 100%);

  --text: #d8d6cd;       /* body on dark */
  --text-dim: #9a988f;   /* muted body */
  --head: #f5f3ec;       /* headings */

  --hair: rgba(245, 243, 236, 0.12);
  --hair-strong: rgba(245, 243, 236, 0.22);

  --display: "Archivo", system-ui, sans-serif;
  --body: "Inter", system-ui, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, "SF Mono", monospace;

  --maxw: 1140px;
  --measure: 60ch;
  --pad: clamp(1.25rem, 5vw, 4rem);
  --section-y: clamp(5rem, 12vh, 9rem);

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

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  /* Tells the browser this is a dark document, which is what makes the native
     scrollbars, form controls and text-selection colours render dark to match.
     Without it they come through in the light default and look pasted on. */
  color-scheme: dark;
  /* Kills the ~300ms delay a mobile browser holds after every tap while it
     waits to see whether you meant to double-tap and zoom. Pinch zoom still
     works; only the double-tap gesture goes, and with it the lag on every
     button on the site. */
  touch-action: manipulation;
}

body {
  margin: 0;
  background: var(--obsidian);
  color: var(--text);
  font-family: var(--body);
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.12rem);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* ---- The second brain: layered background, fixed behind everything ---- */
.bg { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; background: var(--obsidian); }
.bg > * { position: absolute; inset: 0; width: 100%; height: 100%; }

/* Layer 1: the fal-generated clip (hidden until a source is loaded) */
.bg__video { object-fit: cover; opacity: 0; transition: opacity 1.2s var(--ease); }
.bg__video.is-live { opacity: 0.9; }

/* The descent film and the creature are alternatives, never both. main.js adds
   .has-descent to <body> only once the film reports it is actually playing, so
   a visitor whose film never starts keeps the creature rather than a blank
   background. The channel labels go with the creature: they are pinned to its
   tentacles and mean nothing floating over a different animal. */
body.has-descent .bg__creature,
body.has-descent .creature-labels { opacity: 0; visibility: hidden; transition: opacity .8s var(--ease); }

/* ---- Layer 1b: THE CREATURE -------------------------------------------
   Our own generated render, dropped straight onto the page. Screen blending
   is what makes this work without an alpha channel: screen never darkens, so
   every near-black pixel of the render contributes nothing and vanishes into
   the page, while the glowing animal comes through at full strength. A PNG
   cutout would cost four times the bytes and leave a matte line around every
   tentacle. 110 KB as WebP.

   Two nested elements on purpose. The outer one carries the scroll descent
   (--jf-y, written by main.js) and the inner one carries the swim, because a
   CSS animation and a scroll-driven transform cannot share one element
   without fighting over the same property. */
/* Origin sits on the bell, not on the element's centre. Everything the script
   below does then pivots where the animal's mass actually is: it grows toward
   you from the bell, it turns about the bell, and the skew shears hardest at
   the bottom of the frame — which is where the tentacles are. Pivoting about
   the middle of the viewport instead made the whole picture slide whenever it
   turned, which is the giveaway that it is a picture. */
.bg__creature {
  mix-blend-mode: screen;
  will-change: transform;
  transform-origin: 68% 12%;
}
/* THE CREATURE'S BOX, shared. Sized to the render's own 2:3 frame rather than
   stretched across the viewport, for two reasons. A mask applies to an
   element's box, so unless the box IS the picture, feathering the edges
   feathers the wrong rectangle (learned the hard way, 2026-08-05). And the
   channel labels need the same box to sit in, because a percentage inside it
   is then a fixed point ON THE ANIMAL, at any window size. Pushed right of
   centre so the headline lands on dark water rather than on the bell. */
.creature-box {
  position: absolute;
  top: 0;
  height: 112vh;
  width: calc(112vh * 2 / 3);
  left: 68%;
  margin-left: calc(-56vh * 2 / 3);
}

.bg__creature-inner {
  /* Desktop keeps the full 1568x2352 render. Phones get the 880px variant in
     the media query below: on a phone this still IS the picture (no clip
     loads there), so it is the Largest Contentful Paint element, and 241 KB
     was too heavy for the thing Google times. The small one is 68 KB.
     If either file is re-exported, re-export BOTH and keep the preload in
     index.html in step with these two. (2026-08-21) */
  background: url("./assets/bg/jellyfish.webp") no-repeat 50% 50%;
  background-size: 100% 100%;
  /* The bell sits at roughly a third of the way down the frame. Pulsing about
     that point pulls the arms up behind the stroke; pulsing about the middle of
     the picture pumps the whole thing like a bellows. Only used on devices that
     fall back to the still, where main.js drives the stroke from scroll. */
  transform-origin: 50% 30%;
  /* The page grades and vignettes everything behind the content, which pulls
     the render toward grey. A little saturation back, and a little lift. */
  filter: saturate(1.18) brightness(1.08);

  /* FEATHER THE FRAME. The render's water is not pure black — it carries god
     rays and drifting motes — so under screen blending its rectangle showed up
     as a visibly lighter window sitting on the page, with four hard edges.
     Crushing the image to true black would have killed the rays along with the
     seam, so instead the edges are dissolved: two gradients intersected, wide
     at the sides where the seam was worst and shallow at the top so the bell
     is never clipped. The bottom is left open because the tentacles are meant
     to run off the frame. */
  -webkit-mask-image:
    linear-gradient(to right, transparent 0, #000 15%, #000 85%, transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 9%, #000 100%);
  mask-image:
    linear-gradient(to right, transparent 0, #000 15%, #000 85%, transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 9%, #000 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;

  opacity: 0;
  transition: opacity 1.8s var(--ease);
  animation: jf-swim 9s ease-in-out infinite;
  will-change: transform;
}
.bg__creature.is-live .bg__creature-inner { opacity: 1; }

/* The clip sits exactly on the still it was generated from, so it needs no
   fitting of its own: the parent box already carries the render's 2:3 frame,
   and the clip's first frame IS that still. It cross-fades in over the top,
   which is why the swap is invisible even on a slow connection. */
.bg__creature-video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: fill;
  opacity: 0;
  transition: opacity 1.4s var(--ease);
}
.bg__creature-video.is-live { opacity: 1; }

/* NOTHING ON THIS LAYER RUNS OFF A CLOCK (Justin, 2026-08-05: move with the
   scrolling, not independently). The keyframe stroke below is kept only as
   dead code for the moment: it is switched off the instant the creature is
   live, on every path, clip or no clip. Without a clip the animal simply does
   not pulse; it still travels, turns and sways, all of it driven by scroll. */
.has-creature .bg__creature-inner { animation: none; }

/* The pulse. A jellyfish drives itself by squeezing its bell, so the whole
   animal rises slightly as it contracts and settles back as it relaxes. Long
   and shallow: at 13 seconds it reads as drifting, and anything faster starts
   to look like a loading spinner behind the headline. */
@keyframes jf-swim {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1, 1); }
  /* The contraction. A jellyfish squeezes its bell narrower and taller to
     push water out, so the squash is deliberately uneven: it pinches in
     horizontally while stretching vertically, and that asymmetry is what
     reads as a muscle rather than as a zoom. */
  42%      { transform: translate3d(0, -2.6%, 0) scale(0.968, 1.045); }
  /* The relax and glide. It keeps rising for a moment after the push, which
     is the beat that makes the stroke feel like propulsion. */
  72%      { transform: translate3d(0, -0.9%, 0) scale(1.022, 0.985); }
}
@media (prefers-reduced-motion: reduce) {
  .bg__creature-inner { animation: none; }
}

/* Layer 2: live canvas network. Dims to a subtle overlay once a video plays.
   The homepage dropped this layer on 2026-08-06 (see index.html); these rules
   stay because privacy.html and terms.html still use #brain as their
   background. */
.bg__canvas { display: block; transition: opacity 1s var(--ease); }
.bg.has-video .bg__canvas { opacity: 0.45; mix-blend-mode: screen; }

/* Layer 3: purple wash + scroll color shift (keeps it predominantly purple) */
.bg__tint {
  background: radial-gradient(90% 70% at 70% 10%, rgba(107, 60, 199, 0.18), transparent 60%),
              radial-gradient(80% 80% at 10% 90%, rgba(75, 42, 138, 0.16), transparent 60%);
  mix-blend-mode: screen;
  transition: filter 0.6s linear;
}

/* Layer 4: vignette for legibility */
.grain {
  background:
    radial-gradient(120% 80% at 50% 0%, transparent 40%, rgba(10, 10, 10, 0.35) 100%),
    radial-gradient(100% 60% at 50% 100%, rgba(10, 10, 10, 0.55), transparent 60%);
}

main, .footer { position: relative; z-index: 2; }

/* ---- Layout helpers ---- */
.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--pad); }
.measure { max-width: var(--measure); }

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

/* Content sections get a soft scrim so text stays readable over the graph.
   Showcase sections stay clearer so the graph reads vivid. */
.section::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(130% 100% at 50% 50%, rgba(10, 10, 12, 0.9) 35%, rgba(10, 10, 12, 0.55) 70%, transparent 100%);
}
.section--showcase::before {
  background: radial-gradient(130% 100% at 50% 50%, rgba(10, 10, 12, 0.6) 30%, rgba(10, 10, 12, 0.2) 65%, transparent 100%);
}

/* ---- Typography primitives ---- */
/* Label sizes raised across the board 2026-08-21 (Justin: "can all the purple
   text be larger, it's hard to read"). These are mono, uppercase and widely
   letter-spaced, all three of which cost legibility, so the sizes that looked
   fine in the design file read as small on a real screen. Every purple label
   class below moved up together so the family stays consistent; the large
   purple numerals (.statrow__n, .hero-stat__n) were already big and did not
   change. */
.eyebrow {
  font-family: var(--mono);
  font-size: 0.9rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--purple-txt);
  margin: 0 0 1.25rem;
}

.h2 {
  font-family: var(--display);
  font-weight: 700;
  font-stretch: 125%;
  font-size: clamp(1.75rem, 1.2rem + 2.6vw, 3rem);
  line-height: 1.05;
  letter-spacing: -0.01em;
  color: var(--head);
  margin: 0 0 1.5rem;
  text-wrap: balance;
}

/* Service-page h1. Bigger than .h2 and smaller than the homepage hero, which is
   a wordmark-scale statement rather than a heading. Added with the service
   pages 2026-08-21. */
.h1 {
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 125%;
  font-size: clamp(2.1rem, 1.4rem + 3.2vw, 3.8rem);
  line-height: 1.03;
  letter-spacing: -0.015em;
  color: var(--head);
  margin: 0 0 1.5rem;
  text-wrap: balance;
}

.lede {
  font-size: clamp(1.05rem, 1rem + 0.35vw, 1.28rem);
  line-height: 1.6;
  color: var(--text);
  margin: 0;
}

.footnote {
  font-family: var(--mono);
  font-size: 0.78rem;
  line-height: 1.7;
  letter-spacing: 0.01em;
  color: var(--text-dim);
  margin-top: 2.5rem;
}
/* Without this a footnote link falls back to the browser's default blue,
   which is unreadable on the dark ground. */
.footnote a {
  color: var(--purple-txt);
  text-decoration-color: rgba(167, 139, 250, 0.45);
  text-underline-offset: 3px;
  transition: color 0.25s var(--ease);
}
.footnote a:hover { color: var(--ivory); text-decoration-color: var(--ivory); }

/* ---- Wordmark ---- */
.wordmark {
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 125%;
  letter-spacing: 0.04em;
  /* The nav wordmark is a link home and its box was only 16px tall. Padding it
     out gives a thumb something to hit without changing how the mark looks.
     The footer wordmark is a <span>, not a link, so this costs it nothing. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--ivory);
  text-decoration: none;
  display: inline-flex;
  align-items: baseline;
  line-height: 1;
}
.wordmark__nine {
  color: var(--purple-txt);
  margin-left: 0.04em;
}

/* ---- Nav ---- */
.nav {
  position: fixed;
  /* MUST outrank main. This was z-index 2, tied with main, until 2026-08-21.
     Equal z-index means DOM order decides and main comes later, so main
     painted over the fixed nav. The hero box starts at y=0 (its 6rem
     padding-top insets only the CONTENT), so it covered the nav strip and ate
     every click on it: hero buttons worked, nav links silently did nothing.
     Caught in an external audit. Re-test every header link after touching
     any stacking rule. */
  z-index: 50;
  top: 0; left: 0; right: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.1rem var(--pad);
  font-size: 1.15rem;
  transition: background 0.4s var(--ease), border-color 0.4s var(--ease), backdrop-filter 0.4s;
  border-bottom: 1px solid transparent;
}
.nav.is-stuck {
  background: rgba(10, 10, 12, 0.72);
  backdrop-filter: blur(10px);
  border-bottom-color: var(--hair);
}
.nav__cta {
  /* The site's primary call to action, and it measured 37px tall on a phone.
     inline-flex so min-height applies at all (it does nothing on the default
     inline box this used to be, which is why an earlier pass on .btn missed
     this one: the nav CTA is its own component and never used .btn). */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ivory);
  text-decoration: none;
  padding: 0.6rem 1rem;
  /* Solid purple, matching .btn--primary, which says the same four words. An
     outlined nav CTA next to a filled hero CTA read as two different offers.
     Measured: ivory on --purple is 6.1:1, clears AA for normal text. */
  background: var(--purple);
  border: 1px solid var(--purple);
  border-radius: 2px;
  transition: background 0.25s, border-color 0.25s, box-shadow 0.25s;
}
.nav__cta:hover {
  /* NOT --purple-lo. That is a border and large-display colour: ivory on it is
     3.76:1, which fails AA, so a hover that lightened to it made the label
     harder to read at the exact moment of intent. #7a47e0 is 4.97:1. */
  background: #7a47e0;
  border-color: #7a47e0;
  box-shadow: 0 8px 24px -10px rgba(139, 92, 246, 0.55);
}
.nav__links {
  display: flex;
  align-items: center;
  gap: 1.75rem;
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
}
.nav__links a { color: var(--text-dim); text-decoration: none; transition: color 0.2s; }
.nav__links a:hover { color: var(--ivory); }

/* ---- Buttons ---- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 0.95rem 1.5rem;
  /* The nav's teardown button measured 37px tall on a phone. 44px is the
     minimum a thumb can hit reliably, and this is the site's primary call to
     action, so it is the last control that should be fiddly. */
  min-height: 44px;
  border-radius: 2px;
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform 0.2s var(--ease), background 0.25s, border-color 0.25s, box-shadow 0.25s;
}
.btn--primary {
  background: var(--purple);
  color: var(--ivory);
  box-shadow: 0 0 0 rgba(107, 60, 199, 0);
}
.btn--primary:hover {
  /* Was --purple-lo. Ivory on it measures 3.76:1, below the 4.5:1 floor, so the
     site's main CTA lost legibility on hover. #7a47e0 is 4.97:1 and reads as
     the same lift. (Measured 2026-08-21.) */
  background: #7a47e0;
  transform: translateY(-1px);
  box-shadow: 0 10px 30px -10px rgba(139, 92, 246, 0.6);
}
.btn--ghost {
  background: transparent;
  color: var(--ivory);
  border-color: var(--hair-strong);
}
.btn--ghost:hover { border-color: var(--ivory); transform: translateY(-1px); }
.btn--block { width: 100%; }

.cta-row { display: flex; flex-wrap: wrap; gap: 0.9rem; margin-top: 2.25rem; }

/* ---- Hero ---- */
.hero {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: 6rem;
}
.hero::before { background: radial-gradient(120% 90% at 28% 50%, rgba(10,10,12,0.82) 22%, rgba(10,10,12,0.4) 58%, transparent 100%); }
.hero__inner { max-width: 760px; }
.eyebrow { animation: rise 0.8s var(--ease) both; }
.hero__title {
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 125%;
  font-size: clamp(3rem, 2rem + 6vw, 7rem);
  line-height: 0.9;
  letter-spacing: -0.02em;
  color: var(--head);
  margin: 0;
  animation: rise 0.9s var(--ease) 0.05s both;
}
.hero__lede {
  max-width: 46ch;
  margin: 1.75rem 0 0;
  font-size: clamp(1.08rem, 1rem + 0.5vw, 1.35rem);
  color: var(--text);
  animation: rise 0.9s var(--ease) 0.15s both;
}
.cta-row { animation: rise 0.9s var(--ease) 0.25s both; }
.hero__caption {
  font-family: var(--mono);
  font-size: 0.76rem;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  margin-top: 2.5rem;
  max-width: 44ch;
  animation: rise 0.9s var(--ease) 0.35s both;
}

.scroll-cue {
  position: absolute;
  bottom: 2rem; left: 50%;
  transform: translateX(-50%);
  width: 1px; height: 48px;
  background: linear-gradient(var(--silver), transparent);
  overflow: hidden;
}
.scroll-cue span {
  position: absolute; top: 0; left: 0;
  width: 100%; height: 40%;
  background: var(--purple-lo);
  animation: cue 2.2s var(--ease) infinite;
}

/* ---- Chips (the function) ---- */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  list-style: none;
  margin: 2.5rem auto 0;
  padding-inline: var(--pad);
}
.chips li {
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: var(--text);
  padding: 0.55rem 0.95rem;
  border: 1px solid var(--hair);
  border-radius: 2px;
  background: rgba(23, 23, 29, 0.4);
  transition: border-color 0.25s, color 0.25s, background 0.25s;
}
.chips li:hover { border-color: var(--purple-lo); color: var(--ivory); background: rgba(107, 60, 199, 0.12); }

/* ---- Stats ---- */
.stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  margin-top: 3rem;
  background: var(--hair);
  border: 1px solid var(--hair);
}
.stat {
  background: rgba(13, 13, 17, 0.78);
  padding: 2rem 1.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.stat__num {
  font-family: var(--display);
  font-weight: 700;
  font-stretch: 125%;
  font-size: clamp(2.2rem, 1.5rem + 2.5vw, 3.4rem);
  line-height: 0.95;
  color: var(--jade);
  letter-spacing: -0.01em;
}
/* For a stat whose headline is words rather than a figure. "AI search" runs
   305px inside a 285px tile at the full size and wraps to two lines, which
   breaks the row rhythm every other tile keeps. A single step down fits it on
   one line and still reads as the same order of thing as the numbers beside it.
   Added 2026-08-06 with the AI-search tile. */
.stat__num--word {
  font-size: clamp(1.8rem, 1.2rem + 2.05vw, 2.8rem);
  white-space: nowrap;
}
.stat__label {
  font-size: 0.95rem;
  line-height: 1.5;
  color: var(--text-dim);
}

/* ---- Steps ---- */
.steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.25rem;
  margin-top: 2.5rem;
}
.step {
  border: 1px solid var(--hair);
  border-radius: 2px;
  padding: 2.25rem;
  background: rgba(13, 13, 17, 0.6);
  position: relative;
}
.step__num {
  font-family: var(--mono);
  font-size: 0.98rem;
  color: var(--purple-txt);
  letter-spacing: 0.1em;
}
.step__title {
  font-family: var(--display);
  font-weight: 700;
  font-stretch: 125%;
  font-size: clamp(1.3rem, 1.1rem + 1vw, 1.7rem);
  color: var(--head);
  margin: 1rem 0 0.85rem;
  letter-spacing: -0.01em;
}
.step__body { margin: 0; color: var(--text); }
.step__body + .step__body { margin-top: 0.85rem; }
/* The two "ways to work" qualifiers. These reused .svc-card__tags at first,
   which is 0.7rem mono meant for a short tag strip under a service-card title.
   As a two-line sentence doing real qualifying work it was too small to read
   (Justin, 2026-08-21). Given its own class so the eight service cards keep
   their small tags: the name reads as a label, the qualifier as body text. */
.step__role {
  margin: 0.9rem 0 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.step__role-name {
  font-family: var(--mono);
  font-size: 0.94rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--purple-txt);
}
.step__role-for {
  font-size: 1.02rem;
  line-height: 1.45;
  color: var(--text);
}
/* Same story as .step__role: this tag strip also borrowed .svc-card__tags,
   which is sized for a ~235px service card, and sat undersized inside a
   full-width step card (Justin, 2026-08-21). The eight service cards keep the
   smaller size, which is correct for their box. */
.step__tags {
  margin: 1.1rem 0 0;
  font-family: var(--mono);
  font-size: 0.96rem;
  letter-spacing: 0.04em;
  line-height: 1.6;
  color: var(--purple-txt);
}
.steps--two { grid-template-columns: repeat(2, 1fr); }
.step__list { margin: 0.85rem 0 0; padding: 0; list-style: none; color: var(--text); }
.step__list li { margin: 0.3rem 0; }
.step__close { margin: 1.1rem 0 0; font-weight: 700; color: var(--head); }

/* ---- CTA / form ---- */
.cta-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}
.cta-copy { max-width: 46ch; }
.cta-alt { font-size: 0.95rem; color: var(--text-dim); margin-top: 1.75rem; }
.cta-alt a { color: var(--ivory); text-underline-offset: 3px; }

.form {
  border: 1px solid var(--hair);
  border-radius: 3px;
  padding: clamp(1.5rem, 3vw, 2.5rem);
  background: rgba(13, 13, 17, 0.82);
  display: grid;
  gap: 1.1rem;
}
.field { display: grid; gap: 0.45rem; }
.field label {
  font-family: var(--mono);
  font-size: 0.74rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.field .opt { text-transform: none; letter-spacing: 0; opacity: 0.7; }
.field input,
.field textarea {
  font-family: var(--body);
  font-size: 1rem;
  color: var(--ivory);
  background: rgba(8, 8, 10, 0.7);
  border: 1px solid var(--hair-strong);
  border-radius: 2px;
  padding: 0.8rem 0.9rem;
  width: 100%;
  resize: vertical;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.field input:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--purple-lo);
  box-shadow: 0 0 0 3px rgba(107, 60, 199, 0.25);
}
.field input::placeholder { color: rgba(154, 152, 143, 0.5); }
.form__status { font-family: var(--mono); font-size: 0.8rem; margin: 0.25rem 0 0; min-height: 1.2em; color: var(--jade); }
.form__status.is-error { color: #e07a7a; }

/* ---- Contact block (mailto CTA, replaces the form) ---- */
.contact-block {
  border: 1px solid var(--hair);
  border-radius: 3px;
  padding: clamp(1.5rem, 3vw, 2.5rem);
  background: rgba(13, 13, 17, 0.82);
  display: grid;
  gap: 1.1rem;
}
.contact-label {
  font-family: var(--mono);
  font-size: 0.74rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0;
}
.contact-email {
  /* The only contact route on the entire site, and the tap target measured
     33px. It is the last link that should be hard to hit on a phone. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-family: var(--display);
  font-weight: 700;
  font-stretch: 125%;
  /* Must never wrap. It broke as "studio@vedanine.co / m" (2026-08-21), which
     reads as a typo on the one string the whole site is asking people to use.
     word-break: break-all was the cause; it permits a mid-word split.
     Measured with fontTools at wght 700 / wdth 125%: the string is 10.4em wide,
     so the 1.35rem cap needs 265px and clears a 320px screen with room. */
  font-size: clamp(0.95rem, 5vw, 1.35rem);
  color: var(--ivory);
  text-decoration: none;
  letter-spacing: -0.01em;
  white-space: nowrap;
  transition: color 0.2s;
}
.contact-email:hover { color: var(--purple-txt); }
.contact-note {
  font-size: 0.95rem;
  line-height: 1.6;
  color: var(--text-dim);
  margin: 0;
}
.contact-privacy {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--text-dim);
  margin: 0;
}
.contact-privacy a { color: var(--text-dim); text-underline-offset: 3px; }
.contact-privacy a:hover { color: var(--ivory); }

/* ---- Footer ---- */
.footer {
  border-top: 1px solid var(--hair);
  padding-block: 3rem;
  background: rgba(8, 8, 10, 0.9);
}
.footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem 3rem;
  align-items: center;
  justify-content: space-between;
}
.footer__brand { display: flex; align-items: baseline; gap: 1rem; }
.footer .wordmark { font-size: 1.4rem; }
.footer__tag { font-family: var(--mono); font-size: 0.78rem; letter-spacing: 0.08em; color: var(--text-dim); }
.footer__meta { display: flex; flex-wrap: wrap; gap: 0.4rem 1.5rem; font-family: var(--mono); font-size: 0.74rem; letter-spacing: 0.04em; color: var(--text-dim); }
/* Hit area, not text size. Privacy and Terms measured 20px tall in the mobile
   audit against a 44px minimum, so they grow a padded box around the same type
   rather than getting bigger. inline-flex + min-height means the tap target is
   generous while the words look identical. */
.footer__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: 2px;
  color: var(--text-dim);
  text-decoration: none;
  text-underline-offset: 3px;
  transition: color 0.2s;
}
.footer__link:hover { color: var(--ivory); text-decoration: underline; }
/* The row already sets its own gap; the padded links would double it. */
.footer__meta { row-gap: 0; }

/* ---- Pillars (how we think) ---- */
.pillars {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  margin-top: 3rem;
  background: var(--hair);
  border: 1px solid var(--hair);
}
.pillar {
  background: rgba(13, 13, 17, 0.72);
  padding: 2rem 1.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}
.pillar__icon { width: 30px; height: 30px; stroke: var(--purple-lo); fill: none; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
.pillar__name { font-family: var(--display); font-weight: 700; font-stretch: 125%; font-size: 1.15rem; letter-spacing: 0.01em; color: var(--head); margin: 0; }
.pillar__line { margin: 0; font-size: 0.95rem; line-height: 1.5; color: var(--text-dim); }

/* ---- Work / media tiles (placeholders + image/video) ---- */
.work { margin-top: 2.5rem; }
.work__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 1rem; }
.work__tile--wide { grid-column: span 2; }

.media { margin: 0; }
.media--feature { margin-bottom: 1rem; }
.media__frame {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--hair);
  border-radius: 3px;
  background: linear-gradient(135deg, var(--slate), #0c0c10);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.3s var(--ease);
}
.media__frame--16x9 { aspect-ratio: 16 / 9; }
.media__frame--9x16 { aspect-ratio: 9 / 16; }
.media__frame--1x1 { aspect-ratio: 1 / 1; }
.media__frame--4x5 { aspect-ratio: 4 / 5; }
.media__el { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.6s var(--ease); }
.media:hover .media__frame { border-color: var(--purple-lo); }
.media:hover .media__el { transform: scale(1.03); }

.media__ph {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(154, 152, 143, 0.55);
  border: 1px dashed var(--hair-strong);
  padding: 0.5rem 0.85rem;
  border-radius: 2px;
}
.media__badge {
  position: absolute;
  top: 0.6rem; left: 0.6rem;
  z-index: 1;
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ivory);
  background: rgba(107, 60, 199, 0.7);
  padding: 0.3rem 0.5rem;
  border-radius: 2px;
}
.media figcaption { display: flex; flex-direction: column; gap: 0.2rem; padding: 0.9rem 0.15rem 0; }
.work__tag { font-family: var(--mono); font-size: 0.92rem; letter-spacing: 0.09em; text-transform: uppercase; color: var(--purple-txt); }
.work__title { font-family: var(--display); font-weight: 600; font-stretch: 125%; font-size: 1.05rem; color: var(--head); }
.work__desc { font-size: 0.92rem; color: var(--text-dim); line-height: 1.5; }

/* ---- Scroll reveal ---- */
[data-reveal] { opacity: 0; transform: translateY(22px); transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); }
[data-reveal].is-in { opacity: 1; transform: none; }

/* ---- Keyframes ---- */
@keyframes rise { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }
@keyframes cue { 0% { transform: translateY(-100%); } 60%, 100% { transform: translateY(250%); } }

/* ---- Accessibility ---- */
:where(a, button, input, textarea):focus-visible {
  outline: 2px solid var(--purple-lo);
  outline-offset: 3px;
  border-radius: 2px;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
  [data-reveal] { opacity: 1; transform: none; }
  .scroll-cue { display: none; }
}

/* ---- Responsive ---- */
@media (max-width: 860px) {
  .stats { grid-template-columns: repeat(2, 1fr); }
  .steps { grid-template-columns: 1fr; }
  .steps--two { grid-template-columns: 1fr; }
  /* Added 2026-08-21 with the new nav. Three link labels plus the wordmark and
     the CTA do not fit one row on a phone; without this they crush the CTA and
     force horizontal overflow. The links all point at sections the visitor
     scrolls through anyway, so hiding them costs no navigation. */
  .nav__links { display: none; }
  .cta-grid { grid-template-columns: 1fr; }
  .pillars { grid-template-columns: repeat(2, 1fr); }
  .work__grid { grid-template-columns: repeat(2, 1fr); }
}
/* Hero title overflow on phones, fixed 2026-08-21. MEASURED, not guessed:
   "By Design." is 6.332em wide at wght 800 / wdth 125% including the -0.02em
   letter-spacing (measured off assets/fonts/archivo-latin-wdth-normal.woff2
   with fontTools). Against the old clamp(3rem, 2rem + 6vw, 7rem) that
   overflowed the content box by 44px at 320px, 19px at 360px and 1px at 390px,
   so it clipped on essentially every phone and pushed the page sideways.
   The nbsp in "By&nbsp;Design." is deliberate (the design is two lines, not
   three), so the fix is size, not wrapping. This rule is scoped to <=420px
   because the original clamp is safe above ~400px and still wanted there.
   Verified to leave 7px of headroom at 280px, rising to 37px at 420px.
   If the hero wording, weight or width axis ever changes, re-measure. */
@media (max-width: 420px) {
  .hero__title { font-size: clamp(2.3rem, 14vw - 5px, 3.6rem); }
}
/* Safeguard only, after the real width was fixed above: nothing should be
   able to push the page sideways again unnoticed. */
body { overflow-x: clip; }

@media (max-width: 540px) {
  .stats { grid-template-columns: 1fr; }
  .nav { font-size: 1rem; }
  .nav__cta { padding: 0.5rem 0.8rem; font-size: 0.72rem; }
  /* The CTA is three words and wrapped to four lines at 320px, which also
     pushed the page into horizontal overflow. Keep it on one line and let the
     nav shrink instead. */
  .nav__cta { white-space: nowrap; font-size: 0.66rem; letter-spacing: 0.03em; }
  .footer__inner { flex-direction: column; align-items: flex-start; }
  .pillars { grid-template-columns: 1fr; }
  .work__grid { grid-template-columns: 1fr; }
  .work__tile--wide { grid-column: span 1; }
}

/* ============================================================
   Cinematic layer: 3D scroll core, staggered reveals
   ============================================================ */

/* (The plasma rules lived here until 2026-08-06. The WebGL water layer they
   faded in at 14% opacity was removed: it held the whole page below 60fps
   even when idle, and at that opacity, behind the creature render, taking it
   out is not visible in a side-by-side screenshot. See index.html.) */

/* ---- Channel labels riding on the creature's own tentacles ----------------
   Justin, 2026-08-05: "the category words seem to be floating randomly and not
   on the tentacles. The tentacles represent the pillars of our marketing
   agency. The head of the jellyfish is the brains, VEDA9."

   They were floating because they were still pinned to the OLD WebGL creature's
   hubs, and that creature is now a barely-visible water layer sitting at 14%
   behind a completely different animal. The words were correctly placed on
   something nobody can see.

   Fixed by anchoring them to the render instead. This layer is an exact
   geometric twin of the creature: same box, same transform origin, and the
   script writes the same transform string to both every frame. So a label at
   79%/49% is on a specific tentacle and stays on it through the travel, the
   zoom, the tilt, the sway and the cursor lean.

   It is a sibling rather than a child on purpose. The creature is screen-blended
   and edge-masked; text inheriting either would wash out at the frame edges and
   dissolve into the water. */
/* Three levels, and the split matters. The OUTER one never moves, so the fade
   at the screen edges stays pinned to the screen; put that fade on the moving
   layer and it travels with the labels, which fades them at the ends of the
   creature instead of at the ends of the window. The MIDDLE one carries the
   transform and the creature's origin. The INNER one is the creature's box,
   which is what makes a percentage mean a point on the animal. */
.creature-labels {
  position: fixed; inset: 0;
  z-index: 1;
  pointer-events: none;
  overflow: hidden;
  opacity: 0;
  transition: opacity 1.2s var(--ease);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 7vh, #000 calc(100% - 7vh), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 7vh, #000 calc(100% - 7vh), transparent 100%);
}
.creature-labels__tf {
  position: absolute; inset: 0;
  transform-origin: 68% 12%;
  will-change: transform;
}
.has-creature .creature-labels { opacity: 1; }

/* OFF ON PHONES. The names only mean anything when they sit on a tentacle, and
   on a narrow screen the creature is 630px wide inside a 375px viewport, so the
   left-hand column lands squarely in the reading column. Measured in the mobile
   audit: labels printed over content at six of eight scroll positions, with
   "Search" sitting on top of the primary call to action.
   The old hub-driven system had exactly this rule and the replacement lost it,
   so this is a regression being restored, not a new idea. Nothing is lost by
   hiding them: the hero already lists all eight channels as plain text.
   900px matches the width at which the clip loads, so the whole enhanced
   background arrives as one thing rather than in pieces. */
@media (max-width: 900px) {
  .creature-labels { display: none; }
}

/* .bg-label ships at opacity 0 by design: the old hub system set an inline
   opacity on every label every frame, so the stylesheet had to start them
   hidden or they would flash in the wrong place on first paint. These labels
   are placed once and never moved individually, so they need turning back on. */
.creature-labels .bg-label { opacity: 1; }


/* The old hub-driven labels are switched off the moment the render takes over.
   Two sets of eight channel names on screen at once is worse than either. */
.has-creature .bg-labels { display: none; }

/* ---- Channel labels riding on the network hubs ---- */
.bg-labels { position: fixed; inset: 0; z-index: 1; pointer-events: none; overflow: hidden; }
.bg-label {
  position: absolute;
  top: 0; left: 0;
  margin-top: -0.5em;
  padding-left: 11px;
  font-family: var(--mono);
  font-weight: 600;
  font-size: 0.92rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(245, 243, 236, 0.95);
  text-shadow:
    0 0 8px rgba(6, 6, 9, 0.95),
    0 0 4px rgba(6, 6, 9, 0.95),
    1px 1px 2px rgba(6, 6, 9, 1),
    -1px 1px 2px rgba(6, 6, 9, 1),
    1px -1px 2px rgba(6, 6, 9, 1),
    -1px -1px 2px rgba(6, 6, 9, 1);
  white-space: nowrap;
  opacity: 0;
  will-change: transform, opacity;
}
.bg-label::before {
  content: "";
  position: absolute;
  left: 0; top: 50%;
  width: 4px; height: 4px;
  margin-top: -2px;
  border-radius: 50%;
  background: var(--purple-lo);
  box-shadow: 0 0 0 2px rgba(6, 6, 9, 0.9), 0 0 7px var(--purple-lo);
}
/* Pillar hubs (Foresight, Precision, Craft, Endurance): accent-tinted, a touch
   smaller than the channel labels so the hierarchy reads VEDA9 > channels > pillars. */
.bg-label--pillar {
  font-size: 0.82rem;
  color: rgba(214, 200, 255, 0.95);
  letter-spacing: 0.22em;
}
.bg-label--pillar::before {
  background: #c4a8ff;
  box-shadow: 0 0 0 2px rgba(6, 6, 9, 0.9), 0 0 8px #c4a8ff;
}
/* The featured hub during the services tour: larger, brighter, glowing */
.bg-label--feature {
  font-size: 1.08rem;
  letter-spacing: 0.2em;
  color: rgba(245, 243, 236, 0.95);
  padding-left: 15px;
  text-shadow: 0 0 14px rgba(139, 92, 246, 0.65);
}
.bg-label--feature::before {
  width: 6px; height: 6px; margin-top: -3px;
  box-shadow: 0 0 14px var(--purple-lo), 0 0 4px #fff;
}
@media (max-width: 760px) { .bg-labels { display: none; } }
/* Mobile LCP: swap the 241 KB desktop render for the 68 KB variant. Paired
   with the media-scoped preload in index.html so the browser fetches exactly
   one of the two, never both. */
@media (max-width: 760px) {
  /* Updated 2026-09-01: was jellyfish-mobile.webp, a frame of the OLD creature
     render. The phone now gets a vertical still pulled from the new descent film,
     so the picture a phone sees is the same world the film shows. 34 KB. */
  .bg__creature-inner { background-image: url("./assets/bg/jellyfish-descent-mobile.webp"); }
}

/* ---- What we run: eight service cards ---- */
/* Replaced the eight-panel pinned scroll tour (720vh) on 2026-08-01. */
.svc-grid {
  /* THREE columns since the list went to nine on 2026-08-24. At four it ran
     4/4/1 and left BRAND alone beside three empty cells, which reads as a grid
     that was planned for a different number. Nine squares off at 3x3, and the
     wider cards let each one carry its stack list without wrapping to five
     lines. */
  margin-top: clamp(2.5rem, 5vw, 3.75rem);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1rem, 1.6vw, 1.5rem);
}
.svc-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  padding: clamp(1.35rem, 2vw, 1.85rem);
  border: 1px solid var(--hair);
  border-radius: 14px;
  background: rgba(16, 14, 22, 0.5);
  backdrop-filter: blur(6px);
  transition: border-color 0.35s var(--ease), background 0.35s var(--ease), transform 0.35s var(--ease);
}
.svc-card:hover {
  border-color: var(--purple-lo);
  background: rgba(107, 60, 199, 0.1);
  transform: translateY(-3px);
}
.svc-card__eyebrow {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  font-family: var(--mono);
  font-size: 0.85rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.svc-card__num { color: var(--purple-txt); }
.svc-card__title {
  margin: 0;
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 100%;           /* the 125% used elsewhere is too wide for a
                                   ~235px card: it broke titles one word per line */
  font-size: clamp(1.1rem, 0.98rem + 0.4vw, 1.32rem);
  line-height: 1.12;
  letter-spacing: -0.01em;
  color: var(--head);
  text-wrap: balance;           /* cards are ~235px at four columns; keeps the
                                   short titles from breaking one word per line */
}
.svc-card__tags {
  /* The separators and multi-word tags are glued with &nbsp; in index.html, so
     a line never opens with a stray "&middot;" and "Community Management" never
     splits across two lines (both happened, 2026-08-21). Set there, not here,
     because CSS has no way to say "break only at the dots".
     text-wrap: balance evens the remaining lines so the last one is not a
     single orphan word. */
  margin: 0;
  font-family: var(--mono);
  font-size: 0.82rem;
  letter-spacing: 0.02em;
  line-height: 1.6;
  text-wrap: balance;
  color: var(--purple-txt);
}
.svc-card__line {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--text-dim);
}

@media (max-width: 1080px) {
  .svc-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .svc-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ---- Per-project pinned horizontal showcase ---- */
/* Tall section; the sticky stage pins while --p (0..1, written by the shared
   [data-scroll-3d] driver in main.js) slides the filmstrip left. */
.hscroll { --p: 0; position: relative; height: 340vh; }
.hscroll-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
}
/* Darken the network behind the header + captions so text stays legible and the
   filmstrip is the focus; tiles are opaque so they still pop. */
.hscroll-stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(180deg,
    rgba(10, 10, 12, 0.84) 0%,
    rgba(10, 10, 12, 0.55) 30%,
    rgba(10, 10, 12, 0.32) 46%,
    rgba(10, 10, 12, 0.32) 68%,
    rgba(10, 10, 12, 0.82) 100%);
}
.hscroll-head {
  display: flex;
  align-items: flex-end;
  gap: 1rem;
  padding: 0 var(--pad);
  margin-bottom: clamp(1.5rem, 4vh, 3rem);
}
.hscroll-index {
  font-family: var(--mono);
  font-size: 0.92rem;
  letter-spacing: 0.1em;
  color: var(--purple-txt);
  line-height: 1;
  padding-bottom: 0.4rem;
}
.hscroll-kicker {
  margin: 0 0 0.35rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.hscroll-title {
  margin: 0;
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 125%;
  font-size: clamp(1.9rem, 1rem + 3vw, 3.4rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--head);
}
.hscroll-viewport { width: 100%; overflow: hidden; }
.hscroll-track {
  --tile-h: clamp(300px, 54vh, 560px);
  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 2.5rem);
  width: max-content;
  padding: 0 var(--pad);
  will-change: transform;
  /* p=0 shows the start; p=1 pulls the far end to the right edge */
  transform: translateX(calc(var(--p) * (100vw - 100%)));
}
.hshot { flex: 0 0 auto; margin: 0; }
/* Explicit widths from the fixed tile height (aspect-ratio alone doesn't feed the
   track's shrink-to-fit width reliably, so the strip stayed too narrow to scroll). */
.hshot .media__frame { height: var(--tile-h); }
.hshot .media__frame--16x9 { width: calc(var(--tile-h) * 16 / 9); }
.hshot .media__frame--9x16 { width: calc(var(--tile-h) * 9 / 16); }
.hshot .media__frame--1x1  { width: var(--tile-h); }
.hshot .media__frame--4x5  { width: calc(var(--tile-h) * 4 / 5); }
.hscroll-rail {
  height: 2px;
  margin: clamp(1.5rem, 4vh, 3rem) var(--pad) 0;
  background: rgba(255, 255, 255, 0.08);
  border-radius: 2px;
  overflow: hidden;
}
.hscroll-rail__fill {
  display: block;
  height: 100%;
  width: 100%;
  background: var(--purple-lo);
  transform-origin: left;
  transform: scaleX(var(--p));
}

/* Pinned scroll-hijack is poor on touch / for reduced motion — fall back to a
   native swipe strip with snap. */
@media (max-width: 760px), (prefers-reduced-motion: reduce) {
  .hscroll { height: auto; }
  .hscroll-stage { position: static; height: auto; overflow: visible; padding-block: var(--section-y); }
  .hscroll-head { margin-bottom: 1.5rem; }
  .hscroll-viewport { overflow-x: auto; -webkit-overflow-scrolling: touch; scroll-snap-type: x proximity; }
  .hscroll-track { transform: none; width: max-content; --tile-h: clamp(240px, 62vw, 420px); }
  .hshot { scroll-snap-align: start; }
  .hscroll-rail { display: none; }
}

/* ---- 3D scroll showcase: the second brain core ---- */
.section--core {
  --p: 0;
  min-height: 200vh;
  padding-block: 0;
}
.section--core::before {
  background: radial-gradient(120% 90% at 50% 50%, rgba(10, 10, 12, 0.55) 28%, rgba(10, 10, 12, 0.15) 68%, transparent 100%);
}
.core-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
  perspective: 1400px;
}

/* The marquee band gliding behind the card */
.core-marquee {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(0.5rem, 5vh, 3rem);
  z-index: -1;
  pointer-events: none;
  transform: translateX(calc(var(--p) * -6%));
  /* CLIP, not just mask. The rows below are `width: max-content` and run to
     roughly 6,800px, and the mask underneath only stops you SEEING them past
     the edges: masking is a paint operation and has no effect on layout or on
     scroll extent. Without this the document was 6,593px wide inside a 375px
     phone, so the whole page scrolled sideways and every section could be
     dragged off screen. `clip` rather than `hidden` because `hidden` would
     make this a scroll container. (Found in the mobile audit, 2026-08-06.) */
  overflow: clip;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.core-marquee__row {
  display: flex;
  width: max-content;
  white-space: nowrap;
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 125%;
  line-height: 1;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: transparent;
  animation: coremarq 40s linear infinite;
}
/* Stroke opacity raised 2026-08-06 (0.11 -> 0.20 and 0.16 -> 0.28, Justin: "I
   can't really see this hidden text"). These words are the texture behind the
   deck and were reading as almost nothing on a bright screen. Still well under
   the copy in front of them: the cards sit on a near-opaque purple fill, so
   raising this cannot touch the legibility of anything a visitor reads. */
.core-marquee__row--a {
  font-size: clamp(2.8rem, 11vw, 9rem);
  -webkit-text-stroke: 1px rgba(245, 243, 236, 0.20);
}
.core-marquee__row--b {
  font-size: clamp(1.8rem, 7vw, 5.5rem);
  -webkit-text-stroke: 1px rgba(139, 92, 246, 0.28);
  animation-duration: 52s;
  animation-direction: reverse;
}
.core-marquee__row span { padding-inline: 0.28em; }
.core-marquee__row span::after { content: "\00B7"; margin-left: 0.45em; opacity: 0.45; }
@keyframes coremarq { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* The rotating deck */
.core-deck {
  position: relative;
  width: min(440px, 80vw);
  /* Was 5/6. The thesis card carries the most copy of the three and its last
     paragraph ran right onto the bottom border, which reads as text that got
     cut off rather than text that ended. Taller, so the copy finishes inside
     the card with air under it. The outer two gain the same height and simply
     push .ncard__feet further down, which is where it was already going. */
  aspect-ratio: 5 / 6.9;
  transform-style: preserve-3d;
  /* Swing was 22/30deg and scale ended at 1.02 while the deck was one card and
     two props. Now that all three carry copy the deck has to finish flatter and
     slightly smaller, or the outer two turn away from the reader and run off
     the stage just as they become readable.

     --q is --p finished early and held. The raw scroll progress only reaches 1
     at the instant the sticky stage lets go, so a fan driven straight off --p
     would hit its open position and immediately scroll away. --q completes at
     60% of the section and sits there, which gives the three cards a beat to be
     read before the page moves on. */
  --q: min(1, calc(var(--p) * 1.67));
  --fan: 104%;
  transform:
    rotateX(calc((0.5 - var(--p)) * 16deg))
    rotateY(calc((var(--p) - 0.5) * 14deg))
    scale(calc(0.82 + var(--q) * 0.1));
  will-change: transform;
}
.ncard {
  position: absolute;
  inset: 0;
  border-radius: 20px;
  border: 1px solid var(--hair-strong);
  backface-visibility: hidden;
}
/* The outer two. They start stacked behind the thesis card and unreadable, then
   come forward, straighten and light up as the section scrolls, so the copy
   arrives only once it can actually be read. */
.ncard--side {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: 0.8rem;
  padding: clamp(1.2rem, 2.4vw, 1.9rem);
  overflow: hidden;
  /* Inset top and bottom rather than filling the deck box. The two evidence
     cards carry three or four lines each; the thesis card carries nine chips, a
     headline and a paragraph. Holding all three to the thesis card's height
     opened a dead band in the middle of the outer two, and it got worse when
     the deck grew taller. A shorter evidence card either side of a taller
     thesis card is also the right read: the deck has a hero, not three peers of
     equal weight. */
  inset: 7% 0;
  /* Lit enough to read as a card in its own right. At the near-black it started
     on, the lower half of each one dissolved into the page and the card looked
     like it stopped where its text did. */
  background:
    radial-gradient(120% 80% at 30% 0%, rgba(120, 88, 214, 0.34), transparent 62%),
    linear-gradient(158deg, rgba(56, 39, 102, 0.97), rgba(21, 17, 38, 0.98));
  border-color: rgba(139, 92, 246, 0.28);
  box-shadow:
    0 40px 100px -50px rgba(107, 60, 199, 0.6),
    inset 0 1px 0 rgba(255, 255, 255, 0.1);
  opacity: calc(0.3 + var(--q) * 0.7);
}
/* The thesis card overlaps the inner edge of each outer card at anything under
   about 1400px. These keep the copy out of that band rather than relying on the
   lines happening to be short enough. */
.ncard--l { padding-right: 3.8rem; }
.ncard--r { padding-left: 3.8rem; }
.ncard--l {
  transform:
    translateZ(calc(-170px + var(--q) * 140px))
    translateX(calc(-22% - var(--q) * (var(--fan) - 22%)))
    rotateY(calc(22deg - var(--q) * 15deg));
}
.ncard--r {
  /* Mirrored, and not only for symmetry: the thesis card overlaps this one's
     left edge at narrower widths, which is exactly where left-aligned copy
     starts. Pushing the text to the outer edge keeps it clear of the overlap at
     every width the fan runs at. */
  text-align: right;
  transform:
    translateZ(calc(-170px + var(--q) * 140px))
    translateX(calc(22% + var(--q) * (var(--fan) - 22%)))
    rotateY(calc(-22deg + var(--q) * 15deg));
}
.ncard--main {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 1rem;
  padding: clamp(1.4rem, 3vw, 2.3rem);
  overflow: hidden;
  background:
    radial-gradient(125% 95% at 28% 8%, rgba(150, 108, 255, 0.6), transparent 58%),
    linear-gradient(158deg, #5a35b0 0%, #3a2272 52%, #19132e 100%);
  box-shadow:
    0 50px 130px -45px rgba(107, 60, 199, 0.75),
    inset 0 1px 0 rgba(255, 255, 255, 0.16);
}
.ncard__tag {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(245, 243, 236, 0.82);
}
.ncard__spark {
  width: clamp(58px, 13vw, 104px);
  height: clamp(58px, 13vw, 104px);
  align-self: center;
  fill: #0a0a0a;
  filter: drop-shadow(0 0 26px rgba(255, 255, 255, 0.55));
}
.ncard__title {
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 125%;
  font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2.1rem);
  line-height: 1.02;
  letter-spacing: -0.015em;
  color: var(--ivory);
  margin: 0;
}
.ncard__title--sm { font-size: clamp(1.15rem, 0.9rem + 1vw, 1.55rem); }
.ncard__line {
  margin: 0;
  font-size: 0.92rem;
  line-height: 1.5;
  color: rgba(245, 243, 236, 0.74);
}
.ncard--side .ncard__line { font-size: 0.84rem; line-height: 1.45; }
/* The three things a report actually answers. Set as a ruled list rather than
   another paragraph: the card already has one, and a second block of prose
   would read as more of the same instead of as the contents page it is. The
   rule sits on the leading edge, which flips with the card's text alignment. */
.ncard__list {
  margin: 0.2rem 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.42rem;
}
.ncard__list li {
  font-size: 0.8rem;
  line-height: 1.4;
  color: rgba(245, 243, 236, 0.68);
  padding-inline-end: 0.7rem;
  border-inline-end: 2px solid rgba(196, 174, 255, 0.42);
}
/* Removed 2026-08-25: this mirrored the list to the right edge back when only
   the measurement card had one. Now both outer cards carry a list and they have
   to read as the same component, so the left card uses the shared dash bullet. */
/* Sits on the bottom edge so the outer cards are not two thirds empty space
   below their copy. Names the actual craft and the actual tools, which is more
   use to a reader than the padding it replaced. */
.ncard__feet {
  margin: auto 0 0;
  font-family: var(--mono);
  /* Was 0.66rem. Justin, 2026-08-25: too small to read on the fanned cards. */
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.7;
  color: rgba(196, 174, 255, 0.72);
}

/* The eight channels on the thesis card. Added 2026-08-06 to fill the dead band
   between the mark and the headline. Set as chips rather than a run of text with
   separators: at this size a mono list reads as a caption, and this has to read
   as the substance of the card, not a footnote to it. */
.ncard__channels {
  /* THREE columns since the list went to nine on 2026-08-24. Four columns was
     correct for eight (two even rows) and orphans BRAND on a line of its own at
     nine, which is the exact mistake the four-column rule existed to prevent.
     Nine wants 3x3, and a nine that squares off is worth having. */
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.4rem 0.4rem;
  margin: 0;
  padding-inline: clamp(0.25rem, 1.5vw, 1rem);
}
.ncard__channels span {
  font-family: var(--mono);
  /* Was clamp(0.5rem, ..., 0.62rem), which set these at 8-10px. They name the
     nine pillars the whole card is about, so they were the smallest type on the
     page carrying the largest claim on it. */
  font-size: clamp(0.64rem, 0.5vw + 0.46rem, 0.8rem);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-align: center;
  color: rgba(232, 222, 255, 0.9);
  border: 1px solid rgba(232, 222, 255, 0.3);
  border-radius: 999px;
  padding: 0.46rem 0.25rem;
  white-space: nowrap;
}
/* Two columns on a phone: four across would set the type too small to read. */
@media (max-width: 620px) {
  .ncard__channels { grid-template-columns: repeat(2, 1fr); }
}

/* ---- Staggered, blur-in reveals (group children) ---- */
[data-reveal-stagger] > * {
  opacity: 0;
  transform: translateY(26px);
  filter: blur(6px);
  transition:
    opacity 0.7s var(--ease),
    transform 0.7s var(--ease),
    filter 0.7s var(--ease);
  transition-delay: calc(var(--i, 0) * 80ms);
}
[data-reveal-stagger].is-in > * {
  opacity: 1;
  transform: none;
  filter: none;
}

/* Between 900px and 1300px there is not room for a full-width card either side,
   so the fan closes up and the outer two overlap the thesis at the edges. Still
   readable, because the copy sits in the top third of each. */
@media (max-width: 1300px) {
  .core-deck { --fan: 88%; }
}

/* ---- Reduced motion: collapse the cinematic layer gracefully ---- */
@media (prefers-reduced-motion: reduce) {
  .core-marquee__row { animation: none; }
  .core-marquee { transform: none; }
  [data-reveal-stagger] > * { opacity: 1; transform: none; filter: none; }
}

/* The deck flattened, in the two cases where the 3D fan cannot work: a narrow
   window has nowhere to put a card either side of the thesis, and reduced-motion
   should not be paying for a scroll effect at all. Before 2026-08-04 the outer
   two were empty props and collapsing the stack simply hid them. They carry copy
   now, so the fallback has to keep all three readable: a plain vertical run,
   thesis first. */
@media (prefers-reduced-motion: reduce), (max-width: 900px) {
  .section--core { min-height: 0; }
  .core-stage {
    position: static;
    height: auto;
    padding-block: var(--section-y);
    perspective: none;
  }
  .core-deck, .core-deck .ncard { position: static; transform: none; opacity: 1; }
  .core-deck {
    display: flex;
    flex-direction: column;
    gap: 1.1rem;
    width: min(440px, 88vw);
    aspect-ratio: auto;
  }
  .core-deck .ncard--main { order: -1; }
  .core-deck .ncard--r { text-align: left; }
  .core-deck .ncard--l, .core-deck .ncard--r { padding-inline: clamp(1.2rem, 2.4vw, 1.9rem); }
}

/* ============================================================
   Archive page — the full body of work for one client.
   Multi-column so every piece keeps its own aspect ratio and
   nothing gets cropped to fit a uniform grid.
   ============================================================ */
.arch-head { padding: 8rem 0 2.5rem; }
.arch-head .h1 { margin-bottom: 0.6rem; }

/* Visually hidden: present for screen readers and the document outline, invisible
   on screen. Used by the case-page h1 when a client's logo replaces their name, so
   the page still has a real text heading. Not display:none, which would hide it
   from assistive tech too. */
.u-vh {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Client wordmark standing in for the h1 text. Capped by height so logos of
   different aspect ratios sit at a consistent optical size, and by width so a
   very wide wordmark cannot overflow on a phone. Height and opacity match
   .client__mark img on the homepage cards so the same logo reads the same
   weight in both places. */
.arch-head__logo {
  display: block;
  width: auto;
  height: auto;
  max-height: 4.4rem;
  max-width: min(100%, 26rem);
  margin-bottom: 0.2rem;
  opacity: 0.92;
}
@media (max-width: 600px) {
  .arch-head__logo { max-height: 3.2rem; }
}
.arch-back {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.88rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--purple-txt);
  text-decoration: none;
  margin-bottom: 1.6rem;
}
.arch-back:hover { color: var(--ivory); }

/* Two labelled rows: proof above, work below. Each row wraps independently, so
   a long proof run no longer pushes the discipline links into a third ragged
   line. The label is the same mono micro-type as the pills, dimmed, so it reads
   as a heading without taking a pill's visual weight. */
.arch-nav { margin-top: 2rem; display: flex; flex-direction: column; gap: 1.1rem; }
.arch-nav__row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
/* Full-width so the label sits ON ITS OWN LINE above the pills. Inline, it only
   indents the first line, and every wrapped line below runs flush left, which
   reads as a broken grid rather than a group. */
.arch-nav__lab {
  flex: 0 0 100%;
  font-family: var(--mono);
  font-size: 0.86rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--purple-txt);
  margin-bottom: -0.1rem;
}
.arch-nav a {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
  text-decoration: none;
  border: 1px solid var(--hair);
  border-radius: 2px;
  padding: 0.4rem 0.7rem;
  transition: border-color 0.3s var(--ease), color 0.3s var(--ease);
}
.arch-nav a:hover { border-color: var(--purple-lo); color: var(--ivory); }

.arch-cat { padding: 3.5rem 0 0; scroll-margin-top: 5rem; }
.arch-cat__head { display: flex; align-items: baseline; gap: 0.9rem; margin-bottom: 1.4rem; }
.arch-cat__n { font-family: var(--mono); font-size: 0.92rem; color: var(--purple-txt); }
.arch-cat__t { font-family: var(--display); font-weight: 600; font-stretch: 125%; font-size: 1.5rem; color: var(--head); }
.arch-cat__line { flex: 1; height: 1px; background: var(--hair); }

.arch-grid { columns: 3; column-gap: 1.1rem; }
.arch-grid .media { break-inside: avoid; margin-bottom: 1.6rem; display: block; }
.arch-grid .media__frame { aspect-ratio: auto; display: block; }
.arch-grid .media__el { position: static; inset: auto; width: 100%; height: auto; object-fit: contain; }

@media (max-width: 1000px) { .arch-grid { columns: 2; } }
@media (max-width: 640px) {
  .arch-grid { columns: 1; }
  .arch-head { padding-top: 6rem; }
}

.hscroll-more {
  display: inline-block;
  margin-top: 0.55rem;
  font-family: var(--mono);
  font-size: 0.82rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--purple-txt);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease);
}
.hscroll-more:hover { color: var(--ivory); border-bottom-color: var(--ivory); }

/* ---- Archive results: the numbers block at the foot of a case page ---- */
.arch-results { padding-top: 5rem; scroll-margin-top: 5rem; }
.arch-nav__hi { border-color: var(--purple-lo) !important; color: var(--ivory) !important; }
.arch-results__lede {
  max-width: 46rem;
  color: var(--text-dim);
  line-height: 1.65;
  margin-bottom: 2.6rem;
}
.arch-results__sub {
  font-family: var(--mono);
  font-size: 0.92rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--purple-txt);
  margin: 3rem 0 1.2rem;
}

/* Scoped to the case page. These were bare .stats/.stat until 2026-08-01, which
   silently overrode the homepage's own stat grid further up this file (later rule
   wins) and forced its six stats into four columns, leaving a hole in row two. */
.arch-results .stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1.4rem;
  margin-top: 0;
  background: none;
  border: 0;
}
.arch-results .stat {
  background: none;
  border-top: 1px solid var(--hair-strong);
  padding: 1.1rem 0 0;
  min-width: 0;                 /* let a wide figure shrink instead of overflowing */
}
.stat__n {
  font-family: var(--display);
  font-weight: 700;
  font-stretch: 125%;
  font-size: clamp(1.75rem, 2.9vw, 2.6rem);
  line-height: 1;
  color: var(--ivory);
  margin-bottom: 0.6rem;
}
.stat__l { color: var(--head); line-height: 1.4; margin-bottom: 0.35rem; }
.stat__s { font-family: var(--mono); font-size: 0.72rem; color: var(--text-dim); }

.statrows { display: flex; flex-direction: column; }
.statrow {
  display: grid;
  /* Third column held "124,575 -> 406,124" until 2026-08-01, then each row's own
     measurement window. Since 2026-08-03 all three stores share one window and
     one metric, but the column still names both so a row can be read alone. */
  /* Column 2 was 6rem until 2026-08-04, then 8rem, now 10rem. "+104%" at 1.9rem
     display weight is nearly the full 8rem on its own, which left the number
     touching its own label. 10rem keeps clear air between the two. */
  grid-template-columns: 9rem 10rem 14rem 1fr;
  align-items: baseline;
  gap: 1rem;
  padding: 1.1rem 0;
  border-top: 1px solid var(--hair);
}
.statrow:last-child { border-bottom: 1px solid var(--hair); }
.statrow__name {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--head);
}
.statrow__n {
  font-family: var(--display);
  font-weight: 700;
  font-stretch: 125%;
  font-size: 1.9rem;
  line-height: 1;
  color: var(--purple-txt);
}
.statrow__d { font-family: var(--mono); font-size: 0.8rem; color: var(--head); }
.statrow__note { color: var(--text-dim); font-size: 0.92rem; line-height: 1.5; }

.statnote { margin-top: 2.6rem; max-width: 46rem; }
.statnote p { color: var(--head); line-height: 1.7; }
.statnote strong { color: var(--ivory); }
.statnote__fine {
  margin-top: 1rem;
  font-size: 0.82rem !important;
  color: var(--text-dim) !important;
}

.statnote p + p { margin-top: 0.9rem; }

/* Skip link: off-screen until focused, then a normal-looking control. Never
   display:none, which removes it from the tab order and defeats the point. */
.skip {
  position: absolute;
  left: 1rem;
  top: -4rem;
  z-index: 100;
  padding: 0.7rem 1.1rem;
  background: var(--purple-lo);
  color: #0A0A0A;
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  border-radius: 2px;
  transition: top 0.18s ease;
}
.skip:focus { top: 1rem; }
#work-start { scroll-margin-top: 5rem; }

/* ---- Hero stat strip: the proof, above the tiles ----
   Anchors, not decoration. Each one jumps to the section that defends it. */
.hero-stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--hair);
  border: 1px solid var(--hair);
  margin-top: 3.2rem;
}
.hero-stat {
  display: block;
  background: var(--ink);
  padding: 1.6rem 1.4rem;
  text-decoration: none;
  min-width: 0;
  transition: background 0.25s ease;
}
.hero-stat:hover, .hero-stat:focus-visible { background: var(--slate); }
.hero-stat__n {
  display: block;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(1.7rem, 3vw, 2.5rem);
  line-height: 1;
  color: var(--purple-txt);
  margin-bottom: 0.7rem;
}
.hero-stat:hover .hero-stat__n { color: var(--ivory); }
.hero-stat__l { display: block; color: var(--head); font-size: 0.9rem; line-height: 1.45; }

@media (max-width: 900px) { .hero-stats { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .hero-stats { grid-template-columns: 1fr; } }

/* ---- Secondary results panels: organic search, AI search ----
   Same visual family as .arch-results but a lighter weight, so the local-search
   block above stays the headline and these read as further channels rather than
   as three competing conclusions. */
.arch-panel { padding-top: 4rem; scroll-margin-top: 5rem; }
.arch-panel__eyebrow {
  font-family: var(--mono);
  font-size: 0.92rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--purple-txt);
  margin-bottom: 0.9rem;
}
.prows {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 1.4rem;
}
.prow {
  border-top: 1px solid var(--hair-strong);
  padding-top: 1.1rem;
  min-width: 0;
}
.prow__n {
  display: block;
  font-family: var(--display);
  font-weight: 700;
  /* Not font-stretch: 125% like .stat__n. These figures run long ("15.6 -> 10.7")
     and the expanded face pushed them onto two lines in a narrow column. */
  font-size: clamp(1.5rem, 2.4vw, 2.1rem);
  line-height: 1.05;
  color: var(--ivory);
  margin-bottom: 0.6rem;
}
.prow__l { display: block; color: var(--head); line-height: 1.45; margin-bottom: 0.35rem; }
.prow__s { display: block; font-family: var(--mono); font-size: 0.72rem; color: var(--text-dim); }

@media (max-width: 900px) {
  .arch-results .stats { grid-template-columns: repeat(2, 1fr); gap: 1.8rem 1.2rem; }
  .statrow { grid-template-columns: 1fr 1fr; row-gap: 0.35rem; }
  .statrow__note { grid-column: 1 / -1; }
  .prows { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  .arch-results .stats { grid-template-columns: 1fr; }
  .prows { grid-template-columns: 1fr; }
}

/* ---- Client grid: the homepage entry points into each case page ---- */
.clients {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1.4rem;
  margin-top: 3rem;
}
.client {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  border: 1px solid var(--hair);
  border-radius: 4px;
  background: rgba(245, 243, 236, 0.02);
  overflow: hidden;
  transition: border-color 0.4s var(--ease), background 0.4s var(--ease), transform 0.4s var(--ease);
}
.client:hover {
  border-color: var(--purple-lo);
  background: rgba(107, 60, 199, 0.07);
  transform: translateY(-2px);
}
.client__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3.4rem 2rem;
  min-height: 11rem;
  border-bottom: 1px solid var(--hair);
  background: rgba(0, 0, 0, 0.25);
}
.client__mark img {
  max-width: min(72%, 20rem);
  max-height: 4.4rem;          /* logos differ in aspect; cap height so the panels match */
  width: auto;
  height: auto;
  object-fit: contain;
  opacity: 0.92;
  transition: opacity 0.4s var(--ease);
}
.client:hover .client__mark img { opacity: 1; }
.client__body { display: flex; flex-direction: column; gap: 0.7rem; padding: 1.6rem 1.7rem 1.8rem; }
.client__name {
  font-family: var(--display);
  font-weight: 600;
  font-stretch: 125%;
  font-size: 1.28rem;
  color: var(--ivory);
}
.client__desc { color: var(--text-dim); line-height: 1.6; font-size: 0.96rem; }
.client__tags {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--head);
  line-height: 1.7;
}
.client__cta {
  font-family: var(--mono);
  font-size: 0.9rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--purple-txt);
  margin-top: 0.3rem;
}
@media (max-width: 800px) {
  .clients { grid-template-columns: 1fr; }
  .client__mark { padding: 2.6rem 1.5rem; min-height: 9rem; }
}
.arch-grid--2 { columns: 2; }
@media (max-width: 1000px) { .arch-grid--2 { columns: 1; } }

/* ============================================================
   LEGAL PAGES (privacy.html, terms.html)
   Moved here from inline <style> blocks on 2026-08-06. They cannot
   live inline: the production Content-Security-Policy in _headers
   allows styles only from our own files, so an inline block is
   dropped and both pages render as unstyled full-bleed text. That
   breakage is invisible locally unless the preview server is run
   with --csp.
   ============================================================ */
.legal-page {
  max-width: 72ch;
  margin-inline: auto;
  padding: 8rem var(--pad) 6rem;
}
.legal-page h1 {
  font-family: var(--display);
  font-weight: 700;
  font-stretch: 125%;
  font-size: clamp(1.75rem, 1.3rem + 2vw, 2.6rem);
  color: var(--head);
  margin: 0 0 0.5rem;
  letter-spacing: -0.01em;
}
.legal-page .meta {
  font-family: var(--mono);
  font-size: 0.74rem;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  margin: 0 0 3rem;
}
.legal-page h2 {
  font-family: var(--display);
  font-weight: 700;
  font-stretch: 125%;
  font-size: 1.2rem;
  color: var(--head);
  margin: 2.5rem 0 0.75rem;
}
.legal-page p,
.legal-page li {
  font-size: 1rem;
  line-height: 1.7;
  color: var(--text);
  margin: 0 0 1rem;
}
.legal-page ul { padding-left: 1.4rem; margin: 0 0 1rem; }
.legal-page a { color: var(--ivory); }
.legal-page a:hover { color: var(--purple-txt); }
.legal-page .back {
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 3rem;
}
.legal-page .back:hover { color: var(--ivory); }
.legal-page hr { border: none; border-top: 1px solid var(--hair); margin: 2.5rem 0; }
.legal-page .notice {
  border: 1px solid var(--hair);
  border-radius: 3px;
  padding: 1.25rem 1.5rem;
  background: rgba(13, 13, 17, 0.7);
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--text-dim);
  margin-bottom: 2rem;
}

/* ============================================================
   Concept-work disclosure + the film block (added 2026-08-06)
   ============================================================ */

/* The sentence that says a piece was not commissioned. Deliberately NOT styled
   as fine print: it sits at lede weight, because a disclosure a reader has to
   hunt for is not a disclosure. Bordered on the left so it reads as an aside
   rather than as more marketing copy. */
.arch-note {
  margin: 1.4rem 0 0;
  padding-left: 1rem;
  border-left: 2px solid var(--purple-lo);
  max-width: 60ch;
  font-size: 0.95rem;
  line-height: 1.65;
  color: var(--text-dim);
}

/* A case whose deliverable is a film. The clip is 9:16, so it is capped by
   HEIGHT rather than width: a vertical video allowed to fill a desktop column
   is taller than the screen and the viewer never sees the whole frame. */
.film { padding-top: 1rem; }
.film__frame {
  display: flex;
  justify-content: center;
}
.film__el {
  width: auto;
  height: min(78vh, 720px);
  max-width: 100%;
  aspect-ratio: 9 / 16;
  display: block;
  background: #000;
  border-radius: var(--r-lg, 18px);
  box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.75);
}
/* A landscape clip is the opposite problem: capped by height it ends up a
   letterbox strip across the middle of the page, so it is capped by WIDTH and
   takes its shape from the width and height attributes on the element. Added
   2026-08-25 for YOIN, whose clips are 16:9. */
.film__el--wide {
  width: min(100%, 1120px);
  height: auto;
  aspect-ratio: auto;
}
.film__cap {
  margin: 1.1rem auto 0;
  text-align: center;
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: var(--text-dim);
}

/* On a phone the clip is already the width of the screen; height-capping it
   there just leaves bars down both sides. */
@media (max-width: 700px) {
  .film__el { width: 100%; height: auto; }
}

/* ---- Concept-work card in the client grid (added 2026-08-06) ----
   Carries iiJin's own mark on the same dark panel the client cards use
   (Justin's call).

   It gets a taller cap than the other three because it is a much squarer
   lockup: the client logos run 3.5:1 to 5.6:1, this one is 2.2:1. Capping all
   four at the same height made the letters here render about half the size of
   the others and the card looked like an afterthought. Matching the LETTER
   size, not the box height, is what makes the four panels sit level. */
.client--concept .client__mark img { max-height: 5.4rem; }

/* The one thing separating this card from a client card. Do not soften it. */
.client__badge {
  align-self: flex-start;
  font-family: var(--mono);
  /* Was 0.62rem, the smallest text on the site. This badge is load-bearing on
     the iiJin card: it is what separates concept work from a client
     engagement, so it has to be readable, not decorative. (2026-08-21) */
  font-size: 0.78rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--purple-hi, var(--purple-txt));
  border: 1px solid var(--purple-lo);
  border-radius: 999px;
  padding: 0.28rem 0.6rem;
  margin-bottom: -0.15rem;
}

/* ============================================================
   SERVICE PAGES (build_services.py, added 2026-08-21)
   ============================================================ */

/* The service pages have no creature layer behind them, so the hero does not
   need the homepage's full viewport height. It needs to clear the fixed nav
   and start reading immediately. */
.svc-hero { padding-top: 8rem; }

/* Q&A. Deliberately readable rather than a compact accordion: these answers are
   the site's answer-engine surface, and collapsing them behind a click hides
   them from some extractors and from anyone skimming. */
.faq-list {
  display: grid;
  gap: 1.25rem;
  margin-top: 2.5rem;
}
.faq {
  border: 1px solid var(--hair);
  border-radius: 2px;
  padding: 1.85rem;
  background: rgba(13, 13, 17, 0.6);
}
.faq__q {
  font-family: var(--display);
  font-weight: 700;
  font-stretch: 110%;
  font-size: clamp(1.12rem, 1rem + 0.5vw, 1.35rem);
  line-height: 1.25;
  color: var(--head);
  margin: 0 0 0.75rem;
  text-wrap: balance;
}
.faq__a {
  margin: 0;
  color: var(--text);
  line-height: 1.65;
  max-width: 68ch;
}

/* Footer service index. Every service page links to every other, so none is an
   orphan and the eight read as one section. */
.svcnav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.4rem;
  padding-top: 1.6rem;
  margin-top: 1.6rem;
  border-top: 1px solid var(--hair);
  font-family: var(--mono);
  font-size: 0.86rem;
}
.svcnav__link { color: var(--text-dim); text-decoration: none; transition: color 0.2s; }
.svcnav__link:hover { color: var(--ivory); }
.svcnav__link.is-current { color: var(--purple-txt); }

/* The related-services cards are links; the base .svc-card is not, so it has no
   text-decoration or colour reset of its own. */
a.svc-card { text-decoration: none; color: inherit; }
/* The homepage service cards became links to the service pages (2026-08-21).
   Pushed to the bottom of the card so the eight line up regardless of how many
   lines the description above runs to. */
.svc-card__cta {
  margin-top: auto;
  padding-top: 0.9rem;
  font-family: var(--mono);
  font-size: 0.86rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--purple-txt);
}

@media (max-width: 700px) {
  .svc-hero { padding-top: 6.5rem; }
  .faq { padding: 1.35rem; }
}

.svc-keeps {
  /* Added 2026-08-21 instead of the deliverables list a GPT audit asked for.
     A deliverables list pins scope on a page whose whole job is to get a
     conversation that sets scope, and it is the easiest thing on earth for a
     competitor to copy. Ownership is the harder claim and it is already the
     spine of these pages: the client owns the ad account, the photos, the
     domain, the list. This surfaces that thread instead of burying it in FAQs. */
  margin: 2.2rem 0 0;
  padding: 1.15rem 1.35rem;
  border-left: 2px solid var(--purple-lo);
  background: rgba(255, 255, 255, 0.022);
  font-size: 1.02rem;
  line-height: 1.65;
  color: var(--text);
  text-wrap: pretty;
}
.svc-keeps__label {
  display: block;
  margin-bottom: 0.3rem;
  font-family: var(--mono);
  font-size: 0.82rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--purple-txt);
}

/* ---------------------------------------------------------------------------
   Portfolio cards now lead with the WORK, not the logo. Added 2026-08-24.
   Justin's words: "my portfolio is minimized". The grid showed four wordmarks
   and a paragraph each, while 73 real work files sat in assets/work/ appearing
   nowhere on the home page. The logo panel is kept, underneath, because it is
   what tells a human who the client is at a glance.
   --------------------------------------------------------------------------- */
.client__shot {
  display: block;
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border-bottom: 1px solid var(--hair);
  background: rgba(0, 0, 0, 0.4);
}
.client__shot img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 20%;
  /* Slightly over-scaled so the slow settle on hover has somewhere to travel
     without exposing an edge. */
  transform: scale(1.04);
  transition: transform 0.9s var(--ease), filter 0.9s var(--ease);
  filter: saturate(0.92);
}
.client:hover .client__shot img { transform: scale(1.10); filter: saturate(1); }
/* The logo panel was carrying the whole card. With a real image above it, it
   becomes a caption bar rather than a hero. */
.client__mark { padding: 1.5rem 2rem; min-height: 6.5rem; }
.client__mark img { max-height: 3.2rem; }
@media (max-width: 800px) {
  .client__shot { aspect-ratio: 16 / 9; }
  .client__mark { padding: 1.2rem 1.5rem; min-height: 5.5rem; }
}

/* The real wordmark, added 2026-08-24. The nav and footer had been setting
   "VEDA" + "9" as live text in Archivo, which is close to the mark but is not
   the mark: the drawn wordmark has its own spacing and its own purple on the 9.
   The SVG is 6158x1000, so it is sized by height and lets width follow. */
.wordmark--svg { min-height: 44px; }
.wordmark--svg img {
  display: block;
  height: 1.05rem;
  width: auto;
  max-width: none;
}
.nav .wordmark--svg img { height: 1rem; }
.footer .wordmark--svg img { height: 1.3rem; }
@media (max-width: 700px) { .wordmark--svg img { height: 0.92rem; } }
/* The iiJin work is a VERTICAL film, so every still is 1080x1920. Cropping one
   to a 16:10 card cut the model's head off and put a bright white product shot
   on a near-black page. Contained on its own dark ground, the poster reads as
   a poster, which is what it is. */
.client__shot--contain { background: #0a0a0d; }
.client__shot--contain img { object-fit: contain; transform: none; }
.client:hover .client__shot--contain img { transform: scale(1.03); }

/* ---------------------------------------------------------------------------
   Pinned act: "the whole picture". Rebuilt 2026-08-24 after the full-bleed
   version was rejected. Copy holds the left column, the work sits in a framed
   plate on the right, and the plate changes as each line takes over.
   No scrim anywhere: the type has its own dark ground, so the screenshots keep
   their full contrast and stay readable as work.
   --------------------------------------------------------------------------- */
.act { position: relative; }
.wp {
  position: relative;
  display: grid;
  align-items: center;
  background: #0b0b10;
  border-block: 1px solid var(--hair);
}
.wp__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.78fr) minmax(0, 1.22fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: center;
  width: 100%;
}

/* ---- left: the argument ---- */
.wp__copy { position: relative; }
.wp__head { max-width: 15ch; }
/* The six lines all occupy the same box and cross over inside it. The box is
   sized by the longest of them so nothing below it moves as they swap. */
.wp__lines {
  position: relative;
  margin-top: clamp(1.6rem, 3vh, 2.6rem);
  min-height: 5.2em;
}
.wp__line {
  position: absolute;
  inset: 0 0 auto 0;
  max-width: 26ch;
  font-size: clamp(1.35rem, 1rem + 1.1vw, 2rem);
  line-height: 1.32;
  color: var(--ivory);
  margin: 0;
}
.wp__close {
  margin-top: clamp(1rem, 2vh, 1.8rem);
  max-width: 30ch;
  color: var(--purple-txt);
  font-size: clamp(1rem, 0.9rem + 0.4vw, 1.25rem);
}

/* ---- right: the work ---- */
.wp__stack {
  position: relative;
  aspect-ratio: 4 / 3;
  width: 100%;
  max-height: 74vh;
  justify-self: end;
}
.wp__plate {
  position: absolute;
  inset: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--hair);
  border-radius: 8px;
  background: #06060a;
  /* Offset and blur, tinted to the canvas hue. A real raised thing casts light
     downward; a zero-offset halo is decoration. */
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.85), 0 2px 6px rgba(0, 0, 0, 0.5);
  /* Arrives at --a, LEAVES at --b, which is the next plate's --a. Fading in
     only meant two plates were half-visible through every transition, which is
     mush rather than a crossfade. The last plate's --b is past 1 so it holds. */
  /* The outgoing plate is fully gone BEFORE the incoming one starts, rather
     than crossfading with it. Two dense screenshots at half opacity read as a
     printing error, not as a transition: it leaves across the 0.02 in front of
     --b, and the next one arrives across the 0.02 after its own --a, which is
     the same instant. Nothing is ever double-exposed. */
  opacity: calc(
    clamp(0, calc((var(--sc-p, 0) - var(--a)) / 0.02), 1) -
    clamp(0, calc((var(--sc-p, 0) - var(--b) + 0.02) / 0.02), 1));
  will-change: opacity, transform;
}
.wp__plate img {
  flex: 1 1 auto;
  width: 100%;
  min-height: 0;
  /* contain, not cover: these are screenshots and decks, so a crop hides the
     thing the plate exists to show. Letterboxing against the plate's own dark
     ground is the correct trade here. */
  object-fit: contain;
  object-position: 50% 50%;
  padding: 0.6rem 0.6rem 0;
  transform: scale(calc(0.97 + 0.03 * clamp(0, calc((var(--sc-p, 0) - var(--a)) / 0.06), 1)));
}
.wp__plate figcaption {
  flex: 0 0 auto;
  padding: 0.7rem 1rem;
  border-top: 1px solid var(--hair);
  background: rgba(0, 0, 0, 0.5);
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--purple-txt);
}

@media (prefers-reduced-motion: reduce) {
  .wp__plate { transform: none; }
}

@media (max-width: 900px) {
  .wp__grid { grid-template-columns: 1fr; gap: 1.6rem; }
  .wp__head { max-width: none; font-size: 1.5rem; }
  .wp__lines { min-height: 4.6em; }
  .wp__line { max-width: none; font-size: clamp(1.1rem, 4.8vw, 1.5rem); }
  .wp__stack { aspect-ratio: 16 / 11; }
  .wp__close { max-width: none; }
}

/* The nine mark on the deck's centre card, added 2026-08-24. It replaced a
   generic four-point sparkle. Sized off the old sparkle's box so the card's
   rhythm does not change, and it inherits nothing: the SVG carries its own
   purple gradient, which is the brand's. */
.ncard__nine {
  display: block;
  width: auto;
  /* Same box the sparkle held, so the card's rhythm is unchanged. */
  height: clamp(58px, 13vw, 104px);
  margin: 0 auto 1.4rem;
  filter: drop-shadow(0 0 26px rgba(255, 255, 255, 0.4));
}
/* What the measurement card reports, filling what was half a card of empty
   space under a claim. */
.ncard__list {
  margin: 0.9rem 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.5rem;
}
.ncard__list li {
  position: relative;
  padding-left: 1.1em;
  font-size: 0.9rem;
  line-height: 1.55;
  color: var(--text-dim);
}
.ncard__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 0.5em;
  height: 1px;
  background: var(--purple-txt);
}


/* ---------------------------------------------------------------------------
   CASE PAGE: WALKTHROUGH + DESIGN LANGUAGE
   Added 2026-08-25. Justin, pointing at a refero.design style reference: the
   case pages should open with the site moving, then a short write-up, then the
   design language itself. Two reasons it earns its place over more screenshots.
   A scroll-driven page cannot be photographed, and a palette shown with its
   roles is the part of the work a prospect cannot see by looking at pictures.
--------------------------------------------------------------------------- */
.walk { padding-top: 0; }
.walk__fig { margin: 0; }
.walk__el {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 12px;
  background: #08070A;
}
.walk__cap {
  margin: 0.9rem 0 0;
  font-size: 0.86rem;
  line-height: 1.55;
  color: var(--text-dim);
  max-width: 46rem;
}

.dl__h {
  margin: 2.6rem 0 1rem;
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* Each swatch is a chip plus its NAME, its hex and the job it does. The role is
   the whole point: a strip of colour with no roles is a mood board, and the
   thing worth showing a prospect is that every colour was given a job. */
.dl-swatches, .dl-fonts, .dl-scales, .dl-rules__list {
  margin: 0; padding: 0; list-style: none;
}
.dl-swatches {
  display: grid;
  gap: 1px;
  background: var(--line, rgba(255,255,255,0.1));
  border: 1px solid var(--line, rgba(255,255,255,0.1));
  border-radius: 10px;
  overflow: hidden;
}
.dl-sw {
  display: grid;
  grid-template-columns: 2.4rem 9rem 5.5rem 1fr;
  align-items: center;
  gap: 1rem;
  padding: 0.85rem 1.1rem;
  background: var(--bg, #0b0b0f);
}
.dl-sw__chip {
  width: 2.4rem; height: 2.4rem;
  border-radius: 6px;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.14);
}
.dl-sw__name { font-weight: 600; font-size: 0.92rem; }
.dl-sw__hex {
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--text-dim);
  text-transform: uppercase;
}
.dl-sw__role { font-size: 0.86rem; line-height: 1.5; color: var(--text-dim); }
@media (max-width: 720px) {
  .dl-sw { grid-template-columns: 2.4rem 1fr; row-gap: 0.35rem; }
  .dl-sw__hex, .dl-sw__role { grid-column: 2; }
}

.dl-fonts { display: grid; gap: 1.4rem; }
.dl-font { display: grid; gap: 0.3rem; }
.dl-font__name { font-size: 1.9rem; line-height: 1.1; }
.dl-font__role { font-size: 0.86rem; line-height: 1.5; color: var(--text-dim); }

/* The scale is set in the MONO face on purpose. These are specifications, not
   specimens, and setting a spec in the display face invites reading it as one. */
.dl-scales {
  margin-top: 1.6rem;
  display: grid;
  gap: 0.5rem;
}
.dl-scale {
  display: grid;
  grid-template-columns: 8rem 1fr;
  gap: 1rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--line, rgba(255,255,255,0.08));
}
.dl-scale__role { font-size: 0.86rem; font-weight: 600; }
.dl-scale__spec {
  font-family: var(--mono);
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--text-dim);
}
@media (max-width: 720px) {
  .dl-scale { grid-template-columns: 1fr; gap: 0.2rem; }
}

.dl-rulesets {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.6rem;
}
@media (max-width: 800px) { .dl-rulesets { grid-template-columns: 1fr; } }
.dl-rules {
  padding: 1.3rem 1.4rem;
  border: 1px solid var(--line, rgba(255,255,255,0.1));
  border-radius: 10px;
}
.dl-rules__h {
  margin: 0 0 0.9rem;
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.dl-rules--do .dl-rules__h { color: #7ddc9a; }
.dl-rules--dont .dl-rules__h { color: #e8836f; }
.dl-rules__list { display: grid; gap: 0.7rem; }
.dl-rules__list li {
  position: relative;
  padding-left: 1.1em;
  font-size: 0.9rem;
  line-height: 1.55;
  color: var(--text-dim);
}
.dl-rules__list li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.62em;
  width: 0.5em; height: 1px;
}
.dl-rules--do li::before { background: #7ddc9a; }
.dl-rules--dont li::before { background: #e8836f; }

/* ============================================================
   Mobile menu + the promise H1. Added 2026-09-02.
   ============================================================ */



/* The CTA carries two labels and shows one. On a phone the long version and a
   menu toggle do not fit the same row. */
.nav__cta-short { display: none; }

.nav__toggle {
  display: none;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 1px solid var(--hair, rgba(255, 255, 255, 0.16));
  border-radius: 0.4rem;
  background: transparent;
  cursor: pointer;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.28rem;
}
.nav__toggle span {
  display: block;
  width: 1.05rem;
  height: 1.5px;
  background: var(--ivory, #f4f1ea);
  transition: transform 0.25s var(--ease), opacity 0.2s var(--ease);
}
.nav__toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(0.44rem) rotate(45deg); }
.nav__toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav__toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-0.44rem) rotate(-45deg); }
.nav__toggle:focus-visible { outline: 2px solid var(--purple-txt, #b79cff); outline-offset: 2px; }

@media (max-width: 860px) {
  .nav__cta-long { display: none; }
  .nav__cta-short { display: inline; }
  .nav__toggle { display: flex; }

  /* Overrides the display:none at line ~952. The links now live in a panel
     under the bar rather than being deleted from the page.

     Animated with opacity and transform rather than max-height. max-height was
     the first attempt; animating it needs a guessed fixed height and janks when
     the guess is wrong, so this is the better implementation either way.

     HONEST NOTE ON WHAT WAS AND WAS NOT VERIFIED, 2026-09-02. The open/close
     ANIMATION could not be tested. The browser-automation session this was
     built in has a frozen animation timeline: document.timeline.currentTime
     stayed at 0 across 500ms, and a freshly created control div with a plain
     0.2s opacity transition also never moved off 0. So every transitioned
     property reads stuck at its start value there, which for a while looked
     like a cascade bug and is not one.

     What IS verified: the cascade resolves to the right rule
     (.nav.is-open .nav__links matches, element.matches() true), and every
     property that applies WITHOUT a transition flips correctly on toggle
     (visibility hidden -> visible, pointer-events none -> auto, aria-expanded,
     the is-open class). With transitions forced off the panel measured 182px
     tall and laid out correctly. The animation itself needs a real device. */
  .nav__links {
    display: flex !important;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--obsidian, #0a0a0a);
    border-bottom: 1px solid var(--hair, rgba(255, 255, 255, 0.14));
    padding: 0 1.25rem;
    max-height: 70vh;
    overflow-y: auto;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transform: translateY(-8px);
    transition: opacity 0.22s var(--ease), transform 0.22s var(--ease), visibility 0s linear 0.22s;
  }
  .nav.is-open .nav__links {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: none;
    transition: opacity 0.22s var(--ease), transform 0.22s var(--ease), visibility 0s;
  }
  .nav__links a {
    padding: 1rem 0;
    font-size: 1.05rem;
    border-bottom: 1px solid var(--hair, rgba(255, 255, 255, 0.09));
  }
  .nav__links a:last-child { border-bottom: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .nav__links, .nav__toggle span { transition: none !important; }
}

/* The H1 is the two-word brand line again (2026-09-02), so hero__title's own
   margin:0 is correct once more. What is
   still needed is air between the H1 and the supporting line, and between the
   two supporting lines, because they are separate thoughts rather than one
   paragraph broken in half. */
.hero__title { margin-bottom: 1.4rem; }
.hero__inner .lede + .lede { margin-top: 0.9rem; }
