/* ==========================================================================
   Target C Investigations and Forensics Pvt. Ltd.
   Design tokens + homepage styles.

   THIS FILE IS THE SOURCE OF TRUTH FOR TOKENS. Do not take colour, type or
   spacing values for this project from any markdown document.

   Shape lock: border-radius is 0 everywhere. Do not introduce rounded
   corners.
   Colour lock: one accent (red). It is a highlight, never a field.
   ========================================================================== */

/* ==========================================================================
   Typefaces. Self-hosted from vendor/fonts/ — no Google Fonts, no CDN, so a
   page load makes zero third-party requests. That is a privacy commitment
   this site makes in writing (privacy-policy.html, "Cookies, fonts, and what
   this site loads"), so do not reintroduce a remote font link without
   rewriting that clause first.

   Files are the Google Fonts woff2 subsets, unchanged, with their OFL
   licences alongside them in vendor/fonts/ as the SIL licence requires.
   Archivo is the variable file: one download covers wght 400-700 and
   wdth 62-125%. IBM Plex Mono is static, so 400 and 500 are separate files.

   The unicode-range on each face is Google's own. It is load-bearing: the
   browser fetches a file only if the page actually contains a character in
   that range, so the latin-ext faces cost nothing on today's copy (which is
   pure latin) and cover diacritics if a name later needs them.

   font-display: swap matches what the Google stylesheet was doing, so the
   fallback-then-swap behaviour is unchanged. See the .hero__title mask note
   for why the swap frame matters there.
   ========================================================================== */

/* Archivo — variable, wght 400..700, wdth 62%..125% */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url('vendor/fonts/archivo-var-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url('vendor/fonts/archivo-var-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* IBM Plex Mono — 400 and 500 */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('vendor/fonts/plex-mono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('vendor/fonts/plex-mono-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('vendor/fonts/plex-mono-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('vendor/fonts/plex-mono-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* --- Ink ------------------------------------------------------------- */
  --ink:            #0B0B0C;   /* page ground. deliberately not #000 */
  --ink-raised:     #121214;   /* hover / raised panels */
  --ink-sunk:       #080809;   /* bands that sit below the page plane */

  /* --- Accent ---------------------------------------------------------- */
  /* PROVISIONAL. Re-sample the exact red from the client logo when the SVG
     arrives, then change it here only. 3.84:1 on --ink: passes AA for large
     text and UI, FAILS AA for body text. Never set body copy in red. */
  --red:            #D6202B;
  --red-deep:       #A5161F;   /* pressed / hover fill */
  --red-tick:       #FF3B3B;   /* non-text reticle ticks only */

  /* The C only. Deeper and slightly less saturated than --red: at a 7px
     stroke that much area at full chroma reads cheap, and the mark has to
     hold its own next to the one solid red button on the screen. */
  --red-mark:       #B4212A;

  /* Brand red is fine on large display type (3:1 threshold) but fails the
     4.5:1 body-text threshold on --ink. Anything red and under ~24px uses this
     lighter tint instead: 5.9:1 on --ink. Same hue, still unmistakably red.
     Re-derive this alongside --red when the logo lands. */
  --red-text:       #FF4B4B;

  /* --- Text ------------------------------------------------------------ */
  --text:           #F2F2F0;   /* 17.6:1 on --ink */
  --text-dim:       #A6A6AA;   /* 7.2:1 on --ink */
  --text-faint:     #77777C;   /* 4.5:1 on --ink. lower bound for any text */

  /* --- Lines ----------------------------------------------------------- */
  --line:           rgba(242, 242, 240, 0.10);
  --line-strong:    rgba(242, 242, 240, 0.22);

  /* --- Type ------------------------------------------------------------ */
  --font-sans: 'Archivo', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;

  /* Capped at 3.6rem on purpose. The h1 is seven words: at display sizes above
     this it runs past two lines, which is a font-scale error, not a copy
     problem. Restraint is also the brief. */
  --fs-display: clamp(2.25rem, 1.25rem + 3.2vw, 3.6rem);
  --fs-h2:      clamp(1.85rem, 1.15rem + 2.7vw, 3.15rem);
  --fs-h3:      clamp(1.15rem, 1.05rem + 0.42vw, 1.4rem);
  --fs-lead:    clamp(1.05rem, 0.97rem + 0.44vw, 1.28rem);
  --fs-body:    1.0625rem;
  --fs-small:   0.9375rem;
  --fs-micro:   0.75rem;

  /* --- Space ----------------------------------------------------------- */
  --s-2:  0.5rem;
  --s-3:  0.75rem;
  --s-4:  1rem;
  --s-5:  1.5rem;
  --s-6:  2rem;
  --s-7:  3rem;
  --s-8:  4rem;
  --s-9:  6rem;

  --gutter: clamp(1.25rem, 4vw, 3.5rem);
  --measure: 1320px;
  --nav-h: 72px;

  /* --- Motion ---------------------------------------------------------- */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --t-fast: 140ms;
  --t-mid:  240ms;

  /* --- Layers ---------------------------------------------------------- */
  --z-nav: 100;
  --z-float: 90;
  --z-grain: 80;
  --z-skip: 200;

  /* Dark-only page. Tells the UA to render native scrollbars, the <select>
     popup and form controls dark rather than light-on-dark. */
  color-scheme: dark;
}

/* ==========================================================================
   Reset
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + var(--s-5));
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  font-variation-settings: 'wdth' 100;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  /* clip, not hidden: `hidden` turns body into a scroll container and makes
     the sticky header unreliable. */
  overflow-x: clip;
}

h1, h2, h3, p, figure, blockquote, ul, ol { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }

a { color: inherit; text-decoration: none; }

a, button, label, select, summary {
  touch-action: manipulation;              /* kills the 300ms double-tap delay */
  -webkit-tap-highlight-color: rgba(214, 32, 43, 0.24);
}

:focus-visible {
  outline: 2px solid var(--red);
  outline-offset: 3px;
}

::selection { background: var(--red); color: #fff; }

.skip-link {
  position: absolute;
  left: var(--s-4);
  top: -100px;
  z-index: var(--z-skip);
  padding: var(--s-3) var(--s-5);
  background: var(--red);
  color: #fff;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  /* No transition. The skip link is keyboard-initiated and it is the first
     thing a keyboard or screen-reader user reaches; animating its arrival
     delays exactly the moment they are waiting on. It also animated `top`,
     which is a layout property. Do not put either back. */
}
.skip-link:focus { top: var(--s-4); }

/* ==========================================================================
   Layout primitives
   ========================================================================== */

.wrap {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: clamp(4rem, 9vw, 8rem); }

/* Small mono label above a headline. Rationed: max one per three sections. */
.eyebrow {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: var(--s-5);
}

/* Outside the hero an eyebrow is plain small text, not a tracked caps tag. */
.page-hero .eyebrow,
.related-wrap .eyebrow {
  font-family: var(--font-sans);
  font-size: var(--fs-small);
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-dim);
}

.h2 {
  font-size: var(--fs-h2);
  font-weight: 600;
  font-variation-settings: 'wdth' 108;
  line-height: 1.08;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.lead {
  font-size: var(--fs-lead);
  line-height: 1.55;
  color: var(--text-dim);
  max-width: 58ch;
}

/* ==========================================================================
   Buttons. Never animated: these sit on the enquiry path.
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  min-height: 48px;
  padding: var(--s-3) var(--s-6);
  border: 1px solid transparent;
  background: none;
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-align: center;
  cursor: pointer;
  /* Colour changes take the built-in `ease`, which is the right curve for a
     tint. The transform takes --ease-out: a movement on a built-in easing
     reads soft, and this is the enquiry button. */
  transition: background-color var(--t-fast) ease,
              border-color var(--t-fast) ease,
              color var(--t-fast) ease,
              transform var(--t-fast) var(--ease-out);
}

.btn:active { transform: translateY(1px); }

/* White on --red is 5.1:1. Passes AA. */
.btn--primary { background: var(--red); color: #fff; border-color: var(--red); }
.btn--primary:hover { background: var(--red-deep); border-color: var(--red-deep); }

.btn--ghost { color: var(--text); border-color: var(--line-strong); }
.btn--ghost:hover { border-color: var(--text); background: var(--ink-raised); }

.btn__icon { width: 18px; height: 18px; flex: none; }

@media (prefers-reduced-motion: reduce) {
  .btn:active { transform: none; }
}

/* ==========================================================================
   Header
   ========================================================================== */

.nav {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  height: var(--nav-h);
  display: flex;
  align-items: center;
  background: rgba(11, 11, 12, 0.86);
  -webkit-backdrop-filter: blur(10px);     /* unprefixed only lands in Safari 18 */
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
}

/* Placeholder wordmark. Replace with the client logo SVG when it arrives. */
.wordmark {
  font-size: 1.0625rem;
  font-weight: 700;
  font-variation-settings: 'wdth' 112;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
}
.wordmark em { font-style: normal; color: var(--red); }

.nav__links {
  display: flex;
  align-items: center;
  gap: var(--s-6);
  margin-left: auto;
}

.nav__link {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  padding-block: var(--s-2);
  transition: color var(--t-fast) ease;
}
.nav__link:hover { color: var(--text); }

.nav__cta { padding-inline: var(--s-5); min-height: 42px; }

/* Below this the links drop and the Call CTA carries the header on its own.
   Acceptable while this is a one-page site where scrolling reaches every
   section. Revisit with a real menu when the four service pages ship and the
   header becomes the only route to them. */
@media (max-width: 680px) {
  .nav__links { display: none; }
}

/* ==========================================================================
   Hero
   ========================================================================== */

.hero {
  position: relative;
  padding-top: clamp(2.5rem, 6vw, 5rem);
  padding-bottom: clamp(3rem, 7vw, 6rem);
  border-bottom: 1px solid var(--line);
  /* The glow behind the mark is inset -22%, which is what gives it a soft
     falloff instead of a visible box edge. That overhang would otherwise
     extend the document past the viewport (measured: 1497px against a 1440
     viewport). clip, not hidden: hidden would make this a scroll container. */
  overflow: clip;
}

/* --- Film grain -----------------------------------------------------------
   One tiled feTurbulence, rendered once from a data URI. No request, no
   script, no layout box, no pointer events. At 2.5% it is under the level
   where you read it as texture and over the level where flat black looks
   dead. Sits above the page and below the nav, float and skip link.
   -------------------------------------------------------------------------- */

body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: 0.025;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
}

.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.86fr);
  gap: clamp(1.5rem, 3vw, 3rem);
  align-items: center;
}

