/* ==========================================================================
   NORTHLINE WEB CO.
   Design system + page styles. No build step, no framework.

   Structure
     01  Fonts
     02  Tokens
     03  Reset and base
     04  Layout primitives
     05  Typography
     06  Buttons, links, labels
     07  Navigation
     08  Hero
     09  Statement
     10  Services
     11  Bridge (dark to warm transition)
     12  Selected work
     13  Process
     14  Pricing
     15  Estimator
     16  Contact and forms
     17  Footer
     18  Legal pages
     19  Motion
   ========================================================================== */


/* 01  FONTS
   Self hosted. Keeps visitor IP addresses away from third party font CDNs,
   which is what the privacy notice claims. Variable, latin subset only.
   ========================================================================== */

@font-face {
  font-family: 'Bodoni Moda';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('../fonts/bodoni-moda-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: 'Bodoni Moda';
  font-style: italic;
  font-weight: 400 800;
  font-display: swap;
  src: url('../fonts/bodoni-moda-italic-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: 'Schibsted Grotesk';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('../fonts/schibsted-grotesk-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: 'Schibsted Grotesk';
  font-style: italic;
  font-weight: 400 800;
  font-display: swap;
  src: url('../fonts/schibsted-grotesk-italic-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;
}


/* 02  TOKENS
   The page runs two "worlds": a cold dark world (hero, services, contact)
   and a warm editorial world (work, process, pricing, estimator).
   Components read semantic tokens so the same markup works in both.
   ========================================================================== */

:root {
  /* --- palette: cold dark world --- */
  --ink:        #0A0E11;
  --ink-2:      #10161A;
  --ink-3:      #182026;
  --ink-4:      #212B32;
  --mist:       #93A0A8;
  /* lightened so 11-14px faint text clears WCAG AA 4.5:1 on --ink AND on the
     slightly lighter --ink-2 used by the services section */
  --mist-dim:   #7B878E;
  --bone:       #F4F1EA;

  /* --- palette: warm editorial world --- */
  --stone:      #E8E2D6;
  --stone-2:    #DFD7C8;
  --stone-3:    #D3C9B7;
  --bone-lit:   #F4F1E9;
  --ink-warm:   #14120E;
  --ink-warm-2: #4A453C;

  /* --- accent: last light on the ridge. used sparingly. --- */
  --ember:      #C9603C;
  --ember-soft: #E0916F;  /* accent text on dark   7.8:1 */
  --ember-deep: #9E4527;  /* accent text on warm   4.9:1 */

  /* --- semantic tokens: dark world is the default --- */
  --bg:          var(--ink);
  --bg-raised:   var(--ink-2);
  --bg-sunken:   #070A0C;
  --fg:          var(--bone);
  --fg-muted:    var(--mist);
  --fg-faint:    var(--mist-dim);
  --rule:        rgb(244 241 234 / 0.14);
  --rule-strong: rgb(244 241 234 / 0.28);
  --accent-fg:   var(--ember-soft);
  /* focus ring, kept separate from the accent because the two have different
     jobs. WCAG 1.4.11 wants 3:1 against the adjacent surface for a non-text
     indicator; --ember is 4.8:1 on --ink, so the dark world uses it directly.
     The warm world cannot, and overrides this below. */
  --focus:       var(--ember);
  --btn-bg:      var(--bone);
  --btn-fg:      var(--ink);
  --field-bg:    rgb(244 241 234 / 0.04);
  --field-rule:  rgb(244 241 234 / 0.22);

  /* --- spacing --- */
  --sp-1: 0.25rem;
  --sp-2: 0.5rem;
  --sp-3: 0.75rem;
  --sp-4: 1rem;
  --sp-5: 1.5rem;
  --sp-6: 2rem;
  --sp-7: 3rem;
  --sp-8: 4rem;
  --sp-9: 6rem;
  --sp-10: 8rem;

  /* section rhythm: airy. VISUAL_DENSITY 3. */
  --section-y: clamp(5rem, 11vw, 11rem);

  /* --- layout --- */
  --wrap: 1440px;
  --wrap-text: 62ch;
  --gutter: clamp(1.25rem, 5vw, 4.5rem);
  --nav-h: 72px;

  /* --- type --- */
  --font-display: 'Bodoni Moda', 'Didot', 'Times New Roman', serif;
  --font-sans: 'Schibsted Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;

  --fs-display-1: clamp(1.9rem, 9vw, 8.5rem);
  --fs-display-2: clamp(2rem, 5.6vw, 4.75rem);
  --fs-display-3: clamp(1.6rem, 3.4vw, 2.75rem);
  --fs-display-4: clamp(1.35rem, 2.2vw, 1.9rem);
  --fs-lead: clamp(1.0625rem, 1.35vw, 1.3125rem);
  --fs-body: clamp(0.9688rem, 1vw, 1.0625rem);
  --fs-small: 0.875rem;
  --fs-tiny: 0.8125rem;
  --fs-label: 0.6875rem;

  --tracking-label: 0.22em;
  --tracking-wordmark: 0.18em;

  /* --- shape: editorial. everything is square. --- */
  --radius: 0px;

  /* --- shadow: tinted to the surface, never pure black --- */
  --shadow-lift: 0 18px 50px -20px rgb(7 10 12 / 0.7);

  /* --- motion --- */
  --dur-fast: 180ms;
  --dur: 420ms;
  --dur-slow: 900ms;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);

  --z-grain: 90;
  --z-nav: 100;
  --z-menu: 110;
}

/* the warm world. applied per section. */
.world--warm {
  --bg:          var(--stone);
  --bg-raised:   var(--bone-lit);
  --bg-sunken:   var(--stone-2);
  --fg:          var(--ink-warm);
  --fg-muted:    var(--ink-warm-2);
  /* darkened so faint labels clear 4.5:1 on the darkest warm surface
     (--stone-2, used by the estimator) as well as on --stone */
  --fg-faint:    #605950;
  --rule:        rgb(20 18 14 / 0.16);
  --rule-strong: rgb(20 18 14 / 0.34);
  --accent-fg:   var(--ember-deep);
  /* --ember is only 2.81:1 on --stone-2, the estimator surface, and lower again
     on the near white field backgrounds, so the focus ring failed 1.4.11 across
     this entire world. --ember-deep is 4.4:1 on --stone-2 and 5.6:1 on
     --bone-lit, and it is already the accent colour here, so nothing shifts
     visually except that the ring is now actually visible. */
  --focus:       var(--ember-deep);
  --btn-bg:      var(--ink-warm);
  --btn-fg:      var(--stone);
  --field-bg:    rgb(255 255 255 / 0.45);
  --field-rule:  rgb(20 18 14 / 0.26);

  background: var(--bg);
  color: var(--fg);
}


/* 03  RESET AND BASE
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  /* anchor targets clear the fixed nav */
  scroll-padding-top: calc(var(--nav-h) + 1rem);
}
/* Smooth scrolling is switched on by JS after the first paint. Applying it to
   html from the start turns arriving at /#pricing into a multi thousand pixel
   animated scroll past the entire page. */
html.scroll-smooth { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--ink);
  color: var(--bone);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: 1.6;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* clip, not hidden. overflow-x:hidden forces overflow-y to auto, which turns
     <body> into a scroll container and silently kills every position:sticky
     inside it, including the pinned capability stack. clip guards against a
     stray sideways scrollbar without creating a scrollport. */
  overflow-x: clip;
}

/* stop the mobile menu scrolling the page behind it */
body.is-locked { overflow: hidden; }

img, picture, video, svg { display: block; max-width: 100%; }
img { height: auto; }

/* Holding tone. Each frame in the markup carries --tone, the average colour
   of the photograph it is waiting for, measured off the file itself in linear
   light. It sits behind the image, so a picture that has not arrived reads as
   a tonal block in the right key rather than as a hole in the layout, and the
   wipe resolves tone into photograph instead of nothing into photograph.
   No skeleton, no shimmer, no spinner. */
img { background-color: var(--tone, transparent); }

a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.22em; }

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }

h1, h2, h3, h4, p, figure, blockquote, ul, ol, dl, dd { margin: 0; }
ul, ol { padding: 0; list-style: none; }

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}
/* form controls get an inset ring so the outline is never clipped */
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

::selection { background: var(--ember); color: var(--bone); }

.skip-link {
  position: absolute;
  left: var(--sp-4);
  top: -100%;
  z-index: 200;
  padding: 0.75rem 1.25rem;
  background: var(--bone);
  color: var(--ink);
  font-size: var(--fs-small);
  font-weight: 600;
  transition: top var(--dur-fast) var(--ease);
}
.skip-link:focus { top: var(--sp-4); }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Fine film grain over the whole page. Fixed + pointer-events:none so it
   never triggers repaints on scroll. */
.grain {
  position: fixed;
  inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: 0.32;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
}


/* 04  LAYOUT PRIMITIVES
   ========================================================================== */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section {
  position: relative;
  padding-block: var(--section-y);
  background: var(--bg);
  color: var(--fg);
}

/* 12 column editorial grid */
.grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.5vw, 2.25rem);
}

.rule {
  height: 1px;
  border: 0;
  margin: 0;
  background: var(--rule);
}


/* 05  TYPOGRAPHY
   ========================================================================== */