.hero__title {
  font-size: var(--fs-display);
  font-weight: 700;
  font-variation-settings: 'wdth' 112;
  line-height: 0.98;
  letter-spacing: -0.035em;
  margin-bottom: var(--s-5);
}
.hero__title .accent { color: var(--red); }

/* Fixed line breaks, nothing more. These were overflow masks for the load
   sequence until the h1 was taken out of the motion gate on LCP grounds; the
   mask, its descender padding/margin pair and the .line__i inline-blocks all
   went with it, because a mask with nothing translating inside it only ever
   clips glyphs (line-height is 0.98 here, so the half-leading is negative and
   ascenders overhang the line box). Do not put overflow: hidden back. */
.hero__title .line { display: block; }

/* The tagline is 41 characters of mono: 0.6em advance plus tracking each, so
   394px at the default 0.2em. Below 900px the hero copy is the viewport minus
   55px, which put "• Intelligence" on a second line at every width up to 430.
   Tracking eases off with the viewport instead: 2.44vw is one 41st of it, so
   the line always lands ~15px short of its box. Capped at the default 0.2em,
   which it reaches at 463px, so nothing changes above that. At 0 tracking it
   still fits down to 360; the step below buys 344. At 320 it wraps, and the
   nbsp in the markup keeps each bullet with its word. */
.hero .eyebrow { letter-spacing: clamp(0px, calc(2.44vw - 8.9px), 0.2em); }
@media (max-width: 359px) {
  .hero .eyebrow { font-size: 0.6875rem; }
}

.hero__lead { margin-bottom: var(--s-7); max-width: 46ch; }

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
}

.hero__figure {
  position: relative;
  isolation: isolate;
  justify-self: end;
  width: min(100%, 560px);
}

/* Atmosphere behind the mark. A 5% lift at the centre falling to nothing by
   two-thirds out — enough that the black stops reading as flat, not enough to
   register as a glow. Under the SVG, no pointer events, no layout effect. */
.hero__figure::before {
  content: "";
  position: absolute;
  inset: -22%;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(circle at 50% 50%,
              rgba(242, 242, 240, 0.05) 0%,
              rgba(242, 242, 240, 0.018) 38%,
              rgba(242, 242, 240, 0) 68%);
}

/* Past 1200px the mark runs a little past the text column's right edge so it
   reads as placed rather than parked in its box. The bleed is capped well
   inside the wrap's own gutter, so nothing needs clipping to contain it. */
@media (min-width: 1200px) {
  .hero__figure {
    width: min(118%, 640px);
    margin-right: calc(var(--gutter) * -0.6);
  }
}

@media (max-width: 900px) {
  .hero__grid { grid-template-columns: 1fr; gap: var(--s-5); }
  /* The figure follows the copy on narrow screens. It was order: -1, which
     put a wordless graphic across the whole first screen of a phone and
     pushed the h1 below the fold — the one thing a firm that is currently
     invisible on Google cannot afford to do.

     Done with order rather than by moving the markup: the DOM keeps the h1
     first, which is what a screen reader announces and what the LCP
     candidate is measured from. Reading order stays correct; only the
     visual order changes.

     Still small. Below the copy it no longer competes for the first screen,
     and with the reduced top padding the headline and the Call button sit
     together above the fold on a 375x812 phone. */
  .hero { padding-top: var(--s-5); }
  .hero__figure { order: 2; width: min(50%, 188px); justify-self: start; }
  .hero__lead { margin-bottom: var(--s-6); }
}