.display {
  font-family: var(--font-display);
  font-weight: 500;
  font-variation-settings: 'opsz' 96;
  letter-spacing: -0.015em;
  line-height: 1.02;
  text-wrap: balance;
  /* Display line-height is tight and several headings end on an italic word
     with a descender (all by email, shape your project). This reserves the
     space those tails need so they never touch the line beneath. */
  padding-block-end: 0.11em;
}

/* space between a heading and the copy that follows it */
.display + .lead,
.display + .body-text,
.display + .project__meta { margin-top: clamp(1rem, 2vw, 1.5rem); }
.label + .display { margin-top: var(--sp-4); }
.display em, .display i {
  font-style: italic;
  font-weight: 400;
  letter-spacing: -0.005em;
}

.d1 { font-size: var(--fs-display-1); line-height: 0.94; letter-spacing: -0.028em; }
.d2 { font-size: var(--fs-display-2); line-height: 1.05; letter-spacing: -0.02em; }
.d3 { font-size: var(--fs-display-3); line-height: 1.08; letter-spacing: -0.015em; }
.d4 { font-size: var(--fs-display-4); line-height: 1.15; letter-spacing: -0.01em; }

.lead {
  font-size: var(--fs-lead);
  line-height: 1.55;
  color: var(--fg-muted);
  max-width: 46ch;
}

.body-text { max-width: var(--wrap-text); color: var(--fg-muted); }
.body-text p + p { margin-top: 1.1em; }

/* small spaced label. used sparingly, never as decoration on every section. */
.label {
  display: block;
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fg-faint);
}
.label--accent { color: var(--accent-fg); }

/* editorial figure numbers, tabular so columns line up */
.figure-num {
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 40;
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: -0.02em;
}


/* 06  BUTTONS, LINKS, LABELS
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6em;
  min-height: 48px;
  padding: 0.85rem 1.6rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-size: var(--fs-small);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  white-space: nowrap;
  transition:
    background-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}
.btn:active { transform: translateY(1px); }

.btn--primary {
  background: var(--btn-bg);
  color: var(--btn-fg);
  border-color: var(--btn-bg);
}
.btn--primary:hover { background: var(--ember); border-color: var(--ember); color: var(--bone); }

.btn--ghost {
  background: transparent;
  color: var(--fg);
  border-color: var(--rule-strong);
}
.btn--ghost:hover { border-color: var(--fg); background: rgb(244 241 234 / 0.06); }
.world--warm .btn--ghost:hover { background: rgb(20 18 14 / 0.06); }

.btn[disabled], .btn[aria-disabled='true'] {
  opacity: 0.55;
  cursor: not-allowed;
  transform: none;
}

/* underline that draws in from the left on hover */
.link-line {
  position: relative;
  display: inline-block;
  text-decoration: none;
  padding-block-end: 2px;
  font-weight: 500;
}
.link-line::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(var(--line-scale, 1));
  transform-origin: left;
  transition: transform var(--dur) var(--ease);
}
.link-line:hover::after { --line-scale: 0; transform-origin: right; }

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  text-decoration: none;
  font-size: var(--fs-small);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.link-arrow .arrow {
  display: inline-block;
  transition: transform var(--dur) var(--ease);
}
.link-arrow:hover .arrow { transform: translateX(0.3em); }


/* 07  NAVIGATION
   ========================================================================== */

.site-nav {
  position: fixed;
  inset-inline: 0;
  top: 0;
  z-index: var(--z-nav);
  height: var(--nav-h);
  display: flex;
  align-items: center;
  color: var(--bone);
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              backdrop-filter var(--dur) var(--ease);
  border-bottom: 1px solid transparent;
}
/* solid state after scrolling past the hero, and on legal pages */
.site-nav.is-solid,
.site-nav--solid {
  background: rgb(10 14 17 / 0.86);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border-bottom-color: rgb(244 241 234 / 0.12);
}

.site-nav__inner {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
}

/* wordmark */
.wordmark {
  display: inline-flex;
  align-items: baseline;
  gap: 0.6rem;
  text-decoration: none;
  color: inherit;
}
.wordmark__name {
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: var(--tracking-wordmark);
  text-transform: uppercase;
}
.wordmark__co {
  font-size: 0.5625rem;
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  opacity: 0.62;
  padding-left: 0.6rem;
  border-left: 1px solid currentColor;
  align-self: center;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: clamp(1.1rem, 2.4vw, 2.4rem);
}
.nav-links a {
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-decoration: none;
  opacity: 0.82;
  transition: opacity var(--dur-fast) var(--ease);
}
.nav-links a:hover { opacity: 1; }
.nav-links a[aria-current='page'] {
  opacity: 1;
  border-bottom: 1px solid var(--ember);
  padding-bottom: 2px;
}

.nav-actions { display: flex; align-items: center; gap: var(--sp-4); }
.nav-cta {
  min-height: 40px;
  padding: 0.5rem 1.1rem;
  font-size: var(--fs-tiny);
  border: 1px solid rgb(244 241 234 / 0.34);
  background: transparent;
  color: var(--bone);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.nav-cta:hover { background: var(--bone); color: var(--ink); border-color: var(--bone); }

/* hamburger: two rules that cross into an X */
.nav-toggle {
  display: none;
  width: 44px; height: 44px;
  margin-right: -10px;
  background: none;
  border: 0;
  position: relative;
}
.nav-toggle span {
  position: absolute;
  left: 11px;
  width: 22px; height: 1.5px;
  background: currentColor;
  transition: transform var(--dur) var(--ease), opacity var(--dur-fast) var(--ease);
}
.nav-toggle span:nth-child(1) { top: 18px; }
.nav-toggle span:nth-child(2) { top: 25px; }
.nav-toggle[aria-expanded='true'] span:nth-child(1) { transform: translateY(3.5px) rotate(45deg); }
.nav-toggle[aria-expanded='true'] span:nth-child(2) { transform: translateY(-3.5px) rotate(-45deg); }

/* mobile panel */
.nav-panel {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  display: grid;
  grid-template-rows: var(--nav-h) 1fr;
  background: var(--ink);
  visibility: hidden;
  opacity: 0;
  /* visibility flips instantly on open so focus can move into the panel in
     the same tick, and is delayed on close so the fade out is still seen.
     Transitioning visibility itself would leave the panel unfocusable while
     it animated in. */
  transition: opacity var(--dur) var(--ease), visibility 0s linear var(--dur);
}
.nav-panel.is-open {
  visibility: visible;
  opacity: 1;
  transition: opacity var(--dur) var(--ease), visibility 0s linear 0s;
}
.nav-panel__body {
  padding: var(--sp-6) var(--gutter) var(--sp-8);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  overflow-y: auto;
}
.nav-panel__links { display: flex; flex-direction: column; }
.nav-panel__links a {
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 60;
  font-size: clamp(2rem, 11vw, 3.25rem);
  line-height: 1.18;
  letter-spacing: -0.02em;
  text-decoration: none;
  padding-block: 0.28em;
  border-bottom: 1px solid var(--rule);
  /* staggered entry, driven by --i set in markup */
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
  transition-delay: calc(var(--i, 0) * 55ms);
}
.nav-panel.is-open .nav-panel__links a { opacity: 1; transform: none; }
.nav-panel__foot { margin-top: var(--sp-8); }
.nav-panel__foot a { font-size: var(--fs-lead); text-decoration: none; }


/* 08  HERO
   ========================================================================== */

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  grid-template-rows: 1fr auto;
  align-items: end;
  padding-top: calc(var(--nav-h) + var(--sp-6));
  padding-bottom: var(--sp-7);
  overflow: hidden;
  background: var(--ink);
}
.hero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 42%;
}
/* atmospheric overlay: darkens the lower half so type stays legible without
   flattening the photograph */
.hero__media::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to bottom,
      rgb(10 14 17 / 0.62) 0%,
      rgb(10 14 17 / 0.22) 26%,
      rgb(10 14 17 / 0.44) 58%,
      rgb(10 14 17 / 0.88) 88%,
      var(--ink) 100%),
    radial-gradient(120% 80% at 50% 30%, transparent 40%, rgb(7 10 12 / 0.5) 100%);
}

.hero__inner {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
  align-self: end;
}

.hero__headline {
  margin: 0;
  color: var(--bone);
  /* the three .hl-line spans carry the intended breaks, this is only a
     guard so nothing runs the full width of a very wide display */
  max-width: 21ch;
}
.hero__headline .hl-line { display: block; }
.hero__headline em {
  font-style: italic;
  text-transform: none;
  font-weight: 400;
}

.hero__sub {
  margin-top: clamp(1.25rem, 2.4vw, 2rem);
  max-width: 44ch;
  font-size: var(--fs-lead);
  line-height: 1.5;
  color: rgb(244 241 234 / 0.78);
}

.hero__actions {
  margin-top: clamp(1.75rem, 3vw, 2.75rem);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
}

/* hero foot: thin rule + scroll cue, borrowed rhythm from the references
   without copying their compositions */