/* --- Reticle -------------------------------------------------------------
   The signature. The innermost arc is an open ring: the C in Target C.
   Hand-authored because there is no logo asset yet and this is the brand
   mark, not decoration. Pure SVG, no raster, no external request.
   ------------------------------------------------------------------------ */

/* Clipped at the viewBox on purpose: the brackets converge in from outside
   the frame on load, and nothing is allowed to overflow the page. */
/* The linework carries the instrument. Previously the rings sat at 10% and
   the C at full chroma and 9px, so the mark read as a logo dropped on a
   nearly blank field. Everything structural is brought up into the 22-34%
   band where it resolves as drawn line, and the C comes down to meet it. */
.reticle { width: 100%; height: auto; position: relative; z-index: 1; }
.reticle__hair { fill: none; stroke: rgba(242, 242, 240, 0.30); stroke-width: 1; }
.reticle__ring { fill: none; stroke: rgba(242, 242, 240, 0.26); stroke-width: 1; }
.reticle__ring--dashed { stroke: rgba(242, 242, 240, 0.34); stroke-dasharray: 2 10; }
.reticle__tick { stroke: var(--text-dim); stroke-width: 1; }
.reticle__tick--card { fill: none; stroke: var(--red-tick); stroke-width: 1.5; }

/* Outer degree scale: 72 minor marks every 5 degrees, 12 major every 30.
   Static while the inner bezel turns, which is what makes the rotation
   legible as rotation. */
.reticle__scale { fill: none; stroke: rgba(242, 242, 240, 0.20); }
.reticle__scale--major { stroke: rgba(242, 242, 240, 0.40); }

.reticle__bracket { fill: none; stroke: var(--text-dim); stroke-width: 1.5; }
.reticle__c { fill: none; stroke: var(--red-mark); stroke-width: 7; stroke-linecap: butt; }
.reticle__dot { fill: var(--red); }
.reticle__sweep { stroke: var(--red); stroke-width: 1; opacity: 0; }

/* ==========================================================================
   Credibility strip
   ========================================================================== */

.stats { background: var(--ink-sunk); border-bottom: 1px solid var(--line); }

.stats__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}

.stat {
  padding: clamp(2rem, 4vw, 3.25rem) clamp(1rem, 2.5vw, 2rem);
  border-left: 1px solid var(--line);
}
.stat:first-child { border-left: 0; padding-left: 0; }

.stat__value {
  display: block;
  font-family: var(--font-mono);
  font-size: clamp(1.75rem, 1.1rem + 2.4vw, 3rem);
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--s-3);
}

/* The fourth figure is a word, not a number. At the numeral size it wraps. */
.stat__value--text { font-size: clamp(1.3rem, 1rem + 1.15vw, 1.9rem); }

.stat__label {
  display: block;
  font-size: var(--fs-small);
  color: var(--text-dim);
  line-height: 1.4;
}

@media (max-width: 780px) {
  .stats__grid { grid-template-columns: repeat(2, 1fr); }
  .stat { padding-inline: var(--s-4); }
  .stat:first-child { padding-left: var(--s-4); }
  .stat:nth-child(odd) { border-left: 0; padding-left: 0; }
  .stat:nth-child(n+3) { border-top: 1px solid var(--line); }
}

/* ==========================================================================
   Ruled lattices

   Shared by the services quadrants and the testimonials grid. Both are
   hairline lattices, and both now draw those hairlines in before their
   content arrives, so the structure reads as ruled rather than faded up.

   A border-width cannot be animated on the compositor — every frame would be
   a layout — so the visible hairlines are absolutely positioned spans that a
   transform can stretch. The borders they replace are still declared and
   still 1px, now transparent. That is deliberate and it is the whole trick:
   the borders keep holding the geometry, so no cell moves by a pixel and no
   grid track changes width, while the spans carry the colour and take the
   scale. Delete a transparent border and the lattice shifts.

   The spans are absolutely positioned, so inside a grid container they are
   out of flow and never become grid items, and inside a flex cell they are
   never counted by gap.

   Offsets: an absolutely positioned child is placed against its parent's
   padding box, so a line that has to sit on top of a 1px border is pushed
   1px outside it — top: -1px, bottom: -1px, right: -1px. Corners are covered
   by whichever line owns them; nothing needs to overhang to meet.

   Start state (scale 0) lives at the bottom of this file behind .js and
   inside the reduced-motion query, exactly like every other entrance here.
   With JS off, or motion off, every line is at full length from first paint.
   ========================================================================== */

.ruling {
  position: absolute;
  background: var(--line);
  pointer-events: none;
}

/* Horizontals are ruled left to right, verticals top to bottom. */
.ruling--top,
.ruling--base { left: 0; right: 0; height: 1px; transform-origin: left center; }

.ruling--stem,
.ruling--edge { top: 0; bottom: 0; width: 1px; transform-origin: center top; }

.ruling--top  { top: -1px; }
.ruling--base { bottom: -1px; }
.ruling--edge { right: -1px; bottom: -1px; }

/* Both lattices are panels on a 1px gap now, so the drawn rules and the marker
   squares are retired. The spans stay in the markup because the lattice
   timeline in main.js targets them; hidden, they cost nothing. */
.quadrants .ruling,
.quotes .ruling { display: none; }

/* Panel decoration, shared by the services and the testimonials so the two
   read as one build. Pseudo-elements only: nothing to read, nothing to tab to.

   ::before is the accent, a short red bar on the panel's top edge. The
   reticle in the bottom-right corner, and the sheen on ::after, live in
   ghost.css. */
.quadrant::before,
.quote::before {
  content: '';
  position: absolute;
  top: 0;
  left: var(--pad);
  z-index: 2;
  width: 2.25rem;
  height: 2px;
  background: var(--red);
}

/* ==========================================================================
   Services. Four panels on a hairline gap, not cards. Four verticals, no
   numbering: they are not a sequence.
   ========================================================================== */

/* A faint red falloff off the top-left, the way the hero's glow sits behind
   the mark, over a ground that rises from --ink-sunk to --ink. */
#services {
  position: relative;
  overflow: clip;
  background:
    radial-gradient(60% 42% at 0% 0%, rgba(214, 32, 43, 0.075), transparent 70%),
    linear-gradient(to bottom, var(--ink-sunk), var(--ink) 18rem);
}