.hero__foot {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
  margin-top: clamp(2.5rem, 6vw, 5rem);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
  border-top: 1px solid rgb(244 241 234 / 0.16);
  padding-top: var(--sp-4);
}
.hero__meta {
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: rgb(244 241 234 / 0.55);
}
/* subtle scroll cue: a hairline with a travelling highlight */
.scroll-cue {
  position: relative;
  width: 1px;
  height: 48px;
  background: rgb(244 241 234 / 0.22);
  overflow: hidden;
  flex: none;
}
.scroll-cue::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: -60%;
  height: 60%;
  background: linear-gradient(to bottom, transparent, var(--bone));
  animation: cue 2.6s var(--ease-inout) infinite;
}
@keyframes cue {
  0%   { transform: translateY(0); opacity: 0; }
  35%  { opacity: 1; }
  100% { transform: translateY(280%); opacity: 0; }
}


/* 09  STATEMENT
   ========================================================================== */

.statement { background: var(--ink); }
.statement__grid { align-items: start; }
.statement__lead { grid-column: 1 / -1; }
.statement__figure { grid-column: 1 / -1; margin-top: var(--sp-7); }
.statement__figure img { width: 100%; }
.statement__aside { grid-column: 1 / -1; margin-top: var(--sp-6); }

.statement__title { color: var(--bone); margin-top: var(--sp-5); }
.statement__title em { color: var(--ember-soft); }

/* small facts, set as a definition list rather than cards */
.facts { display: grid; gap: 0; margin-top: var(--sp-5); }
.facts div {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-block: 0.85rem;
  border-top: 1px solid var(--rule);
  font-size: var(--fs-small);
}
.facts div:last-child { border-bottom: 1px solid var(--rule); }
.facts dt { color: var(--fg-faint); }
.facts dd { margin: 0; text-align: right; color: var(--fg); }

@media (min-width: 900px) {
  .statement__lead { grid-column: 1 / span 5; position: sticky; top: calc(var(--nav-h) + 3rem); }
  .statement__figure { grid-column: 7 / -1; margin-top: 0; }
  .statement__aside { grid-column: 7 / -1; margin-top: var(--sp-6); }
}


/* 10  SERVICES
   ========================================================================== */

.services { background: var(--ink-2); }

.services__head { margin-bottom: clamp(2.5rem, 5vw, 4.5rem); }
.services__head .d2 { max-width: 20ch; }

/* asymmetric: a tall image column, a list of capabilities, a wide image */
.services__layout { display: grid; gap: clamp(1.5rem, 3vw, 3rem); }

.svc-figure { position: relative; overflow: hidden; }
.svc-figure img {
  width: 100%;
  transition: transform 1.2s var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .svc-figure:hover img { transform: scale(1.035); }
}
.svc-figure figcaption {
  margin-top: var(--sp-3);
  font-size: var(--fs-tiny);
  color: var(--fg-faint);
}

/* capability rows: hairlines and space, not cards.
   This is also the no-JS presentation of the pinned stack below. */
.capabilities { border-top: 1px solid var(--rule); }
.cap {
  display: grid;
  grid-template-columns: 2.5rem 1fr;
  gap: var(--sp-4);
  align-items: baseline;
  padding-block: clamp(1.1rem, 2vw, 1.6rem);
  border-bottom: 1px solid var(--rule);
}
.cap__num {
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  color: var(--fg-faint);
  font-variant-numeric: tabular-nums;
}
.cap__body { display: block; }
.cap__title {
  display: block;
  font-size: clamp(1.0625rem, 1.5vw, 1.25rem);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--fg);
}
.cap__desc { display: block; margin-top: 0.35rem; color: var(--fg-muted); font-size: var(--fs-small); }

.services__note {
  margin-top: var(--sp-8);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--rule);
  font-size: var(--fs-small);
  color: var(--fg-muted);
  max-width: 60ch;
}

.capstack__rail { display: none; }

@media (min-width: 900px) {
  .services__layout {
    grid-template-columns: 5fr 6fr;
    align-items: start;
    gap: clamp(2rem, 5vw, 5rem);
  }
}


/* --- the pinned capability sequence --------------------------------------
   An enhancement layered over the list above, scoped to .js so the content
   never depends on it. The stack locks to the viewport and steps through one
   capability at a time.

   The scroll distance lives in --cap-step and is spent as a runway sitting
   in flow after the pin. It has to be real flow content, not padding: a
   sticky element is confined to its containing block's CONTENT box, so
   padding on the track would give the pin nowhere to travel and it would
   never stick at all. main.js measures the same runway, so the stylesheet
   and the script can never disagree about how long the lock lasts.
   ------------------------------------------------------------------------- */

.js .capstack { --cap-step: 21vh; }
.js .capstack::after {
  content: '';
  display: block;
  height: calc(10 * var(--cap-step));
}

/* --cap-frame is the pinned block's own height, spelled out from the parts
   it is made of so it can be restated here rather than measured. Knowing it
   is what lets the lock sit in the middle of the screen instead of clinging
   to the top of it with two thirds of the viewport empty underneath. */
.js .services__layout {
  --cap-frame: calc(2px + clamp(1.75rem, 5vh, 3rem) + clamp(17.5rem, 34vh, 19.5rem));
  --cap-top: max(
    calc(var(--nav-h) + 1rem),
    calc(var(--nav-h) + (100svh - var(--nav-h) - var(--cap-frame)) / 2)
  );
}

.js .capstack__pin {
  position: sticky;
  top: var(--cap-top);
  min-height: var(--cap-frame);
  display: grid;
  align-content: center;
}

/* The photograph holds its place beside the sequence for the whole run, and
   both columns share one frame so they start and finish together instead of
   leaving the text stranded against a taller image. */
@media (min-width: 900px) {
  .js .capstack { --cap-step: 25vh; }

  .js .services__layout { --cap-frame: clamp(21rem, 56vh, 33rem); }

  .js .services__still {
    position: sticky;
    top: var(--cap-top);
    height: var(--cap-frame);
    display: flex;
    flex-direction: column;
  }
  .js .services__still .svc-figure {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
  }
  .js .services__still .svc-figure img {
    flex: 1 1 auto;
    min-height: 0;
    object-fit: cover;
  }
}

/* progress: ten segments, the current one scrubbed live by scroll position */
.js .capstack__rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 6px;
  max-width: 20rem;
  margin-bottom: clamp(1.75rem, 5vh, 3rem);
}
.js .capstack__seg {
  position: relative;
  height: 2px;
  background: var(--rule);
  overflow: hidden;
}
.js .capstack__seg i {
  position: absolute;
  inset: 0;
  background: var(--fg-faint);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur) var(--ease);
}
.js .capstack__seg.is-done i { transform: scaleX(1); }
.js .capstack__seg.is-current i {
  background: var(--accent-fg);
  transform: scaleX(var(--cap-sub, 0));
  transition: none;   /* scrubbed by scroll, so animating it would lag behind */
}

/* The rows stack in one place. Reserving the height here keeps the pinned
   box a fixed size, so stepping never nudges the layout under the reader.
   Sized against the longest description at the narrowest column it has to
   survive, measured rather than guessed: the tallest row runs to 257px. */
.js .capstack .capabilities {
  position: relative;
  border-top: 0;
  min-height: clamp(17.5rem, 34vh, 19.5rem);
}
/* Top aligned, not centred. The rows differ in height because the
   descriptions differ in length, and centring them would move the numeral
   and the title a little on every step. Anchored to the top they never
   shift, and only the last line of the description varies. */
.js .capstack .cap {
  position: absolute;
  inset: 0 0 auto 0;
  display: block;
  padding-block: 0;
  border-bottom: 0;
  opacity: 0;
  transform: translateY(14px);
  pointer-events: none;
  user-select: none;
}
.js .capstack .cap.is-active {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  user-select: auto;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.js .capstack .cap__num {
  display: block;
  font-size: clamp(2.75rem, 5.5vw, 4.5rem);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--fg-faint);
  margin-bottom: clamp(0.85rem, 2.4vh, 1.4rem);
}
.js .capstack .cap__title {
  font-size: clamp(1.375rem, 2.5vw, 2.125rem);
  line-height: 1.14;
  letter-spacing: -0.018em;
}
.js .capstack .cap__desc {
  margin-top: clamp(0.65rem, 1.8vh, 1.05rem);
  font-size: var(--fs-lead);
  line-height: 1.55;
  max-width: 40ch;
}


/* 11  BRIDGE  (dark to warm transition)
   One deliberate art directed hand off, not a random section flip.
   ========================================================================== */

.bridge {
  position: relative;
  min-height: clamp(340px, 52vh, 620px);
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--ink-2);
  padding-inline: var(--gutter);
  text-align: center;
}
.bridge__media { position: absolute; inset: 0; }
.bridge__media img { width: 100%; height: 100%; object-fit: cover; }
.bridge__media::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    var(--ink-2) 0%,
    rgb(16 22 26 / 0.55) 22%,
    rgb(16 22 26 / 0.5) 55%,
    rgb(232 226 214 / 0.35) 86%,
    var(--stone) 100%);
}
.bridge__text {
  position: relative;
  z-index: 1;
  max-width: 22ch;
  color: var(--bone);
  text-shadow: 0 2px 30px rgb(7 10 12 / 0.55);
}


/* 12  SELECTED WORK
   ========================================================================== */

.work__head { margin-bottom: clamp(2.5rem, 5vw, 4rem); }

/* honest badge. this is information, not decoration. */
/* Replaces .concept-tag, retired 23 August 2026 when the two concept projects
   came off the site. Same slot in .project__meta, but this one is a real link
   to a live client site, so it reads as a link rather than a status badge. */
.project__link {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--accent-fg);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid currentColor;
  transition: opacity var(--dur) var(--ease);
}
.project__link::after {
  content: '↗';
  font-weight: 400;
  transition: transform var(--dur) var(--ease);
}
@media (hover: hover) {
  .project__link:hover { opacity: 0.72; }
  .project__link:hover::after { transform: translate(2px, -2px); }
}


.project { display: grid; gap: clamp(1.5rem, 3vw, 2.5rem); }
.project + .project { margin-top: clamp(4rem, 9vw, 8rem); }

.project__index {
  font-size: var(--fs-display-4);
  color: var(--fg-faint);
  line-height: 1;
}
.project__title { margin-top: var(--sp-3); }
.project__meta {
  margin-top: var(--sp-4);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-5);
  font-size: var(--fs-tiny);
  color: var(--fg-muted);
}
.project__meta span { display: inline-flex; align-items: center; }
.project__body { margin-top: var(--sp-4); max-width: 52ch; color: var(--fg-muted); }
.project__body p + p { margin-top: 0.9em; }
.project__tags {
  margin-top: var(--sp-5);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}
.project__tags li {
  font-size: var(--fs-tiny);
  color: var(--fg-muted);
  border: 1px solid var(--rule);
  padding: 0.3rem 0.65rem;
}

/* --- arrival -------------------------------------------------------------
   The block used to appear as one opacity fade over everything at once,
   which is the gesture every template ships. It now assembles in reading
   order, and the two parts carrying the most weight move rather than fade:
   a rule draws itself across the column, and the name rises from under its
   own edge. The supporting copy only settles, so there is one clear gesture
   to watch instead of six competing ones.

   Nothing new appears on the page. It is the same block, arriving properly.
   ------------------------------------------------------------------------- */

/* the masthead rule that belongs to the project name */
.project__index::after {
  content: '';
  display: block;
  margin-top: var(--sp-4);
  height: 1px;
  background: var(--rule-strong);
}
.js .project__index::after { transform: scaleX(0); transform-origin: left center; }
.js .project.is-in .project__index::after {
  transform: scaleX(1);
  transition: transform 1s var(--ease) 60ms;
}

.js .project .project__index { opacity: 0; transform: translateY(8px); }
.js .project.is-in .project__index {
  opacity: 1; transform: none;
  transition: opacity 600ms var(--ease), transform 600ms var(--ease);
}

/* the name rises from behind its own edge. .display already reserves the
   descender tails in padding, so the clip lands below them, not through. */
.project__title { overflow: hidden; }
.project__title > span { display: block; }
/* 125%, not 105%. The line box is shorter than the heading box once the
   descender padding is counted, so a tighter figure leaves a sliver showing
   above the mask on some faces. */
.js .project .project__title > span { transform: translateY(125%); }
.js .project.is-in .project__title > span {
  transform: none;
  transition: transform 900ms var(--ease) 160ms;
}

.js .project .project__meta,
.js .project .project__body,
.js .project .project__tags li { opacity: 0; transform: translateY(10px); }
.js .project.is-in .project__meta,
.js .project.is-in .project__body,
.js .project.is-in .project__tags li {
  opacity: 1; transform: none;
  transition: opacity 700ms var(--ease), transform 700ms var(--ease);
}
.js .project.is-in .project__meta { transition-delay: 300ms; }
.js .project.is-in .project__body { transition-delay: 370ms; }
.js .project.is-in .project__tags li:nth-child(1) { transition-delay: 440ms; }
.js .project.is-in .project__tags li:nth-child(2) { transition-delay: 475ms; }
.js .project.is-in .project__tags li:nth-child(3) { transition-delay: 510ms; }
.js .project.is-in .project__tags li:nth-child(4) { transition-delay: 545ms; }

/* --- depth ---------------------------------------------------------------
   The photographs sit slightly oversize in their frames and drift against
   the scroll, so the frame reads as an opening rather than a pasted picture.
   The travel is about ten pixels: felt, not watched.

   This replaces a hover scale. Hover was the wrong sense to hang it on. Most
   of these are seen on a phone, where hover does not exist, so the effect
   simply was not there for the people it was meant for.
   ------------------------------------------------------------------------- */
.project__figure { overflow: hidden; }
.project__figure img { width: 100%; }
/* Scoped to .js: the overscale only earns its 3% crop if something is going
   to move inside it. With scripting off the photograph is shown whole. */
.js .project__figure img {
  transform: translate3d(0, var(--par, 0px), 0) scale(1.06);
}
/* The wipe animates the clip. The drift must not be transitioned as well, or
   it arrives a frame behind the scroll and smears. */
.js .reveal-mask.project__figure.is-in img { transition-property: clip-path; }

/* two deliberately different compositions */
@media (min-width: 900px) {
  .project { grid-template-columns: repeat(12, minmax(0, 1fr)); align-items: center; }

  /* 01: wide image left, text right */
  .project--a .project__figure-main { grid-column: 1 / span 7; grid-row: 1; }
  .project--a .project__text { grid-column: 9 / -1; grid-row: 1; }
  .project--a .project__figure-alt {
    grid-column: 5 / span 4;
    grid-row: 2;
    margin-top: calc(var(--sp-7) * -1);
  }

  /* 02: text left, tall image right, detail band beneath */
  .project--b .project__text { grid-column: 1 / span 4; grid-row: 1; }
  .project--b .project__figure-main { grid-column: 6 / -1; grid-row: 1; }
  .project--b .project__figure-alt {
    grid-column: 1 / span 5;
    grid-row: 2;
    margin-top: clamp(1.5rem, 3vw, 2.5rem);
  }
}


/* 13  PROCESS
   ========================================================================== */

.process { background: var(--bone-lit); }
.process__head { margin-bottom: clamp(2.5rem, 5vw, 4rem); }

/* numbered editorial sequence on a single continuous line */
.steps {
  position: relative;
  display: grid;
  gap: 0;
  border-top: 1px solid var(--rule);
}
.step {
  display: grid;
  grid-template-columns: 3.25rem 1fr;
  gap: var(--sp-4);
  align-items: start;
  /* Cells stretch to the tallest step in the row, and auto rows would share
     that spare height out between the number and the text. That pushed each
     title down by however many lines its description ran short, so the
     titles across a row never lined up. Pack them to the top instead. */
  align-content: start;
  padding-block: clamp(1.25rem, 2.4vw, 1.9rem);
  border-bottom: 1px solid var(--rule);
}
.step__num {
  font-size: 1.5rem;
  line-height: 1;
  color: var(--fg-faint);
  font-variant-numeric: tabular-nums lining-nums;
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 30;
}
.step__title { display: block; font-weight: 600; font-size: clamp(1.0625rem, 1.5vw, 1.1875rem); }
.step__desc { display: block; margin-top: 0.3rem; color: var(--fg-muted); font-size: var(--fs-small); max-width: 54ch; }

@media (min-width: 1000px) {
  /* four across, two full rows of four. Eight steps, no orphan cell. */
  .steps { grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 0; }
  .step {
    grid-template-columns: 1fr;
    gap: var(--sp-3);
    padding: clamp(1.5rem, 2vw, 2rem) clamp(1.1rem, 1.6vw, 1.5rem) clamp(2rem, 3vw, 2.75rem);
    border-top: 1px solid var(--rule);
    border-bottom: 0;
    border-right: 1px solid var(--rule);
  }
  /* Matching space either side of every vertical rule, so no number or line
     of text ends up pressed against one. The step that opens a row keeps its
     left edge flush instead, because that is the edge the heading and the
     paragraph above are set to. */
  .step:nth-child(4n + 1) { padding-left: 0; }
  .step:nth-child(4n) { border-right: 0; }
}

.process__aside {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  display: grid;
  gap: clamp(1.5rem, 3vw, 2.5rem);
}
.process__figure img { width: 100%; }
.process__points { display: grid; gap: var(--sp-4); align-content: center; }
.process__points li {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-3);
  font-size: var(--fs-small);
  color: var(--fg-muted);
}
.process__points b { color: var(--fg); font-weight: 600; }
.process__points .tick { color: var(--accent-fg); }

@media (min-width: 900px) {
  .process__aside { grid-template-columns: 1fr 1fr; align-items: center; }
}


/* 14  PRICING
   ========================================================================== */

.pricing__head { margin-bottom: clamp(2.5rem, 5vw, 4rem); }

/* editorial price table: hairlines and typography, not four identical cards */
.tiers { border-top: 1px solid var(--rule-strong); }
.tier {
  display: grid;
  gap: var(--sp-3);
  padding-block: clamp(1.75rem, 3vw, 2.5rem);
  border-bottom: 1px solid var(--rule);
}
.tier__name { font-size: var(--fs-display-4); line-height: 1; }
.tier__price {
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 60;
  font-variant-numeric: lining-nums;
  font-size: clamp(2rem, 4vw, 3.25rem);
  line-height: 0.95;
  letter-spacing: -0.02em;
}
.tier__price .from {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fg-faint);
  margin-bottom: 0.5rem;
}
.tier__for { color: var(--fg-muted); font-size: var(--fs-small); max-width: 34ch; }
.tier__list { display: grid; gap: 0.45rem; font-size: var(--fs-small); color: var(--fg-muted); }
.tier__list li { display: grid; grid-template-columns: auto 1fr; gap: 0.6rem; }
.tier__list .tick { color: var(--accent-fg); }
.tier__limits {
  font-size: var(--fs-tiny);
  color: var(--fg-faint);
  max-width: 34ch;
}