/* Headline left, lead right, on a shared baseline, under a short red bar. */
.services__head {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  align-items: end;
  column-gap: clamp(2rem, 6vw, 6rem);
  row-gap: var(--s-5);
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
}
.services__head::before,
.quotes__head::before {
  content: '';
  grid-column: 1 / -1;
  width: 3rem;
  height: 2px;
  background: var(--red);
}
.services__head .lead { max-width: 46ch; justify-self: end; padding-bottom: 0.35em; }
@media (max-width: 900px) {
  .services__head { grid-template-columns: minmax(0, 1fr); }
  .services__head .lead { justify-self: start; padding-bottom: 0; }
}

/* The gap shows the container's colour, which is what draws the hairlines. */
.quadrants {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1px;
  border: 1px solid var(--line-strong);
  background: var(--line-strong);
}

.quadrant {
  --pad: clamp(1.75rem, 3.2vw, 2.9rem);
  position: relative;
  overflow: hidden;
  isolation: isolate;
  padding: calc(var(--pad) + 0.9rem) var(--pad) var(--pad);
  background:
    linear-gradient(160deg, rgba(242, 242, 240, 0.045) 0%, rgba(242, 242, 240, 0) 42%),
    var(--ink);
  transition: background-color var(--t-mid) ease;
}
/* Above the ghost. The ruling spans are display: none, so :not() is only a
   guard for the day one is shown again. */
.quadrant > *:not(.ruling) { position: relative; z-index: 1; }

/* --- Quadrant hover ------------------------------------------------------
   The panel cannot be translated as a whole: it sits in a hairline grid and
   moving it tears the edges away from its neighbours. So the title rises a
   couple of pixels and the panel steps up one shade.

   The transform lives inside the no-preference query. With reduced motion on,
   the colours still answer the pointer; nothing moves. :focus-within brings
   the same feedback to a keyboard user tabbing onto the title link.

   Hover additionally requires a real pointer. A tap on a touch screen fires
   :hover and would leave the title lifted, stuck until something else is
   tapped. The colour change stays ungated on purpose: a stuck tint is
   harmless, a stuck displacement is not.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: no-preference) {
  /* Two classes, not one: the plain .quadrant__link rule further down carries
     a colour-only transition and would otherwise win on source order, making
     the lift snap. */
  .quadrant .quadrant__link { transition: color var(--t-mid) ease, transform var(--t-mid) var(--ease-out); }
  .quadrant:focus-within .quadrant__link { transform: translateY(-2px); }
}
@media (hover: hover) and (pointer: fine) {
  .quadrant:hover { background-color: var(--ink-raised); }
}
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .quadrant:hover .quadrant__link { transform: translateY(-2px); }
}

.quadrant__title {
  font-size: clamp(1.4rem, 1.1rem + 1.15vw, 1.95rem);
  font-weight: 600;
  font-variation-settings: 'wdth' 106;
  line-height: 1.15;
  letter-spacing: -0.022em;
  text-wrap: balance;
  max-width: 18ch;
  margin-bottom: var(--s-4);
}

/* A hairline arrow after the title says this goes somewhere. The empty
   alternative keeps it out of the accessible name. */
.quadrant__link::after {
  content: '\2192' / '';
  display: inline-block;
  margin-left: 0.5em;
  font-weight: 400;
  color: var(--text-faint);
  transition: color var(--t-mid) ease;
}
.quadrant:hover .quadrant__link::after,
.quadrant__link:focus-visible::after { color: var(--red-text); }

/* Three lines of prose, not a list of eight to ten items. Set at body size:
   with the list gone this paragraph carries the cell, and it has to outrank
   the terms below it. */
.quadrant__desc {
  color: var(--text-dim);
  font-size: var(--fs-body);
  line-height: 1.65;
  max-width: 50ch;
  margin-bottom: clamp(1.5rem, 2.4vw, 2.1rem);
}

/* An inline link in body copy has to read as a link without hovering. Same
   treatment as the legal pages: underlined, --red-text for 4.5:1 on hover. */
.quadrant__desc a {
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
  transition: color var(--t-fast) ease;
}
.quadrant__desc a:hover { color: var(--red-text); }

/* The searchable terms, kept. They are why this section can rank: the terms a
   client actually types are the specific ones, and the homepage is the page
   that ranks. Cutting them to the service pages alone would have moved every
   one of them a click away from the crawler's first stop.

   Body face at small size, in full text colour so they read as terms rather
   than fine print, under a hairline, each led by the section's red square.
   A <ul> rather than a bare sentence, so the list keeps its semantics and a
   screen reader still announces the count. */
.quadrant__terms {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem var(--s-5);
  padding-top: var(--s-4);
  border-top: 1px solid var(--line);
  font-size: var(--fs-small);
  line-height: 1.6;
  color: var(--text);
}
.quadrant__terms li {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
}
.quadrant__terms li::before {
  content: '';
  flex: none;
  width: 4px;
  height: 4px;
  background: var(--red);
}

@media (max-width: 820px) {
  .quadrants { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Approach. This one IS a sequence, so it is numbered.
   ========================================================================== */

.approach { background: var(--ink-sunk); border-block: 1px solid var(--line); }

.approach__head { margin-bottom: clamp(2.5rem, 5vw, 4rem); max-width: 60ch; }

.approach__rail { position: relative; }

.approach__track {
  position: absolute;
  left: 0;
  right: 0;
  top: 5px;
  height: 1px;
  background: var(--line);
}

.approach__progress {
  position: absolute;
  left: 0;
  right: 0;
  top: 5px;
  height: 1px;
  background: var(--red);
  /* Base state is drawn. The zero-scale start lives in the .js motion block at
     the foot of this file, same as every other animated element, so the rail
     is never blank with JS off or reduced motion on. */
  transform-origin: left center;
}

.approach__steps {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(1.5rem, 3vw, 2.5rem);
}

.step { position: relative; padding-top: var(--s-6); }

.step::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 11px;
  height: 11px;
  background: var(--ink-sunk);
  border: 1px solid var(--red);
}

.step__num {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: 0.16em;
  color: var(--red-text);
  margin-bottom: var(--s-3);
}

.step__title {
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.015em;
  margin-bottom: var(--s-3);
}

.step__body { font-size: var(--fs-small); color: var(--text-dim); }

@media (max-width: 860px) {
  .approach__steps { grid-template-columns: 1fr; gap: var(--s-6); }
  /* left: 5px lines the rail up with the centre of the 11px step markers. */
  .approach__track,
  .approach__progress { left: 5px; right: auto; top: 5px; bottom: 5px; width: 1px; height: auto; }
  .approach__progress { transform-origin: center top; }
  .step { padding-top: 0; padding-left: var(--s-6); }
  .step::before { top: 6px; }
}

/* ==========================================================================
   Statement. Real, attributed quote from the founder.
   ========================================================================== */

.statement { padding-block: clamp(4rem, 9vw, 8rem); }

.statement__quote {
  font-size: clamp(1.5rem, 0.9rem + 2.3vw, 2.75rem);
  font-weight: 500;
  font-variation-settings: 'wdth' 104;
  line-height: 1.22;
  letter-spacing: -0.025em;
  max-width: 22ch;
  text-wrap: balance;
}

.statement__mark { color: var(--red); }

.statement__by {
  margin-top: var(--s-6);
  font-size: var(--fs-small);
  color: var(--text-dim);
}
.statement__by strong { color: var(--text-dim); font-weight: 500; }

/* ==========================================================================
   Contact. The enquiry path. Nothing in here is animated, ever.
   ========================================================================== */

.contact { border-top: 1px solid var(--line); background: var(--ink-sunk); }

.contact__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(2.5rem, 6vw, 5rem);
}