@media (min-width: 860px) {
  .tier {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr) minmax(0, 1.2fr) minmax(0, 1fr);
    gap: clamp(1.25rem, 2.5vw, 2.5rem);
    align-items: start;
  }
}

.pricing__note {
  margin-top: var(--sp-6);
  font-size: var(--fs-small);
  color: var(--fg-muted);
  max-width: 62ch;
}


/* 15  ESTIMATOR
   ========================================================================== */

.estimator { background: var(--stone-2); }
.estimator__head { margin-bottom: clamp(2rem, 4vw, 3rem); }

.est__progress {
  display: flex;
  gap: 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  margin-bottom: clamp(2rem, 4vw, 3rem);
  overflow-x: auto;
  scrollbar-width: none;
}
.est__progress::-webkit-scrollbar { display: none; }
.est__progress li {
  flex: 1 0 auto;
  min-width: 8.5rem;
  font-size: var(--fs-tiny);
  color: var(--fg-faint);
  position: relative;
}
/* each step label is a real button so the sequence is keyboard navigable */
.est__progress li button {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  width: 100%;
  min-height: 46px;
  padding: 0.85rem 0.75rem 0.85rem 0;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  transition: color var(--dur-fast) var(--ease);
}
.est__progress li button:hover { color: var(--fg); }

/* On narrow screens the five steps wrap onto two rows rather than scrolling
   sideways. A horizontally scrolling strip with no visible affordance hides
   steps 4 and 5, and every step here is a real navigation control. */
@media (max-width: 699px) {
  .est__progress { flex-wrap: wrap; overflow-x: visible; }
  .est__progress li { flex: 1 0 33.333%; min-width: 0; }
  .est__progress li button { padding-right: 0.4rem; font-size: 0.75rem; }
}
.est__progress li::before {
  content: '';
  position: absolute;
  left: 0; bottom: -1px;
  width: 0; height: 1px;
  background: var(--ember);
  transition: width var(--dur-slow) var(--ease);
}
.est__progress li.is-done::before,
.est__progress li.is-current::before { width: 100%; }
.est__progress li.is-current { color: var(--fg); font-weight: 600; }
.est__progress li.is-done { color: var(--fg-muted); }
.est__progress .n { font-variant-numeric: tabular-nums; }

.est__layout { display: grid; gap: clamp(2rem, 4vw, 3rem); align-items: start; }
@media (min-width: 1000px) {
  .est__layout { grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); }
  /* A sticky panel taller than the space under the nav pins its top and puts
     its own bottom permanently out of reach, so it gets to scroll itself. */
  .est__summary-wrap {
    position: sticky;
    top: calc(var(--nav-h) + 1.5rem);
    max-height: calc(100svh - var(--nav-h) - 3rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
  }
}

.est__step { display: none; }
.est__step.is-active { display: block; animation: stepIn var(--dur) var(--ease) both; }
@keyframes stepIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.est__legend {
  font-size: var(--fs-display-4);
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 40;
  letter-spacing: -0.015em;
  line-height: 1.15;
  padding: 0;
  margin-bottom: var(--sp-2);
}
.est__hint { color: var(--fg-muted); font-size: var(--fs-small); margin-bottom: var(--sp-5); max-width: 52ch; }

fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }

/* selectable option: the native input stays, it is just visually hidden so
   keyboard and screen reader behaviour is the real thing */
.opt { position: relative; display: block; }
.opt input {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.opt__box {
  display: grid;
  gap: 0.5rem;
  padding: clamp(1rem, 1.8vw, 1.35rem);
  border: 1px solid var(--rule);
  background: rgb(255 255 255 / 0.32);
  transition: border-color var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
  height: 100%;
}
.opt input:hover + .opt__box { border-color: var(--rule-strong); }
.opt input:checked + .opt__box {
  border-color: var(--ink-warm);
  background: var(--bone-lit);
  box-shadow: inset 0 0 0 1px var(--ink-warm);
}
.opt input:focus-visible + .opt__box { outline: 2px solid var(--focus); outline-offset: 2px; }
.opt input:disabled + .opt__box { opacity: 0.45; }
.opt input:disabled { cursor: not-allowed; }
.opt input:active + .opt__box { transform: translateY(1px); }

.opt__top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); }
.opt__name { font-weight: 600; font-size: 1.0625rem; }
.opt__price {
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 30;
  font-variant-numeric: lining-nums;
  font-size: 1.25rem;
  white-space: nowrap;
}
.opt__for { font-size: var(--fs-small); color: var(--fg-muted); }
.opt__inc { font-size: var(--fs-tiny); color: var(--fg-muted); }
.opt__lim { font-size: var(--fs-tiny); color: var(--fg-faint); }

.opt-grid { display: grid; gap: var(--sp-3); }
@media (min-width: 700px) { .opt-grid--2 { grid-template-columns: 1fr 1fr; } }
@media (min-width: 620px) { .opt-grid--pages { grid-template-columns: repeat(5, 1fr); } }

/* compact page-count buttons */
.opt--compact .opt__box {
  text-align: center;
  justify-items: center;
  padding: 0.9rem 0.5rem;
  min-height: 48px;
  align-content: center;
}
.opt--compact .opt__name { font-size: 1rem; }

/* add-on row: checkbox list with hairlines */
.addons { border-top: 1px solid var(--rule); }
/* display:block is load bearing. This is a <label>, so it is inline by default,
   and an inline box gives the absolutely positioned input inside it a containing
   block assembled from line boxes rather than the row. `inset: 0` then resolved
   to almost nothing, so all seven add-ons had an unhittable checkbox: no focus
   ring, dead hover, and no way to reach them with a keyboard. */
.addon { display: block; position: relative; border-bottom: 1px solid var(--rule); }
.addon input {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}
.addon__box {
  display: grid;
  grid-template-columns: 1.25rem 1fr auto;
  gap: var(--sp-3);
  align-items: start;
  padding-block: clamp(0.9rem, 1.6vw, 1.15rem);
  transition: background-color var(--dur-fast) var(--ease);
}
.addon input:hover + .addon__box { background: rgb(255 255 255 / 0.3); }
.addon input:focus-visible + .addon__box { outline: 2px solid var(--focus); outline-offset: -2px; }
.addon input:disabled + .addon__box { opacity: 0.42; }
.addon input:disabled { cursor: not-allowed; }

.addon__mark {
  width: 1.15rem; height: 1.15rem;
  border: 1px solid var(--field-rule);
  margin-top: 0.15rem;
  display: grid;
  place-items: center;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
.addon__mark::after {
  content: '';
  width: 0.5rem; height: 0.28rem;
  border-left: 1.5px solid var(--stone);
  border-bottom: 1.5px solid var(--stone);
  transform: rotate(-45deg) scale(0);
  transition: transform var(--dur-fast) var(--ease);
}
.addon input:checked + .addon__box .addon__mark { background: var(--ink-warm); border-color: var(--ink-warm); }
.addon input:checked + .addon__box .addon__mark::after { transform: rotate(-45deg) scale(1); }

.addon__name { display: block; font-weight: 600; font-size: 1rem; }
.addon__desc { display: block; font-size: var(--fs-tiny); color: var(--fg-muted); margin-top: 0.15rem; max-width: 46ch; }
.addon__price {
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 24;
  font-variant-numeric: lining-nums;
  font-size: 1.0625rem;
  white-space: nowrap;
}
.addon__included {
  font-size: var(--fs-tiny);
  color: var(--accent-fg);
  font-family: var(--font-sans);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-weight: 600;
}

/* recommendation / notice */
.est__notice {
  margin-top: var(--sp-4);
  padding: var(--sp-4);
  border: 1px solid var(--rule-strong);
  border-left: 2px solid var(--ember);
  background: rgb(255 255 255 / 0.4);
  font-size: var(--fs-small);
  display: grid;
  gap: var(--sp-3);
}
.est__notice p { margin: 0; }
.est__notice strong { font-weight: 600; }
.est__notice[hidden] { display: none; }
.est__notice-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.est__notice .btn { min-height: 40px; padding: 0.5rem 1rem; font-size: var(--fs-tiny); }

.est__nav {
  margin-top: clamp(1.75rem, 3vw, 2.5rem);
  display: flex;
  gap: var(--sp-3);
  flex-wrap: wrap;
  align-items: center;
}
.est__nav .spacer { flex: 1; }

/* summary panel */
.est__summary {
  border: 1px solid var(--rule-strong);
  background: var(--bone-lit);
  padding: clamp(1.25rem, 2.4vw, 1.75rem);
}
.est__summary h3 {
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fg-faint);
  font-weight: 600;
  margin-bottom: var(--sp-4);
}
.sum__rows { display: grid; gap: 0; margin-bottom: var(--sp-4); }
.sum__row {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-block: 0.55rem;
  border-bottom: 1px solid var(--rule);
  font-size: var(--fs-small);
}
.sum__row .k { color: var(--fg-muted); }
.sum__row .v { text-align: right; font-variant-numeric: tabular-nums; }
.sum__row--muted .v { color: var(--fg-muted); }