.contact__title { margin-bottom: var(--s-5); }
.contact__lead { margin-bottom: var(--s-7); }

.channels { display: grid; gap: 0; border-top: 1px solid var(--line); }

.channel {
  display: flex;
  align-items: baseline;
  gap: var(--s-4);
  padding-block: var(--s-4);
  border-bottom: 1px solid var(--line);
  transition: color var(--t-fast) ease;
}
.channel:hover { color: var(--red-text); }

.channel__label {
  flex: none;
  width: 86px;
  font-size: var(--fs-small);
  color: var(--text-dim);
}

.channel__value {
  font-size: var(--fs-lead);
  font-weight: 500;
  letter-spacing: -0.01em;
  word-break: break-word;
}

.channel__value--sm { font-size: var(--fs-body); font-weight: 400; line-height: 1.5; }

/* --- Form ---------------------------------------------------------------- */

/* "Send on WhatsApp" is shown only once something can actually send it.
   main.js adds .js-form to <html> at the moment it attaches the submit
   handler; until then only "Send by email" shows, which posts to
   /enquiry.php and needs no JavaScript. This covers scripting being off,
   main.js failing, and main.js still loading.

   The polarity is deliberate: the safe state is the default. A button that
   cannot send costs the visitor the enquiry, and they would not know. */
html:not(.js-form) .form__wa { display: none; }

.form { display: grid; gap: var(--s-5); }

.form__row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-5);
}

.field { display: grid; gap: var(--s-2); }

.field__label {
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--text-dim);
}

.field__req { color: var(--red-text); }

.field__input,
.field__select,
.field__textarea {
  width: 100%;
  min-height: 48px;
  padding: var(--s-3) var(--s-4);
  background: var(--ink);
  border: 1px solid var(--line-strong);
  color: var(--text);
  font-size: var(--fs-body);
  transition: border-color var(--t-fast) ease;
}

.field__input::placeholder,
.field__textarea::placeholder { color: var(--text-faint); }

.field__input:hover,
.field__select:hover,
.field__textarea:hover { border-color: var(--text-faint); }

.field__input:focus,
.field__select:focus,
.field__textarea:focus { border-color: var(--red); outline-offset: 1px; }

.field__textarea { min-height: 130px; resize: vertical; line-height: 1.6; }

.field__select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-dim) 50%),
                    linear-gradient(135deg, var(--text-dim) 50%, transparent 50%);
  background-position: calc(100% - 20px) 50%, calc(100% - 14px) 50%;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-right: var(--s-8);
}
.field__select option { background: var(--ink); color: var(--text); }

.field__hint { font-size: var(--fs-micro); color: var(--text-faint); line-height: 1.5; }

/* Honeypot. Off-screen rather than display:none so more bots fill it. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.consent {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-3);
  align-items: start;
  font-size: var(--fs-small);
  color: var(--text-dim);
}

.consent input[type='checkbox'] {
  width: 20px;
  height: 20px;
  margin: 2px 0 0;
  accent-color: var(--red);
  flex: none;
}

.consent a { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.consent a:hover { color: var(--red-text); }

.form__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); }

/* Injected by JS after submission. Never present in the markup at rest. */
.form__status {
  padding: var(--s-4);
  border-left: 2px solid var(--red);
  background: var(--ink-raised);
  font-size: var(--fs-small);
  line-height: 1.55;
}
/* Success and failure must be distinguishable before the text is read. */
.form__status--ok  { border-left-color: var(--text); }
.form__status--err { border-left-color: var(--red-text); }

@media (max-width: 940px) {
  .contact__grid { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
  .form__row { grid-template-columns: 1fr; }
}

/* ==========================================================================
   WhatsApp float
   ========================================================================== */

/* Size and inset live in variables so the page padding that reserves room
   for the float can be derived from them rather than guessed. */
:root {
  --wa-size:  52px;                     /* well above the 44px tap minimum */
  --wa-inset: clamp(1rem, 3vw, 2rem);
  --bar-h:    56px;
}

/* Wider than a phone: the WhatsApp button alone, bottom right. Dark with a red
   icon rather than a solid red block, so the hero's Call button stays the one
   solid red element on screen. The Call half of the bar exists for phones. */
.float-bar__call { display: none; }

.wa-float {
  position: fixed;
  right: var(--wa-inset);
  bottom: var(--wa-inset);
  z-index: var(--z-float);
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  min-height: var(--wa-size);
  padding: var(--s-3) var(--s-5);
  background: var(--ink-raised);
  color: var(--text);
  border: 1px solid var(--line-strong);
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  font-weight: 500;
  letter-spacing: 0.04em;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
  transition: border-color var(--t-fast) ease;
}
.wa-float:hover { border-color: var(--text); }
.wa-float svg { width: 20px; height: 20px; flex: none; color: var(--red); }

/* Hidden while the contact section is on screen: it carries the same phone
   and WhatsApp routes, and a second WhatsApp button beside the form's own
   reads as a mistake. main.js sets the class; without JS it is never set and
   the float simply stays. Instant, never a transition. */
.contact-in-view .float-bar { display: none; }

/* --- Phones: a bottom bar, Call beside WhatsApp ---------------------------
   A corner disc sat over the right edge of the text all the way down the
   page. A full-width bar keeps the text column clear: content scrolls above
   it, and the footer reserves the bar's height so nothing comes to rest
   underneath. Nothing here hides, delays or animates either link.
   ------------------------------------------------------------------------ */
@media (max-width: 560px) {
  .float-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: var(--z-float);
    display: grid;
    grid-template-columns: 1fr 1fr;
    padding-bottom: env(safe-area-inset-bottom);
    background: var(--ink-raised);
    border-top: 1px solid var(--line-strong);
  }

  .float-bar__call,
  .float-bar .wa-float {
    position: static;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-3);
    min-height: var(--bar-h);
    padding: 0 var(--s-4);
    background: none;
    border: 0;
    box-shadow: none;
    color: var(--text);
    font-family: var(--font-mono);
    font-size: var(--fs-small);
    font-weight: 500;
    letter-spacing: 0.04em;
  }
  .float-bar .wa-float { border-left: 1px solid var(--line-strong); }
  .float-bar__call svg { width: 20px; height: 20px; flex: none; color: var(--red); }

  .footer {
    padding-bottom: calc(var(--s-9) + var(--bar-h) + env(safe-area-inset-bottom));
  }
}

/* ==========================================================================
   Footer
   ========================================================================== */

/* Extra bottom padding keeps the WhatsApp float from sitting over the last
   line of the footer. */
.footer { border-top: 1px solid var(--line); padding-block: var(--s-8) var(--s-9); }

.footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) repeat(2, minmax(0, 1fr));
  gap: clamp(2rem, 5vw, 4rem);
  padding-bottom: var(--s-8);
}

/* The client's slogan, once per page, as the wordmark's own line. Eyebrow
   type so it reads as brand, not copy. Middots are aria-hidden in the markup. */
.footer__slogan {
  margin-top: var(--s-3);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.footer__blurb {
  margin-top: var(--s-5);
  font-size: var(--fs-small);
  color: var(--text-dim);
  max-width: 34ch;
}

.footer__h {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--text-dim);
  margin-bottom: var(--s-4);
}

.footer__list { display: grid; gap: var(--s-3); font-size: var(--fs-small); }
.footer__list a:hover { color: var(--red-text); }

.footer__addr { font-style: normal; color: var(--text-dim); font-size: var(--fs-small); line-height: 1.6; }

.footer__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4) var(--s-6);
  align-items: center;
  justify-content: space-between;
  padding-top: var(--s-5);
  border-top: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.08em;
  color: var(--text-faint);
}

.footer__legal { display: flex; gap: var(--s-5); flex-wrap: wrap; }
.footer__legal a:hover { color: var(--text); }

@media (max-width: 780px) {
  .footer__grid { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Motion states.

   Hidden start states are applied ONLY when (a) JS has run and (b) the user
   has not asked for reduced motion. With JS off, or with reduced motion on,
   every element below renders at its final state. Nothing on the page
   depends on JS to become visible.

   The enquiry path (.contact, .wa-float, .nav__cta, .hero__actions) is
   deliberately absent from this block. It must never be gated by motion.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {
  /* 28px and a hair of scale, not 18px flat. The old value was small enough
     that the reveal read as a rendering delay rather than as an entrance.
     Still transform and opacity only, and still nothing the page depends on. */
  /* No scale: 0.985 was roughly five pixels on a 350px card spread over
     three quarters of a second — below the threshold at which anyone sees it,
     while still costing a composited property.

     No will-change either. It used to sit here, which promoted every
     [data-reveal] on the page to its own compositor layer from first paint
     and held it there until the reader scrolled that far — two dozen layers
     on a page most of which is below the fold. main.js now sets it in the
     trigger's onEnter and clears it onComplete, so a layer exists only while
     something is actually moving. */
  .js [data-reveal] {
    opacity: 0;
    transform: translateY(28px);
  }

  /* --- The wipe -----------------------------------------------------------
     data-reveal="wipe" takes a block out of the fade and hands it to a
     clip-path instead: the text is uncovered from its left edge rather than
     faded up. The distinction is worth the extra rule. A fade reads as the
     text being generated; a wipe reads as the text having been there all
     along and something moving off it, which is the right claim for a firm
     whose product is a finding rather than a composition.

     No opacity and no displacement — the wipe carries it alone, so this
     overrides both properties the rule above sets. Same specificity as
     [data-reveal], so it has to stay after it.

     main.js sets clip-path to none once the wipe finishes. It has to: a
     clip-path clips descendants at the box edge even at inset 0, which would
     cut the focus ring off any link inside a wiped block.
     ---------------------------------------------------------------------- */
  .js [data-reveal="wipe"] {
    opacity: 1;
    transform: none;
    clip-path: inset(0% 100% 0% 0%);
  }

  /* --- Ruled lattices -----------------------------------------------------
     Every hairline starts at zero length. The two lattice timelines in
     main.js rule them in, then uncover the cell contents behind them.

     The content selectors exclude .ruling: the lines are siblings of the
     content, not children of it, because a clip-path on a cell would clip
     the cell's own hairlines and there would be nothing to draw.
     ---------------------------------------------------------------------- */
  .js .ruling--top,
  .js .ruling--base { transform: scaleX(0); }

  .js .ruling--stem,
  .js .ruling--edge { transform: scaleY(0); }

  .js .quadrant > *:not(.ruling),
  .js .quote > *:not(.ruling) { clip-path: inset(0% 100% 0% 0%); }

  /* The reticle is aria-hidden decoration, so hiding it until GSAP arrives
     costs nothing semantically. */
  .js .reticle__assembly { opacity: 0; }

  /* --- Hero left column ---------------------------------------------------
     THE H1 AND THE LEAD ARE ABSENT FROM THIS BLOCK ON PURPOSE. Do not add
     them back in any form.

     The 30 Aug change put the headline into the load sequence, moving it with
     transform inside an overflow mask on the theory that a transform — unlike
     opacity — keeps an element eligible as an LCP candidate. It does not.
     translateY(105%) inside overflow: hidden puts the text outside its paint
     box, so it paints nothing and is disqualified exactly as opacity 0 is.
     Lighthouse mobile then named .hero__lead as the LCP element and measured
     2977ms, with 2527ms of that sitting in Render Delay: the largest paint
     was waiting on GSAP. Both are now unhidden and paint on the first frame.

     What remains here is everything that is not an LCP candidate:

       eyebrow   — small, above the headline, never the largest paint.
       actions   — the Call button. Transform only, never opacity: it is the
                   enquiry path and must be clickable from the first frame,
                   which a transform does not prevent and an opacity would.

     The load moment is carried by the reticle, as it was before 30 Aug.
     ---------------------------------------------------------------------- */
  .js .hero .eyebrow { opacity: 0; transform: translateX(-14px); }
  .js .hero__actions { transform: translateY(14px); }

  .js .approach__progress { transform: scaleX(0); }

  /* The four approach steps start hidden here rather than through
     [data-reveal], because they are not a reveal group: the rail's scrubbed
     trigger moves them, so each arrives as the progress line reaches it. The
     guard is the same one that covers the progress line — motion off and the
     rule never applies, JS off and .js is never added, and either way all
     four read at their final state. Same 28px as the reveals elsewhere, so
     the section still matches the page. */
  .js .approach .step {
    opacity: 0;
    transform: translateY(28px);
  }
}

@media (prefers-reduced-motion: no-preference) and (max-width: 860px) {
  .js .approach__progress { transform: scaleY(0); }
}

/* ==========================================================================
   Mobile navigation.

   Added when the four service pages shipped. Until then the header links
   simply hid below 680px, which was acceptable on a one-page site where
   scrolling reached every section. It is not acceptable now that the header
   is the only route to the service pages.

   Native <details>, so it opens and closes with JS off. No library, no
   handler, no focus trap to get wrong.
   ========================================================================== */

.menu { display: none; position: relative; }

.menu__toggle {
  display: inline-flex;
  align-items: center;
  min-height: 42px;
  padding-inline: var(--s-4);
  border: 1px solid var(--line-strong);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  cursor: pointer;
  list-style: none;
  transition: color var(--t-fast) ease, border-color var(--t-fast) ease;
}

/* Safari still needs the vendor pseudo-element to drop the disclosure arrow. */
.menu__toggle::-webkit-details-marker { display: none; }
.menu__toggle::marker { content: ''; }

.menu[open] .menu__toggle { color: var(--text); border-color: var(--text); }

.menu__panel {
  position: absolute;
  right: 0;
  top: calc(100% + var(--s-3));
  z-index: var(--z-nav);
  min-width: 15rem;
  display: grid;
  padding: var(--s-2);
  background: var(--ink-raised);
  border: 1px solid var(--line-strong);
}

.menu__link {
  padding: var(--s-3) var(--s-4);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  transition: color var(--t-fast) ease, background-color var(--t-fast) ease;
}
.menu__link:hover { color: var(--text); background: var(--ink); }
.menu__link[aria-current='page'] { color: var(--red-text); }

/* The two non-service entries, set below a hairline so the four verticals
   read as one group rather than six equal links. */
.menu__link--sub { color: var(--text-faint); }
.menu__link--sub:first-of-type { margin-top: var(--s-2); border-top: 1px solid var(--line); padding-top: var(--s-4); }

/* The header CTA is on the enquiry path and is never removed at any width.
   It no longer needs a long/short label pair: "Enquire" fits a 360px viewport
   beside the wordmark and the menu button, which the full phone number did
   not. The number itself is in the mobile menu, the contact section and the
   footer, so nothing was lost by taking it out of the header. */

@media (max-width: 680px) {
  .menu { display: block; }
}

@media (max-width: 560px) {
  .nav__cta { padding-inline: var(--s-4); }
}

/* ==========================================================================
   Service pages

   Shares every token with the homepage. The only new idea here is the .svc
   row: a two-column hairline list, which is the quadrant treatment stretched
   to full width. Still no cards, still radius 0.
   ========================================================================== */

.page-hero {
  padding-top: clamp(2rem, 5vw, 3.5rem);
  padding-bottom: clamp(2.5rem, 5vw, 4rem);
  border-bottom: 1px solid var(--line);
}

.crumb {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
  margin-bottom: var(--s-6);
  font-size: var(--fs-small);
  color: var(--text-dim);
}
.crumb a { color: var(--text-dim); transition: color var(--t-fast) ease; }
.crumb a:hover { color: var(--red-text); }

/* Deliberately smaller than the homepage --fs-display. There is one display
   moment on this site and it belongs to the homepage h1. */
.page-hero__title {
  font-size: var(--fs-h2);
  font-weight: 600;
  font-variation-settings: 'wdth' 108;
  line-height: 1.06;
  letter-spacing: -0.02em;
  text-wrap: balance;
  max-width: 22ch;
  margin-bottom: var(--s-5);
}

.page-hero__lead { max-width: 54ch; }
.page-hero__lead--date { margin-bottom: var(--s-3); }

/* 404 only. Its own class, not .hero__actions: that one carries the
   homepage's load-in start-state. */
.page-hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-7);
}