.sum__total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--rule-strong);
}
.sum__total .k { font-size: var(--fs-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--fg-faint); font-weight: 600; }
.sum__total .v {
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 72;
  font-variant-numeric: lining-nums tabular-nums;
  font-size: clamp(1.9rem, 4vw, 2.75rem);
  line-height: 1;
  letter-spacing: -0.02em;
}
.sum__monthly {
  margin-top: var(--sp-2);
  text-align: right;
  font-size: var(--fs-small);
  color: var(--fg-muted);
  font-variant-numeric: tabular-nums;
}
.sum__disclaimer {
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--rule);
  font-size: var(--fs-tiny);
  color: var(--fg-muted);
}
.sum__manual {
  margin-top: var(--sp-3);
  padding: var(--sp-3);
  border-left: 2px solid var(--ember);
  background: rgb(201 96 60 / 0.08);
  font-size: var(--fs-tiny);
}
.sum__manual[hidden] { display: none; }
.est__summary .btn { width: 100%; margin-top: var(--sp-4); }

/* value change pulse, driven by JS toggling a class */
.is-bumped { animation: bump var(--dur) var(--ease); }
@keyframes bump {
  0%   { opacity: 0.45; transform: translateY(-3px); }
  100% { opacity: 1; transform: none; }
}


/* 15b  QUESTIONS
   Native details/summary so it still opens and closes with JavaScript off.
   ========================================================================== */

.faq__head { margin-bottom: clamp(2rem, 4vw, 3rem); }
.faq__list { border-top: 1px solid var(--rule); }

.faq__item { border-bottom: 1px solid var(--rule); }
.faq__item summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-5);
  padding-block: clamp(1.1rem, 2vw, 1.5rem);
  cursor: pointer;
  list-style: none;
  font-size: clamp(1.0625rem, 1.5vw, 1.1875rem);
  font-weight: 600;
  color: var(--fg);
  transition: color var(--dur-fast) var(--ease);
}
.faq__item summary::-webkit-details-marker { display: none; }
.faq__item summary:hover { color: var(--accent-fg); }
.faq__item summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }

/* plus that becomes a minus, drawn from two rules rather than an icon font */
.faq__sign {
  position: relative;
  flex: none;
  width: 14px; height: 14px;
  margin-top: 0.35em;
}
.faq__sign::before,
.faq__sign::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 50%;
  height: 1.5px;
  background: currentColor;
  transition: transform var(--dur) var(--ease);
}
.faq__sign::after { transform: rotate(90deg); }
.faq__item[open] .faq__sign::after { transform: rotate(0deg); }

.faq__answer {
  padding-bottom: clamp(1.1rem, 2vw, 1.5rem);
  max-width: 66ch;
  color: var(--fg-muted);
  font-size: var(--fs-body);
  line-height: 1.6;
}
.faq__answer p + p { margin-top: 0.9em; }
.faq__answer a { color: var(--fg); }

@media (prefers-reduced-motion: no-preference) {
  .faq__item[open] .faq__answer { animation: faqIn var(--dur) var(--ease) both; }
  @keyframes faqIn {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: none; }
  }
}


/* 15c  PERSISTENT MOBILE CTA
   The page is long and the nav CTA is behind the hamburger on small screens,
   which leaves no visible route to the estimator while scrolling. Appears
   once the hero is passed, retires once the enquiry form is on screen so it
   never sits on top of the thing it points at.
   ========================================================================== */

.mobile-cta {
  position: fixed;
  left: var(--gutter);
  right: var(--gutter);
  bottom: 0.9rem;
  z-index: 95;
  display: none;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  min-height: 52px;
  padding: 0.8rem 1.25rem;
  background: var(--bone);
  color: var(--ink);
  text-decoration: none;
  font-size: var(--fs-small);
  font-weight: 600;
  letter-spacing: 0.01em;
  box-shadow: 0 12px 38px -12px rgb(7 10 12 / 0.75);
  opacity: 0;
  transform: translateY(150%);
  /* hidden state must not be clickable or focusable, including when
     JavaScript never runs and the class is never added */
  visibility: hidden;
  pointer-events: none;
  transition:
    opacity var(--dur) var(--ease),
    transform var(--dur) var(--ease),
    visibility 0s linear var(--dur);
}
.mobile-cta__note { font-weight: 500; font-size: var(--fs-tiny); opacity: 0.6; }
.mobile-cta.is-visible {
  opacity: 1;
  transform: none;
  visibility: visible;
  pointer-events: auto;
  transition:
    opacity var(--dur) var(--ease),
    transform var(--dur) var(--ease),
    visibility 0s linear 0s;
}
.mobile-cta:active { transform: translateY(1px); }

@media (max-width: 899px) { .mobile-cta { display: flex; } }


/* 16  CONTACT AND FORMS
   ========================================================================== */

.contact { position: relative; background: var(--ink); overflow: hidden; }
.contact__media { position: absolute; inset: 0; z-index: 0; }
.contact__media img { width: 100%; height: 100%; object-fit: cover; }
.contact__media::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(to bottom,
    var(--ink) 0%, rgb(10 14 17 / 0.9) 22%, rgb(10 14 17 / 0.95) 60%, var(--ink) 100%);
}
.contact > .wrap { position: relative; z-index: 1; }

.contact__layout { display: grid; gap: clamp(2rem, 5vw, 4rem); align-items: start; }
@media (min-width: 950px) {
  .contact__layout { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); }
}

.contact__aside .d2 { max-width: 14ch; }
.contact__mail {
  display: inline-block;
  margin-top: var(--sp-5);
  font-size: var(--fs-lead);
  text-decoration: none;
}

.form-grid { display: grid; gap: var(--sp-5); }
@media (min-width: 700px) { .form-grid--2 { grid-template-columns: 1fr 1fr; } }

.field { display: grid; gap: 0.4rem; }
.field > label {
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  font-weight: 600;
  color: var(--fg-muted);
}
.field .req { color: var(--ember-soft); }
.world--warm .field .req { color: var(--ember-deep); }

.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 0.85rem 0.95rem;
  min-height: 48px;
  background: var(--field-bg);
  border: 1px solid var(--field-rule);
  border-radius: var(--radius);
  color: var(--fg);
  font-size: 1rem;
  transition: border-color var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease);
}
.field textarea { min-height: 9rem; resize: vertical; line-height: 1.55; }
.field input::placeholder, .field textarea::placeholder { color: var(--fg-faint); opacity: 1; }
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--fg-faint); }

/* native select needs an explicit arrow once the background is restyled */
.field select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 1.15rem) 1.35rem, calc(100% - 0.78rem) 1.35rem;
  background-size: 0.375rem 0.375rem, 0.375rem 0.375rem;
  background-repeat: no-repeat;
  padding-right: 2.5rem;
}
/* the dropdown list itself is drawn by the OS, force readable colours */
.field select option { background: var(--ink-2); color: var(--bone); }
.world--warm .field select option { background: var(--bone-lit); color: var(--ink-warm); }

.field__hint { font-size: var(--fs-tiny); color: var(--fg-faint); }

.field__error {
  font-size: var(--fs-tiny);
  color: var(--ember-soft);
  font-weight: 600;
  min-height: 0;
}
.world--warm .field__error { color: var(--ember-deep); }
.field__error:empty { display: none; }
.field[data-invalid='true'] input,
.field[data-invalid='true'] select,
.field[data-invalid='true'] textarea { border-color: var(--ember); }

/* consent checkbox */
.consent { position: relative; display: grid; grid-template-columns: 1.25rem 1fr; gap: var(--sp-3); align-items: start; }
.consent input { width: 1.15rem; height: 1.15rem; margin: 0.2rem 0 0; accent-color: var(--ember); }
.consent label { font-size: var(--fs-small); color: var(--fg-muted); }
.consent a { color: var(--fg); }

/* estimator payload preview */
.config-preview {
  border: 1px solid var(--rule);
  background: rgb(244 241 234 / 0.04);
  padding: var(--sp-4);
}
.config-preview[hidden] { display: none; }
.config-preview h3 {
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fg-faint);
  font-weight: 600;
  margin-bottom: var(--sp-3);
}
.config-preview pre {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-tiny);
  color: var(--fg-muted);
  white-space: pre-wrap;
  line-height: 1.65;
}
.config-preview__clear {
  margin-top: var(--sp-3);
  background: none;
  border: 0;
  padding: 0;
  font-size: var(--fs-tiny);
  color: var(--fg-faint);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.form__foot { display: grid; gap: var(--sp-4); }
.form__submit { justify-self: start; min-width: 14rem; }

/* submit states */
.btn__spinner {
  width: 0.85em; height: 0.85em;
  border: 1.5px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.form__status { font-size: var(--fs-small); }
.form__status:empty { display: none; }
.form__status[data-state='success'] {
  border: 1px solid var(--rule-strong);
  border-left: 2px solid var(--ember);
  padding: var(--sp-4);
  color: var(--fg);
}
.form__status[data-state='error'] {
  border: 1px solid var(--ember);
  padding: var(--sp-4);
  color: var(--fg);
}
.form__status a { color: var(--ember-soft); }


/* 17  FOOTER
   ========================================================================== */

.site-footer {
  background: var(--bg-sunken);
  color: var(--bone);
  padding-block: clamp(3.5rem, 7vw, 6rem) var(--sp-6);
  border-top: 1px solid rgb(244 241 234 / 0.1);
}
.footer__top { display: grid; gap: clamp(2rem, 4vw, 3.5rem); }
@media (min-width: 850px) {
  .footer__top { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr); }
}

.footer__brand .wordmark { margin-bottom: var(--sp-4); }
.footer__line { color: var(--mist); max-width: 34ch; font-size: var(--fs-small); }
.footer__mail { display: inline-block; margin-top: var(--sp-5); font-size: var(--fs-lead); text-decoration: none; }

.footer__col h2 {
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--mist-dim);
  font-weight: 600;
  margin-bottom: var(--sp-4);
}
.footer__col li + li { margin-top: 0.55rem; }
.footer__col a { font-size: var(--fs-small); color: var(--mist); text-decoration: none; transition: color var(--dur-fast) var(--ease); }
.footer__col a:hover { color: var(--bone); }
.footer__col dl { display: grid; gap: 0.55rem; font-size: var(--fs-small); }
.footer__col dl div { display: flex; justify-content: space-between; gap: var(--sp-4); }
.footer__col dt { color: var(--mist-dim); }
.footer__col dd { margin: 0; color: var(--mist); }

.footer__cta {
  margin-top: clamp(3rem, 6vw, 5rem);
  padding-top: clamp(2rem, 4vw, 3rem);
  border-top: 1px solid rgb(244 241 234 / 0.12);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
}
.footer__cta .d3 { max-width: 18ch; }

.footer__legal {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: var(--sp-5);
  border-top: 1px solid rgb(244 241 234 / 0.1);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-6);
  justify-content: space-between;
  font-size: var(--fs-tiny);
  color: var(--mist-dim);
}
.footer__legal a { color: var(--mist-dim); text-decoration: none; }
.footer__legal a:hover { color: var(--mist); }


/* 18  LEGAL PAGES
   Same design system. These are part of the site, not an afterthought.
   ========================================================================== */

.page-head {
  position: relative;
  padding-top: calc(var(--nav-h) + clamp(4rem, 10vw, 8rem));
  padding-bottom: clamp(3rem, 6vw, 5rem);
  background: var(--ink);
  border-bottom: 1px solid var(--rule);
  overflow: hidden;
}
.page-head__media { position: absolute; inset: 0; z-index: 0; opacity: 0.62; }
.page-head__media img { width: 100%; height: 100%; object-fit: cover; }
.page-head__media::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(to bottom,
    rgb(10 14 17 / 0.5) 0%,
    rgb(10 14 17 / 0.78) 55%,
    var(--ink) 97%);
}
.page-head > .wrap { position: relative; z-index: 1; }
.page-head .d2 { margin-top: var(--sp-4); max-width: 18ch; }
.page-head__meta { margin-top: var(--sp-5); font-size: var(--fs-small); color: var(--mist); }

/* .legal sections carry .world--warm in the markup, which supplies the warm
   background AND the warm text tokens. Setting only background/color here
   would leave --fg-muted and --fg-faint on their dark world values, putting
   pale grey body copy on cream. */
.legal__layout { display: grid; gap: clamp(2rem, 4vw, 3.5rem); align-items: start; }
@media (min-width: 950px) {
  .legal__layout { grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); }
  /* The terms contents list runs to nineteen entries. Left uncapped it would
     be taller than a short viewport, and sticky would hold the overflowing
     tail permanently below the fold. */
  .legal__toc {
    position: sticky;
    top: calc(var(--nav-h) + 2rem);
    max-height: calc(100svh - var(--nav-h) - 3.5rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
  }
}

.legal__toc h2 {
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fg-faint);
  font-weight: 600;
  margin-bottom: var(--sp-4);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--rule);
}
.legal__toc li + li { margin-top: 0.5rem; }
.legal__toc a {
  font-size: var(--fs-small);
  color: var(--fg-muted);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.legal__toc a:hover { color: var(--fg); }

.prose { max-width: 70ch; }
.prose > section + section { margin-top: clamp(2.5rem, 5vw, 3.5rem); }
.prose h2 {
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 40;
  font-size: clamp(1.4rem, 2.4vw, 1.9rem);
  line-height: 1.2;
  letter-spacing: -0.015em;
  font-weight: 500;
  margin-bottom: var(--sp-4);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--rule);
}
.prose h3 {
  font-size: 1.0625rem;
  font-weight: 600;
  margin-top: var(--sp-5);
  margin-bottom: var(--sp-2);
}
.prose p, .prose li { color: var(--fg-muted); }
.prose p + p { margin-top: 1em; }
.prose ul + p { margin-top: 1.1em; }
.prose ul { margin-top: var(--sp-3); display: grid; gap: 0.6rem; }
/* Deliberately NOT a grid. These list items mix inline <strong> with running
   text, and a grid container promotes every inline child to its own grid
   item: the text after a <strong> lands in the narrow marker column and
   wraps a couple of characters at a time. Marker is absolutely positioned so
   the content stays in normal inline flow. */
.prose ul li {
  position: relative;
  padding-left: 1.35rem;
  font-size: var(--fs-small);
  line-height: 1.62;
}
.prose ul li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.78em;
  width: 7px;
  height: 1px;
  background: var(--fg-faint);
}
.prose a { color: var(--fg); }
.prose strong { color: var(--fg); font-weight: 600; }

/* inline technical strings, currently only hostnames in the privacy notice.
   The site has no monospace face, and adding one for two hostnames would be a
   design decision of its own, so these stay in the sans face a shade smaller.

   Marked with a tint rather than an underline on purpose: prose links are
   underlined in full strength ink, so an underlined hostname sitting in the
   same paragraph reads as a link and invites a click that goes nowhere.
   --field-bg is the right token because it means "small inset surface" in both
   worlds, sitting slightly lighter than whatever it is on.

   overflow-wrap matters: these are long unbroken strings inside a 70ch column
   and would otherwise push the page sideways on a narrow phone. */
.prose code {
  font-family: inherit;
  font-size: 0.9375em;
  color: var(--fg);
  background: var(--field-bg);
  /* kept tight horizontally: a wider inset leaves a visible gap between the
     chip and a following comma or full stop. */
  padding: 0.1em 0.24em;
  overflow-wrap: anywhere;
}

.prose__note {
  margin-top: clamp(2.5rem, 5vw, 3.5rem);
  padding: var(--sp-5);
  border: 1px solid var(--rule);
  border-left: 2px solid var(--ember);
  background: rgb(255 255 255 / 0.4);
  font-size: var(--fs-small);
}


/* 19  MOTION
   Scroll reveals are added by IntersectionObserver. If JS never runs the
   content is visible by default: .reveal only hides once .js is on <html>.
   ========================================================================== */

.js .reveal {
  opacity: 0;
  transform: translateY(22px);
}
.js .reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}

/* Image mask reveal: the photograph wipes up into place.
   The clip is applied to the <img>, never to the observed .reveal-mask
   element itself. Clipping the observed element to zero height drives its
   IntersectionObserver ratio to zero, so it would never cross the threshold
   and would stay hidden forever. */
.js .reveal-mask img { clip-path: inset(100% 0 0 0); }
.js .reveal-mask.is-in img {
  clip-path: inset(0 0 0 0);
  transition:
    clip-path 1.15s var(--ease) var(--reveal-delay, 0ms),
    transform 1.2s var(--ease);
}

/* hero entrance, runs on load rather than on scroll */
.js .hero__headline .hl-line,
.js .hero__sub,
.js .hero__actions,
.js .hero__foot {
  opacity: 0;
  transform: translateY(18px);
}
.js .hero.is-ready .hero__headline .hl-line,
.js .hero.is-ready .hero__sub,
.js .hero.is-ready .hero__actions,
.js .hero.is-ready .hero__foot {
  opacity: 1;
  transform: none;
  transition: opacity 1s var(--ease), transform 1s var(--ease);
}
.js .hero.is-ready .hero__headline .hl-line:nth-child(1) { transition-delay: 120ms; }
.js .hero.is-ready .hero__headline .hl-line:nth-child(2) { transition-delay: 220ms; }
.js .hero.is-ready .hero__headline .hl-line:nth-child(3) { transition-delay: 320ms; }
.js .hero.is-ready .hero__sub { transition-delay: 450ms; }
.js .hero.is-ready .hero__actions { transition-delay: 540ms; }
.js .hero.is-ready .hero__foot { transition-delay: 630ms; }