.intro { padding-bottom: 0; }

.prose {
  display: grid;
  gap: var(--s-5);
  max-width: 62ch;
  color: var(--text-dim);
}

.svc-group + .svc-group { margin-top: clamp(3rem, 6vw, 5rem); }

.svc-group__title {
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--text);
  margin-bottom: var(--s-5);
}

.svc-list { border-top: 1px solid var(--line); }

.svc {
  display: grid;
  grid-template-columns: minmax(0, 0.4fr) minmax(0, 0.6fr);
  gap: clamp(1rem, 3vw, 3.5rem);
  padding-block: clamp(1.75rem, 3vw, 2.5rem);
  border-bottom: 1px solid var(--line);
  transition: background-color var(--t-mid) ease;
}
.svc:hover { background: var(--ink-raised); }
.svc:hover .svc__title { color: var(--red-text); }

.svc__title {
  font-size: var(--fs-h3);
  font-weight: 600;
  font-variation-settings: 'wdth' 106;
  line-height: 1.2;
  letter-spacing: -0.015em;
  text-wrap: balance;
  transition: color var(--t-mid) ease;
}

.svc__body {
  display: grid;
  gap: var(--s-4);
  font-size: var(--fs-small);
  color: var(--text-dim);
  max-width: 62ch;
}

@media (max-width: 820px) {
  .svc { grid-template-columns: 1fr; gap: var(--s-4); }
}

/* Scope corrections and similar. Red rule, never red text: --red fails the
   body-text contrast threshold and this is body copy. */
.note {
  margin-top: clamp(2rem, 4vw, 3rem);
  padding-left: var(--s-5);
  border-left: 2px solid var(--red);
  font-size: var(--fs-small);
  color: var(--text-dim);
  max-width: 62ch;
}

.closing { background: var(--ink-sunk); border-block: 1px solid var(--line); }
.closing__title { margin-bottom: var(--s-6); max-width: 20ch; }

/* --- Other verticals --------------------------------------------------- */

.related-wrap { padding-block: clamp(3rem, 6vw, 5rem); }

.related {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  margin-top: var(--s-5);
}

.related__item {
  display: grid;
  align-content: start;
  gap: var(--s-4);
  padding: clamp(1.5rem, 3vw, 2.25rem) clamp(1.5rem, 3vw, 2.25rem)
           clamp(1.5rem, 3vw, 2.25rem) 0;
  transition: background-color var(--t-mid) ease;
}
.related__item + .related__item {
  border-left: 1px solid var(--line);
  padding-left: clamp(1.5rem, 3vw, 2.25rem);
}
.related__item:hover { background: var(--ink-raised); }

.related__title {
  font-size: var(--fs-h3);
  font-weight: 600;
  font-variation-settings: 'wdth' 106;
  line-height: 1.2;
  letter-spacing: -0.015em;
}

@media (max-width: 820px) {
  .related { grid-template-columns: 1fr; }
  .related__item + .related__item {
    border-left: 0;
    border-top: 1px solid var(--line);
    padding-left: 0;
  }
  .related__item { padding-right: 0; }
}

/* --- About page ----------------------------------------------------------
   The same 0.4 / 0.6 split as the .svc row: photo where a row's title sits,
   the client's profile text where its body sits. The only image on the site,
   so it carries a hairline frame rather than floating on the ink. */

.nav__link[aria-current='page'] { color: var(--text); }

.profile__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.4fr) minmax(0, 0.6fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}

.profile__photo { border: 1px solid var(--line); max-width: 440px; }
.profile__photo img { width: 100%; height: auto; }

/* Between two transparent sections, so it gets the band the homepage
   testimonials got for the same reason: an unmarked seam reads as a void. */