.js .site-nav { opacity: 0; transform: translateY(-10px); }
.js .site-nav.is-ready {
  opacity: 1; transform: none;
  transition: opacity 700ms var(--ease), transform 700ms var(--ease),
              background-color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

/* Reduced motion: everything resolves instantly, nothing loops. */
@media (prefers-reduced-motion: reduce) {
  html.scroll-smooth { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .js .reveal, .js .hero__headline .hl-line, .js .hero__sub,
  .js .hero__actions, .js .hero__foot, .js .site-nav {
    opacity: 1 !important;
    transform: none !important;
  }
  .js .reveal-mask img { clip-path: none !important; }
  .scroll-cue::after { display: none; }
  .svc-figure:hover img { transform: none; }
  /* the capability stack still steps, it just stops sliding while it does */
  .js .capstack .cap { transform: none !important; }
  /* projects assemble instantly, and the photographs hold still and whole */
  .js .project__figure img { transform: none !important; }
  .js .project .project__index,
  .js .project .project__title > span,
  .js .project .project__meta,
  .js .project .project__body,
  .js .project .project__tags li { opacity: 1 !important; transform: none !important; }
  .js .project__index::after { transform: scaleX(1) !important; }
}


/* --- responsive nav switch ------------------------------------------------ */
@media (max-width: 899px) {
  :root { --nav-h: 64px; }
  .nav-links, .nav-actions .nav-cta { display: none; }
  .nav-toggle { display: block; }
}
@media (min-width: 900px) {
  .nav-panel { display: none; }
}

/* ============================================================ LANGUAGE SWITCH
   Danish is the default in the markup; this only toggles to English. */
.lang-switch{
  display:inline-flex;
  align-items:stretch;
  border:1px solid rgba(255,255,255,.22);
  border-radius:2px;
  overflow:hidden;
  flex:none;
}
.lang-switch__btn{
  appearance:none;
  -webkit-appearance:none;
  background:transparent;
  border:0;
  margin:0;
  padding:.42rem .6rem;
  font:inherit;
  font-size:.7rem;
  letter-spacing:.14em;
  text-transform:uppercase;
  line-height:1;
  color:rgba(255,255,255,.6);
  cursor:pointer;
  transition:color .18s ease, background-color .18s ease;
}
.lang-switch__btn + .lang-switch__btn{border-left:1px solid rgba(255,255,255,.22)}
.lang-switch__btn:hover{color:#fff}
.lang-switch__btn.is-on{background:rgba(255,255,255,.92); color:#0A0E11}
.lang-switch__btn:focus-visible{outline:2px solid currentColor; outline-offset:-2px}

/* inside the mobile panel it sits above the email line and reads in full */
.lang-switch--panel{margin:0 0 1rem}
.lang-switch--panel .lang-switch__btn{padding:.55rem .9rem; font-size:.75rem}

/* on the warm sections the nav inverts, so follow it */
.site-nav.is-solid .lang-switch{border-color:rgba(255,255,255,.28)}

@media (max-width:900px){
  .nav-actions .lang-switch{display:none}
}
@media (min-width:901px){
  .lang-switch--panel{display:none}
}


/* 20  ATMOSPHERE
   Added 17 August 2026. Five additions, each with a job rather than a flourish:
   an ambient drift behind the dark sections so they are not flat black, a
   heading reveal that wipes instead of sliding, a reading indicator in the nav,
   a nav that gets out of the way, and a hero that breathes.

   Everything here animates transform, opacity or clip-path only. Nothing in
   this section triggers layout, and every looping animation is off under
   prefers-reduced-motion via the block in section 19.
   ========================================================================== */

/* --- ambient drift -------------------------------------------------------
   Two very wide, very faint radial washes that move against each other. The
   point is that a dark section should look like a lit room rather than a
   switched off screen. At 0.10 alpha over #0A0E11 this is felt more than seen,
   which is the intent — turn it up and it reads as a gradient, which is
   exactly the cheap look this is avoiding.

   The gradients sit on a pseudo element so no markup carries them, and the
   section's own .wrap is lifted to position: relative so content paints above
   without needing a z-index anywhere. */
.ambient { position: relative; isolation: isolate; }
.ambient > .wrap { position: relative; }

.ambient::before {
  content: '';
  position: absolute;
  inset: -25% -12%;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(38% 44% at 24% 26%, rgb(201 96 60 / 0.10), transparent 68%),
    radial-gradient(36% 40% at 76% 74%, rgb(122 152 178 / 0.09), transparent 70%);
  will-change: transform;
}
@media (prefers-reduced-motion: no-preference) {
  .ambient::before { animation: drift 52s var(--ease-inout) infinite alternate; }
  @keyframes drift {
    from { transform: translate3d(-2.4%, -1.6%, 0) scale(1); }
    to   { transform: translate3d( 2.4%,  1.6%, 0) scale(1.14); }
  }
}

/* --- display headings wipe, they do not slide ----------------------------
   The generic .reveal in section 19 moves everything 22px up. Used on a
   Bodoni headline that reads as the same fade-up every other site ships, so
   display type gets its own treatment: the block wipes open from the top while
   rising a fraction of its own size.

   The insets are negative on purpose. clip-path clips the border box, and a
   Didone's ascenders and descenders overshoot theirs — inset(0 ...) would
   shave the top off the very letters this is meant to show. */
.js .display.reveal {
  opacity: 1;
  transform: translateY(0.16em);
  clip-path: inset(-0.3em 0 100% 0);
}
.js .display.reveal.is-in {
  transform: none;
  clip-path: inset(-0.3em 0 -0.35em 0);
  transition:
    clip-path 1.05s var(--ease) var(--reveal-delay, 0ms),
    transform 1.05s var(--ease) var(--reveal-delay, 0ms);
}

/* --- reading indicator ---------------------------------------------------
   A one pixel rule under the nav showing how far through the page you are.
   It only appears once the nav has gone solid, so it never sits on top of the
   hero photograph. --read is written by main.js. */
.nav-progress {
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  transform: scaleX(var(--read, 0));
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--ember) 0%, var(--ember-soft) 100%);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
  pointer-events: none;
}
/* --solid is the legal pages, which are solid from the first pixel and have no
   hero to keep clear of. */
.site-nav.is-solid .nav-progress,
.site-nav--solid .nav-progress { opacity: 1; }

/* --- the nav gets out of the way ----------------------------------------
   Scrolling down retires it, scrolling up brings it straight back. On a page
   built around full width editorial type, 72px of permanent chrome is 72px the
   photographs and headlines do not get. */
.site-nav.is-tucked { transform: translateY(-100%); }
.js .site-nav.is-ready.is-tucked { transform: translateY(-100%); }
/* Both lists restate backdrop-filter from the base rule in section 06. It is
   what fades the blur in as the nav goes solid, and re-declaring transition
   here would otherwise silently drop it. */
.site-nav { transition: background-color var(--dur) var(--ease),
                        border-color var(--dur) var(--ease),
                        backdrop-filter var(--dur) var(--ease),
                        transform 480ms var(--ease); }
.js .site-nav.is-ready {
  transition: opacity 700ms var(--ease), transform 480ms var(--ease),
              background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              backdrop-filter var(--dur) var(--ease);
}
/* never tuck while the mobile menu is open — the close button lives in it */
body.is-locked .site-nav { transform: none !important; }

/* --- the hero breathes ---------------------------------------------------
   A 30 second scale from 1 to 1.05 and back. Slow enough that it reads as
   depth rather than as movement; you notice it if you sit on the page, not
   while you are reading the headline. */
@media (prefers-reduced-motion: no-preference) {
  .hero__media img {
    will-change: transform;
    animation: breathe 30s var(--ease-inout) infinite alternate;
  }
  @keyframes breathe {
    from { transform: scale(1); }
    to   { transform: scale(1.05); }
  }
}

/* --- primary button fills rather than flicks -----------------------------
   The colour change was instantaneous on a 180ms background-color transition.
   Sweeping a fill across from the left costs the same and reads as deliberate. */
@media (hover: hover) and (pointer: fine) {
  .btn--primary { position: relative; overflow: hidden; isolation: isolate; }
  .btn--primary::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: var(--ember);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 420ms var(--ease);
  }
  .btn--primary:hover::before,
  .btn--primary:focus-visible::before { transform: scaleX(1); }
  .btn--primary:hover,
  .btn--primary:focus-visible {
    background: var(--btn-bg);
    border-color: var(--ember);
    color: var(--bone);
  }
}

/* 21  WHATSAPP
   --------------------------------------------------------------------------
   The mail address and the WhatsApp link are the same kind of thing, so they
   stack as one pair wherever contact details appear. The first child keeps
   whatever margin it already had, which leaves the old spacing untouched.
   ========================================================================== */
.contact-pair {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-3);
}
.wa-link { display: inline-flex; align-items: center; gap: 0.5em; }
.wa-ico { width: 1em; height: 1em; fill: currentColor; flex: none; opacity: 0.9; }
.contact__wa, .footer__wa { font-size: var(--fs-lead); }

/* submit and WhatsApp sit side by side, and wrap on a narrow phone */
.form__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-4); }
.form__wa { text-decoration: none; }


/* --- reduced motion ------------------------------------------------------
   Section 19 already flattens durations globally. These are the states that
   also have to resolve, or the element would sit in its hidden position. */
@media (prefers-reduced-motion: reduce) {
  .js .display.reveal { clip-path: none !important; transform: none !important; }
  .ambient::before { animation: none !important; }
  .hero__media img { animation: none !important; transform: none !important; }
  .site-nav.is-tucked { transform: none !important; }
}