.statement--band { background: var(--ink-sunk); border-block: 1px solid var(--line); }

.awards__title { margin-bottom: clamp(2rem, 4vw, 3rem); }

.awards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(1.75rem, 3vw, 2.75rem);
  border-top: 1px solid var(--line);
}

.award {
  display: grid;
  gap: var(--s-2);
  align-content: start;
  padding-block: clamp(1.25rem, 2.5vw, 1.75rem);
  border-bottom: 1px solid var(--line);
}

.award__name {
  font-size: var(--fs-h3);
  font-weight: 600;
  font-variation-settings: 'wdth' 106;
  line-height: 1.25;
  letter-spacing: -0.015em;
}

.award__meta {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

.related--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

@media (max-width: 980px) {
  .related--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .related--4 .related__item:nth-of-type(3) { border-left: 0; padding-left: 0; }
  .related--4 .related__item:nth-of-type(n+3) { border-top: 1px solid var(--line); }
}

@media (max-width: 820px) {
  .profile__grid { grid-template-columns: 1fr; }
  .profile__photo { max-width: 360px; }
  .awards { grid-template-columns: 1fr; }
  .related--4 { grid-template-columns: 1fr; }
}

/* --- Footer, four columns ----------------------------------------------- */

.footer__grid--4 { grid-template-columns: 1.4fr repeat(3, minmax(0, 1fr)); }

@media (max-width: 980px) {
  .footer__grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .footer__grid--4 { grid-template-columns: 1fr; }
}

/* --- Linked quadrant titles on the homepage ------------------------------ */

.quadrant__link { display: block; transition: color var(--t-mid) ease; }
.quadrant:hover .quadrant__link { color: var(--red-text); }

/* ==========================================================================
   Legal pages

   Reuses the service-page .svc row: clause title left, clause body right.
   Nothing new is invented here except real list markers, which the reset at
   the top of this file removes globally and which a legal document needs.
   ========================================================================== */

/* The rows are not links and must not behave like them. */
.legal .svc:hover { background: none; }
.legal .svc:hover .svc__title { color: inherit; }

/* Inline links in body copy: underlined, because in a policy the reader has
   to be able to see what is a link without hovering. --red-text, not --red:
   this is body-size text and --red fails 4.5:1 on --ink. */
.legal .svc__body a,
.legal .prose a {
  color: var(--red-text);
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
  transition: color var(--t-fast) ease;
}
.legal .svc__body a:hover,
.legal .prose a:hover { color: var(--text); }

.legal-list {
  list-style: disc;
  padding-left: 1.15em;
  display: grid;
  gap: var(--s-3);
}
.legal-list li::marker { color: var(--text-faint); }
.legal-list--plain { list-style: none; padding-left: 0; }

/* Hostnames read as data, not prose. */
.legal-host { font-family: var(--font-mono); font-size: 0.92em; color: var(--text); }

/* The postal address sits inside a list item, so it stays inline. */
.legal-addr { display: inline; font-style: normal; }

/* ==========================================================================
   Client accounts

   A hairline lattice, not cards. Same borders, same radius 0, same restraint
   as the homepage quadrants. Five quotes sit on a six-column track — three,
   then two wider — so the second row reads as intentional rather than as a
   row that ran out of content.

   No hover state: these are not links and must not behave like one.
   ========================================================================== */

/* The band. Every section on this page carries 128px of padding top and
   bottom at 1440, so every seam between two sections is 256px. Everywhere
   else something marks it: .stats, .approach and .contact all sit on
   --ink-sunk, and the last two carry a hairline as well. This was the one
   seam where two transparent sections met, so the statement's bottom padding
   and this section's top padding read as a single unbroken field of black,
   about 300px from the founder's caption to this heading and 447px to the
   first quote. Nothing was wrong with the spacing; nothing was marking it.

   The statement above keeps its transparent ground, which now makes it a
   plain black band between two tinted ones. That is the right treatment for
   a pull-quote from the founder. */
/* A faint red falloff off the top-right, mirroring Services so the two
   sections do not repeat each other. */
.quotes-section {
  position: relative;
  overflow: clip;
  background:
    radial-gradient(60% 42% at 100% 0%, rgba(214, 32, 43, 0.065), transparent 70%),
    var(--ink-sunk);
  border-top: 1px solid var(--line);
}

/* Same head as Services: red bar, headline left, note right on its baseline. */
.quotes__head {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  align-items: end;
  column-gap: clamp(2rem, 6vw, 6rem);
  row-gap: var(--s-5);
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
}
.quotes__head .h2 { margin-bottom: 0; }

.quotes__note {
  font-size: var(--fs-small);
  color: var(--text-faint);
  max-width: 48ch;
  justify-self: end;
  padding-bottom: 0.5em;
}
@media (max-width: 900px) {
  .quotes__head { grid-template-columns: minmax(0, 1fr); }
  .quotes__note { justify-self: start; padding-bottom: 0; }
}

/* The same panels as the services. No hover treatment: a quote is not a link,
   so the panel does not pretend to be one. */
.quotes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  border: 1px solid var(--line-strong);
  background: var(--line-strong);
}

.quote {
  --pad: clamp(1.75rem, 3.2vw, 2.9rem);
  position: relative;
  overflow: hidden;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: var(--s-6);
  padding: calc(var(--pad) + 0.9rem) var(--pad) var(--pad);
  background:
    linear-gradient(160deg, rgba(242, 242, 240, 0.045) 0%, rgba(242, 242, 240, 0) 42%),
    var(--ink);
}
.quote > *:not(.ruling) { position: relative; z-index: 1; }

/* Five quotes on two columns would leave a hole that shows the line colour as
   a solid block. The odd last one takes the whole row instead. */
.quote:last-of-type:nth-of-type(odd) { grid-column: 1 / -1; }
.quote:last-of-type:nth-of-type(odd) .quote__text { max-width: 62ch; }

/* Lead size and full text colour: these are the strongest proof on the page
   and were set as its quietest text. */
.quote__text {
  flex: 1;
  font-size: clamp(1.1rem, 1rem + 0.5vw, 1.32rem);
  line-height: 1.6;
  letter-spacing: -0.005em;
  text-wrap: pretty;
  color: var(--text);
}

/* The quote marks take the text colour. The red in this section is the bar and
   the square, once per quote, not a pair of marks on every one. */
.quote__mark { font-weight: 600; }

/* The name sits under a hairline, led by the services' red square. */
.quote__by {
  padding-top: var(--s-4);
  border-top: 1px solid var(--line);
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--text);
}
.quote__by::before {
  content: '';
  display: inline-block;
  width: 4px;
  height: 4px;
  margin-right: 0.6rem;
  vertical-align: middle;
  background: var(--red);
}

.quote__role {
  font-weight: 400;
  color: var(--text-dim);
}

@media (max-width: 620px) {
  .quotes { grid-template-columns: 1fr; }
}
